【问题标题】:How to create scrollable page of carousels in Android?如何在 Android 中创建可滚动的轮播页面?
【发布时间】:2015-01-20 13:46:26
【问题描述】:

我正在尝试为我的 Android 应用程序构建一个 UI,其中包含一个可垂直滚动的水平滚动轮播页面(类似于 Netflix 应用程序所做的)。这种行为是如何完成的?

一个基本的实现就足以让我开始。 UI 还有一些其他要求,我将在此处列出以供参考,因为它可能会影响我可以使用的类或库。

1) 轮播之间的垂直滚动应该是平滑的,但是当用户释放时,UI 应该“捕捉”到最近的轮播(因此用户总是在轮播行上,而不是在两个轮播之间)。 em>

2) 轮播上的水平滚动应该是平滑的,但是当用户释放时,UI 应该“捕捉”到轮播中最近的项目。

3) 应该可以在轮播中的项目上覆盖附加信息

4) UI 应该适应任何屏幕尺寸。

5) 应该可以使用箭头键导航(对于无触摸屏设备)

6) 应该适用于各种 Android 版本(可能通过支持库)

7) 应该可以在 GPL 许可的开源应用中使用

可接受的答案不必满足所有这些要求。一个好的答案至少应该涉及导航多个轮播(而不是只有一个轮播)。

这是我所设想的基本模型(我很灵活,不必看起来像这样......重点只是为了澄清我在说什么——每一行都包含很多项目可以左右滚动,整个页面可以上下滚动)

