如何在 Android 中创建可滚动的轮播页面?

Posted

技术标签:

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

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

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

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

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

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

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

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

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

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

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

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

【问题讨论】:

【参考方案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_
    android:layout_
    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_
    android:layout_
    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_
    android:layout_
    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 的布局膨胀以实现 轮播效果

github link of android-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_
                android:layout_
                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/…

以上是关于如何在 Android 中创建可滚动的轮播页面?的主要内容,如果未能解决你的问题,请参考以下文章

如何在没有滚动条的 Tailwind 中创建可滚动元素

如何在颤动中创建可滚动的行

如何在 python kivy 中创建可滚动的 FloatLayout

解决纵向滚屏导致的轮播图异常

用原生的javascript 实现一个无限滚动的轮播图

如何在 vuetify 中创建可滚动的 v-list?