我正在尝试为我的Android应用程序构建一个UI,其中包含一个可垂直滚动的可水平滚动轮播页面(类似于Netflix应用程序的功能)。这种行为是如何完成的?
一个基本的实现就足以让我开始。 UI还有一些其他要求,我将在此处作为参考,因为它可能会影响我可以使用的类或库。
1)旋转木马之间的垂直滚动应该是平滑的,但是当用户释放时,UI应该“捕捉到”最近的旋转木马(因此用户总是在旋转木马行上,而不是在两个旋转木马之间)。
2)旋转木马上的水平滚动应该是平滑的,但是当用户发布时,UI应该“捕捉”旋转木马中最近的项目。
3)应该可以在转盘中的项目上叠加其他信息
4)UI应适用于任何屏幕尺寸。
5)应使用箭头键导航(适用于无触摸屏的设备)
6)应该适用于各种Android版本(可能通过支持库)
7)可以在根据GPL许可的开源应用程序中使用
可接受的答案不必满足所有这些要求。至少,一个好的答案应该包括导航多个轮播(相对于一个轮播)。
这是一个基本上我想象的模型(我很灵活,不必看起来像这样......点只是为了澄清我在说什么 - 每行都包含很多可以左右滚动的项目,整个页面可以上下滚动)
答案 0 :(得分:47)
为了拥有灵活的设计并拥有无限制的商品,您可以使用RecyclerView
作为LinearLayoutManager.VERTICAL
创建LayoutManager
作为根视图。对于每一行,您可以添加另一行RecyclerView
但现在使用LinearLayoutManager.HORIZONTAL
作为LayoutManager
。
1)旋转木马之间的垂直滚动应该是平滑的,但是何时 用户发布时,UI应该“捕捉到”最近的轮播(所以 用户总是在旋转木马行上,而不是在两个旋转木马之间。
2)旋转木马上的水平滚动应该是平滑的,但是当用户使用时 发布时,UI应该“捕捉”旋转木马中最近的项目。
为了实现我使用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许可的开源应用程序中使用
确定强>
快乐的编码!!
答案 1 :(得分:6)
您可以将ListView与自定义OnTouchListener(用于捕捉项目)一起用于垂直滚动,并使用自定义OnTouchListener(用于捕捉项目)再次使用TwoWayGridView
<强> main.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" />
活动代码将类似于以下内容
@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;
}
}
答案 2 :(得分:2)
我建议使用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
我希望这会对你有所帮助。
答案 3 :(得分:2)
我需要这样的事情,我刚才用过: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);
答案 4 :(得分:2)
您可以在ScrollView
内ScrollView
使用Vertical LinearLayout
作为父级for loop
{{1}}对{em>轮播效果包含coverflow
的布局进行充气