【问题讨论】:

    标签: android android-layout carousel


    【解决方案1】:

    主要思想

    为了拥有灵活的设计和无限项,您可以创建RecyclerView 作为根视图,LinearLayoutManager.VERTICAL 作为LayoutManager。对于每一行,您可以添加另一个RecyclerView,但现在将LinearLayoutManager.HORIZONTAL 用作LayoutManager

    结果

    来源

    Code

    要求

    1) 轮播之间的垂直滚动应该是平滑的,但是当 用户发布时,UI 应该“捕捉到”最近的轮播(所以 用户总是在轮播行上,而不是在两个轮播之间)。

    2) 轮播上的水平滚动应该是平滑的,但是当用户 发布时,用户界面应该“捕捉”到轮播中最近的项目。

    为了实现我使用OnScrollListener 的那些,当状态变为 SCROLL_STATE_IDLE 时,我检查顶部和底部视图以查看其中哪些具有更多可见区域,然后滚动到该位置。对于每一行,我对每个行适配器的左视图和右视图都这样做。这样,您的轮播或行的一侧总是适合。例如,如果顶部安装了底部,则不安装,反之亦然。我认为如果你多玩一点,你可以做到这一点,但你必须知道窗口的尺寸并在运行时更改轮播的尺寸。

    3) 应该可以在项目上覆盖附加信息 在轮播中

    如果您使用RelativeLayoutFrameLayout 作为每个项目的根视图,您可以将信息放在彼此之上。如您所见,数字位于图像的顶部。

    4) UI 应该适应任何屏幕尺寸。

    如果您知道如何支持多种屏幕尺寸,您可以轻松做到这一点,如果您不知道请阅读文档。 Supporting Multiple Screens

    5) 应该可以使用箭头键导航(对于无触摸屏 设备)

    使用下面的函数

    mRecyclerView.scrollToPosition(position);
    

    6) 应该适用于各种 Android 版本(可能通过 支持库)

    导入android.support.v7.widget.RecyclerView;

    7) 应该可以在 GPL 许可的开源应用中使用

    好的

    编码愉快!!

    【讨论】:

    • 虽然这里有很多可以接受的答案,但我还是决定接受你的。它不仅彻底回答了原始问题,而且解决了我的每一个附加要求,并考虑了显示无限项目的能力(我最初发布问题时没有考虑到这一点)。在几天内授予此奖励后,我仍将继续履行我的承诺,再将赏金翻倍。
    • @paulscode :给他22小时后结束的赏金
    • @kaushik,完成。 (如果超时,它无论如何都会自动应用,因为他是发布 100 代表赏金后给出的唯一 2+ 答案)
    • 有人可以建议我如何将图块的 onitem 单击引用返回到 Main Activiy。我被困在那里了。
    【解决方案2】:

    您可以将 ListView 与自定义 OnTouchListener(用于捕捉项目)一起用于垂直滚动,并再次将 TwoWayGridView 与自定义 OnTouchListener(用于捕捉项目)一起使用

    ma​​in.xml

    <ListView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/containerList"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:background="#E8E8E8"
        android:divider="@android:color/transparent"
        android:dividerHeight="16dp" />
    

    list_item_hgrid.xml

    <com.jess.ui.TwoWayGridView xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/grid"
        android:layout_width="match_parent"
        android:layout_height="160dp"
        android:layout_marginBottom="16dp"
        app:cacheColorHint="#E8E8E8"
        app:columnWidth="128dp"
        app:gravity="center"
        app:horizontalSpacing="16dp"
        app:numColumns="auto_fit"
        app:numRows="1"
        app:rowHeight="128dp"
        app:scrollDirectionLandscape="horizontal"
        app:scrollDirectionPortrait="horizontal"
        app:stretchMode="spacingWidthUniform"
        app:verticalSpacing="16dp" />
    

    Activity 代码如下所示

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.test);
    
        ListView containerList = (ListView) findViewById(R.id.containerList);
        containerList.setAdapter(new DummyGridsAdapter(this));
        containerList.setOnTouchListener(mContainerListOnTouchListener);
    }
    
    private View.OnTouchListener mContainerListOnTouchListener = new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    View itemView = ((ListView) view).getChildAt(0);
                    int top = itemView.getTop();
                    if (Math.abs(top) >= itemView.getHeight() / 2) {
                        top = itemView.getHeight() - Math.abs(top);
                    }
    
                    ((ListView) view).smoothScrollBy(top, 400);
            }
    
            return false;
        }
    };
    

    这里是测试适配器

    private static class DummyGridsAdapter extends BaseAdapter {
    
        private Context mContext;
    
        private TwoWayGridView[] mChildGrid;
    
        public DummyGridsAdapter(Context context) {
            mContext = context;
    
            mChildGrid = new TwoWayGridView[getCount()];
            for (int i = 0; i < mChildGrid.length; i++) {
                mChildGrid[i] = (TwoWayGridView) LayoutInflater.from(context).
                        inflate(R.layout.list_item_hgrid, null);
                mChildGrid[i].setAdapter(new DummyImageAdapter(context));
                mChildGrid[i].setOnTouchListener(mChildGridOnTouchListener);
            }
        }
    
        @Override
        public int getCount() {
            return 8;
        }
    
        @Override
        public Object getItem(int position) {
            return position;
        }
    
        @Override
        public long getItemId(int position) {
            return 0;
        }
    
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            return mChildGrid[position];
        }
    
        private View.OnTouchListener mChildGridOnTouchListener = new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_UP:
                        View itemView = ((TwoWayGridView) view).getChildAt(0);
                        int left = itemView.getLeft();
                        if (Math.abs(left) >= itemView.getWidth() / 2) {
                            left = itemView.getWidth() - Math.abs(left);
                        }
    
                        ((TwoWayGridView) view).smoothScrollBy(left, 400);
                }
    
                return false;
            }
        };
    
    }
    
    private static class DummyImageAdapter extends BaseAdapter {
    
        private Context mContext;
    
        private final int mDummyViewWidthHeight;
    
        public DummyImageAdapter(Context context) {
            mContext = context;
    
            mDummyViewWidthHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 128,
                    context.getResources().getDisplayMetrics());
        }
    
        @Override
        public int getCount() {
            return 16;
        }
    
        @Override
        public Object getItem(int position) {
            int component = (getCount() - position - 1) * 255 / getCount();
            return Color.argb(255, 255, component, component);
        }
    
        @Override
        public long getItemId(int position) {
            return 0;
        }
    
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ImageView imageView = new ImageView(mContext);
            imageView.setBackgroundColor((Integer) getItem(position));
            imageView.setLayoutParams(new TwoWayGridView.LayoutParams(mDummyViewWidthHeight, mDummyViewWidthHeight));
            return imageView;
        }
    
    }
    

    【讨论】:

    • 另一个非常详细的答案!今晚我将尝试并比较所有建议,然后确定最适合我的需求。
    【解决方案3】:

    我建议使用 Recycler 视图。

    您可以创建水平和垂直列表或网格视图。在我看来,viewpager 有时会变得复杂。

    我正在开发视频点播应用程序,这救了我。

    在您的情况下,设置起来很容易。我会给你一些代码。

    您将需要以下内容:
    XML 视图 - 声明回收布局的位置。
    适配器 - 您需要一个视图来填充适配器并填充回收视图。

    创建视图

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycle_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="none"
        android:orientation="horizontal"
        android:gravity="center"
        android:overScrollMode="never"/>
    

    在此处声明您希望轮播显示的位置。

    接下来要创建适配器:

    public class HorizontalCarouselItemAdapter extends RecyclerView.Adapter<HorizontalCarouselItemAdapter.ViewHolder> {
    
        List<objects> items;
        int itemLayout;
    
        public HorizontalCarouselItemAdapter(Context context, int itemLayout, List<objects> items) {
            this.context = context;
            this.itemLayout = itemLayout;
            this.items = items;
    
        }
    
        @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View v = LayoutInflater.from(parent.getContext()).inflate(itemLayout, parent, false);
            return new ViewHolder(v);
        }
    
    
        @Override public void onBindViewHolder(final ViewHolder holder, final int position) {
    
            this.holders = holder;
            final GenericAsset itemAdapter = items.get(position);
            holder.itemImage.setDrawable //manipulate variables here
    
    
        }
    
    
        @Override public int getItemCount() {
            return items.size();
        }
    
        public static class ViewHolder extends RecyclerView.ViewHolder {
            public ImageView itemImage;
    
    
    
            public ViewHolder(View itemView) {
                super(itemView);
                itemImage = (ImageView) itemView.findViewById(R.id.carousel_cell_holder_image);
    
    
            }
        }
    

    这是您将数据提供给适配器以填充每个轮播项目的地方。
    最后声明它并调用适配器:

    recyclerView = (RecyclerView)findViewById(R.id.recycle_view);
    ListLayoutManager manager = new ListLayoutManager(getApplication(), ListLayoutManager.Orientation.HORIZONTAL);
    recyclerView.setLayoutManager(manager);
    
    CustomAdpater adapter = new CustomAdapter(getApplication(), data);
    recyclerView.setAdapter(adapter);
    

    您可以创建一个带有回收视图的列表视图来实现您想要的。
    这个类非常适合平滑滚动和内存优化。

    这是它的链接:

    https://developer.android.com/reference/android/support/v7/widget/RecyclerView.html

    希望对你有帮助。

    【讨论】:

    • 感谢您的详细回答。我一定会看看这个!
    • 不用担心,如果您对此有任何疑问,请告诉我!
    【解决方案4】:

    您可以在 ScrollView 中使用 ScrollView 作为父级,在 for loop 中放置 Vertical LinearLayout 为包含 coverflow 的布局膨胀以实现 轮播效果

    【讨论】:

    • 这是向轮播添加效果的一个很好的例子。我更倾向于寻找所选页面比上一页/下一页略大的效果,而不是像这样使用旋转。但它仍然是一个很好的参考例子。谢谢!
    • 好的,我会尝试实现,如果我成功了会通知你
    【解决方案5】:

    不久前我需要类似的东西,我只是用过那个:https://github.com/simonrob/Android-Horizontal-ListView

    简单、强大、可定制。

    我的版本示例:

    public class HorizontalListView extends AdapterView<ListAdapter> {
    
    public boolean mAlwaysOverrideTouch = true;
    protected ListAdapter mAdapter;
    private int mLeftViewIndex = -1;
    private int mRightViewIndex = 0;
    protected int mCurrentX;
    protected int mNextX;
    private int mMaxX = Integer.MAX_VALUE;
    private int mDisplayOffset = 0;
    protected Scroller mScroller;
    private GestureDetector mGesture;
    private Queue<View> mRemovedViewQueue = new LinkedList<View>();
    private OnItemSelectedListener mOnItemSelected;
    private OnItemClickListener mOnItemClicked;
    private OnItemLongClickListener mOnItemLongClicked;
    private boolean mDataChanged = false;
    
    
    public HorizontalListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }
    
    private synchronized void initView() {
        mLeftViewIndex = -1;
        mRightViewIndex = 0;
        mDisplayOffset = 0;
        mCurrentX = 0;
        mNextX = 0;
        mMaxX = Integer.MAX_VALUE;
        mScroller = new Scroller(getContext());
        mGesture = new GestureDetector(getContext(), mOnGesture);
    }
    
    @Override
    public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener) {
        mOnItemSelected = listener;
    }
    
    @Override
    public void setOnItemClickListener(AdapterView.OnItemClickListener listener) {
        mOnItemClicked = listener;
    }
    
    @Override
    public void setOnItemLongClickListener(AdapterView.OnItemLongClickListener listener) {
        mOnItemLongClicked = listener;
    }
    
    private DataSetObserver mDataObserver = new DataSetObserver() {
    
        @Override
        public void onChanged() {
            synchronized (HorizontalListView.this) {
                mDataChanged = true;
            }
            invalidate();
            requestLayout();
        }
    
        @Override
        public void onInvalidated() {
            reset();
            invalidate();
            requestLayout();
        }
    
    };
    
    @Override
    public ListAdapter getAdapter() {
        return mAdapter;
    }
    
    @Override
    public View getSelectedView() {
        //TODO: implement
        return null;
    }
    
    @Override
    public void setAdapter(ListAdapter adapter) {
        if (mAdapter != null) {
            mAdapter.unregisterDataSetObserver(mDataObserver);
        }
        mAdapter = adapter;
        mAdapter.registerDataSetObserver(mDataObserver);
        reset();
    }
    
    private synchronized void reset() {
        initView();
        removeAllViewsInLayout();
        requestLayout();
    }
    
    @Override
    public void setSelection(int position) {
        //TODO: implement
    }
    
    private void addAndMeasureChild(final View child, int viewPos) {
        LayoutParams params = child.getLayoutParams();
        if (params == null) {
            params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
        }
    
        addViewInLayout(child, viewPos, params, true);
        child.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
                MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));
    }
    
    @Override
    protected synchronized void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    
        if (mAdapter == null) {
            return;
        }
    
        if (mDataChanged) {
            int oldCurrentX = mCurrentX;
            initView();
            removeAllViewsInLayout();
            mNextX = oldCurrentX;
            mDataChanged = false;
        }
    
        if (mScroller.computeScrollOffset()) {
            mNextX = mScroller.getCurrX();
        }
    
        if (mNextX <= 0) {
            mNextX = 0;
            mScroller.forceFinished(true);
        }
        if (mNextX >= mMaxX) {
            mNextX = mMaxX;
            mScroller.forceFinished(true);
        }
    
        int dx = mCurrentX - mNextX;
    
        removeNonVisibleItems(dx);
        fillList(dx);
        positionItems(dx);
    
        mCurrentX = mNextX;
    
        if (!mScroller.isFinished()) {
            post(new Runnable() {
                @Override
                public void run() {
                    requestLayout();
                }
            });
    
        }
    }
    
    private void fillList(final int dx) {
        int edge = 0;
        View child = getChildAt(getChildCount() - 1);
        if (child != null) {
            edge = child.getRight();
        }
        fillListRight(edge, dx);
    
        edge = 0;
        child = getChildAt(0);
        if (child != null) {
            edge = child.getLeft();
        }
        fillListLeft(edge, dx);
    
    
    }
    
    private void fillListRight(int rightEdge, final int dx) {
        while (rightEdge + dx < getWidth() && mRightViewIndex < mAdapter.getCount()) {
    
            View child = mAdapter.getView(mRightViewIndex, mRemovedViewQueue.poll(), this);
            addAndMeasureChild(child, -1);
            rightEdge += child.getMeasuredWidth();
    
            if (mRightViewIndex == mAdapter.getCount() - 1) {
                mMaxX = mCurrentX + rightEdge - getWidth();
            }
    
            if (mMaxX < 0) {
                mMaxX = 0;
            }
            mRightViewIndex++;
        }
    
    }
    
    private void fillListLeft(int leftEdge, final int dx) {
        while (leftEdge + dx > 0 && mLeftViewIndex >= 0) {
            View child = mAdapter.getView(mLeftViewIndex, mRemovedViewQueue.poll(), this);
            addAndMeasureChild(child, 0);
            leftEdge -= child.getMeasuredWidth();
            mLeftViewIndex--;
            mDisplayOffset -= child.getMeasuredWidth();
        }
    }
    
    private void removeNonVisibleItems(final int dx) {
        View child = getChildAt(0);
        while (child != null && child.getRight() + dx <= 0) {
            mDisplayOffset += child.getMeasuredWidth();
            mRemovedViewQueue.offer(child);
            removeViewInLayout(child);
            mLeftViewIndex++;
            child = getChildAt(0);
    
        }
    
        child = getChildAt(getChildCount() - 1);
        while (child != null && child.getLeft() + dx >= getWidth()) {
            mRemovedViewQueue.offer(child);
            removeViewInLayout(child);
            mRightViewIndex--;
            child = getChildAt(getChildCount() - 1);
        }
    }
    
    private void positionItems(final int dx) {
        if (getChildCount() > 0) {
            mDisplayOffset += dx;
            int left = mDisplayOffset;
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                int childWidth = child.getMeasuredWidth();
                child.layout(left, 0, left + childWidth, child.getMeasuredHeight());
                left += childWidth;
            }
        }
    }
    
    public synchronized void scrollTo(int x) {
        mScroller.startScroll(mNextX, 0, x - mNextX, 0);
        requestLayout();
    }
    
    public synchronized void scrollToChild(int position) {
        //TODO
        requestLayout();
    }
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return mGesture.onTouchEvent(ev);
    }
    
    protected boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                              float velocityY) {
        synchronized (HorizontalListView.this) {
            mScroller.fling(mNextX, 0, (int) -velocityX, 0, 0, mMaxX, 0, 0);
        }
        requestLayout();
    
        return true;
    }
    
    protected boolean onDown(MotionEvent e) {
        mScroller.forceFinished(true);
        return true;
    }
    
    private OnGestureListener mOnGesture = new GestureDetector.SimpleOnGestureListener() {
    
        @Override
        public boolean onDown(MotionEvent e) {
            return HorizontalListView.this.onDown(e);
        }
    
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                               float velocityY) {
            return HorizontalListView.this.onFling(e1, e2, velocityX, velocityY);
        }
    
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2,
                                float distanceX, float distanceY) {
    
            synchronized (HorizontalListView.this) {
                mNextX += (int) distanceX;
            }
            requestLayout();
    
            return true;
        }
    
        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            Rect viewRect = new Rect();
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                int left = child.getLeft();
                int right = child.getRight();
                int top = child.getTop();
                int bottom = child.getBottom();
                viewRect.set(left, top, right, bottom);
                if (viewRect.contains((int) e.getX(), (int) e.getY())) {
                    if (mOnItemClicked != null) {
                        mOnItemClicked.onItemClick(HorizontalListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId(mLeftViewIndex + 1 + i));
                    }
                    if (mOnItemSelected != null) {
                        mOnItemSelected.onItemSelected(HorizontalListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId(mLeftViewIndex + 1 + i));
                    }
                    break;
                }
            }
            return true;
        }
    
        @Override
        public void onLongPress(MotionEvent e) {
            Rect viewRect = new Rect();
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                int left = child.getLeft();
                int right = child.getRight();
                int top = child.getTop();
                int bottom = child.getBottom();
                viewRect.set(left, top, right, bottom);
                if (viewRect.contains((int) e.getX(), (int) e.getY())) {
                    if (mOnItemLongClicked != null) {
                        mOnItemLongClicked.onItemLongClick(HorizontalListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId(mLeftViewIndex + 1 + i));
                    }
                    break;
                }
            }
        }
    };
    }
    

    这是 XML:

                 <com.example.package.widgets.HorizontalListView
                    android:id="@+id/horizontal_listview"
                    android:layout_marginTop="30dp"
                    android:layout_marginLeft="10dp"
                    android:layout_marginRight="10dp"
                    android:layout_width="fill_parent"
                    android:layout_height="80dp"
                    android:background="@color/light_gray"
                    />
    

    在 OnCreate 中:

    mAdapter = new ArrayAdapter<Uri>(this, R.layout.viewitem) {
    
            @Override
            public int getCount() {
                return listUriAdapter.size();
            }
    
            @Override
            public Uri getItem(int position) {
                return listUriAdapter.get(position);
            }
    
            @Override
            public long getItemId(int position) {
                return 0;
            }
    
            @Override
            public View getView(final int position, View convertView, ViewGroup parent) {
                // do what you have to do
                return retval;
            }
        };
        onItemClickListener = new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
    
            }
        };
        onItemLongClickListener = new AdapterView.OnItemLongClickListener() {
            @Override
            public boolean onItemLongClick(AdapterView<?> adapterView, View view, int i, long l) {
    
                return false;
            }
        };
        horizontalListView.setOnItemClickListener(onItemClickListener);
        horizontalListView.setOnItemLongClickListener(onItemLongClickListener);
        horizontalListView.setAdapter(mAdapter);
    

    【讨论】:

    • 该课程看起来也很有希望,感谢您指出。有没有同时涉及水平和垂直滚动的示例?
    • 我只将它用于一个水平滚动视图,但我想您可以在某种 EndlessAdapter 中将几个堆叠在另一个之上。 survivingwithandroid.com/2013/10/…
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-02
    • 1970-01-01
    相关资源
    最近更新 更多