How to create scrollable page of carousels in Android?

后端 未结 5 948
再見小時候
再見小時候 2020-12-12 15:36

I am attempting to build a UI for my Android app which contains a vertically scrollable page of horizontally scrollable carousels (something like what the Netflix app does).

相关标签:
5条回答
  • 2020-12-12 16:11

    I would suggest the Recycler view.

    You can create horizontal and vertical list or gridviews. In my opinion the viewpager can become complicated at times.

    I'm working on video on demand application and this saved me.

    In your case it will be easy to set up. I will give you some code.

    You will need the following:
    XML View - Where the recycle layout is declared.
    Adapter - You will need a view to populate the adapter and fill the recycleview.

    Creating the view

    <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"/>
    

    Declare this where you want the carousel to display.

    Next you want to create the adapter:

    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);
    
    
            }
        }
    

    This is where you feed the data to the adapter to populate each carousel item.
    Finally declare it and call the adapter:

    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);
    

    You can create a listview with recycle views to achieve what you want.
    This class is great for smooth scrolling and memory optimisation.

    This is the link for it:

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

    I hope this helps you.

    0 讨论(0)
  • 2020-12-12 16:12

    You can use ListView with a custom OnTouchListener (for snapping items) for the vertical scrolling and TwoWayGridView again with a custom OnTouchListener (for snapping items)

    enter image description here

    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" />
    

    And the Activity code will be something like the following

    @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;
        }
    };
    

    And here are the test adapters

    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;
        }
    
    }
    
    0 讨论(0)
  • 2020-12-12 16:16

    You can use a ScrollView as parent inside that ScrollView place a Vertical LinearLayout in for loop inflate a layout which consist coverflow for carousel effect

    • github link of android-coverflow
    0 讨论(0)
  • 2020-12-12 16:25

    I needed somthing like that a while back, I just used that : https://github.com/simonrob/Android-Horizontal-ListView

    Simple, powerful, customizable.

    Example of my version :

    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;
                }
            }
        }
    };
    }
    

    Here is the 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"
                    />
    

    In the 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);
    
    0 讨论(0)
  • 2020-12-12 16:29

    Main Idea

    In order to have a flexible design and having unlimited items you can create a RecyclerView as a root view with a LinearLayoutManager.VERTICAL as a LayoutManager. for each row you can put another RecyclerView but now with a LinearLayoutManager.HORIZONTAL as a LayoutManager.

    Result

    enter image description here

    Source

    Code

    Requirements

    1) Vertical scrolling between carousels should be smooth, but when user releases, the UI should "snap to" the closest carousel (so the user is always on a carousel row, not between two carousels).

    2) Horizontal scrolling on a carousel should be smooth, but when user releases, the UI should "snap to" the closest item in the carousel.

    In order to achieve those I used OnScrollListener and when the states goes SCROLL_STATE_IDLE I check top and bottom views to see which of them has more visible region then scroll to that position. for each rows I do so for left and right views for each row adapter. In this way always one side of your carousels or rows fit. for example if top is fitted the bottom is not or vise versa. I think if you play a little more you can achieve that but you must know the dimension of window and change the dimension of carousels at runtime.

    3) Should be possible to overlay additional information over an item in the carousel

    If you use RelativeLayout or FrameLayout as a root view of each item you can put information on top of each other. as you can see the numbers are on the top of images.

    4) UI should be adaptable to any screen size.

    if you know how to support multiple screen size you can do so easily, if you do not know read the document. Supporting Multiple Screens

    5) Should be navigable with the arrow keys (for touchscreen-less devices)

    use below function

    mRecyclerView.scrollToPosition(position);
    

    6) Should work on a wide range of Android versions (possibly through the support library)

    import android.support.v7.widget.RecyclerView;

    7) Should be OK to use in an open-source app licensed under the GPL

    Ok

    happy coding!!

    0 讨论(0)
提交回复
热议问题