如何在 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) 应该可以在项目上覆盖附加信息 在轮播中
如果您使用RelativeLayout
或FrameLayout
作为每个项目的根视图,您可以将信息放在彼此之上。如您所见,数字位于图像的顶部。
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(用于捕捉项目)一起使用
main.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
的布局膨胀以实现 轮播效果
【讨论】:
这是向轮播添加效果的一个很好的例子。我更倾向于寻找所选页面比上一页/下一页略大的效果,而不是像这样使用旋转。但它仍然是一个很好的参考例子。谢谢! 好的,我会尝试实现,如果我成功了会通知你【参考方案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 中创建可滚动的轮播页面?的主要内容,如果未能解决你的问题,请参考以下文章