web-dev-qa-db-fra.com

Comment créer une page défilante de carrousels dans Android?

J'essaie de créer une interface utilisateur pour mon Android qui contient une page à défilement vertical de carrousels à défilement horizontal (quelque chose comme ce que fait l'application Netflix). Comment ce type de comportement est-il accompli?

Une implémentation de base serait suffisante pour me lancer. Il y a quelques autres exigences pour l'interface utilisateur, que je vais inclure ici pour référence, car cela peut avoir un impact sur les classes ou les bibliothèques que je peux utiliser.

1) Le défilement vertical entre les carrousels doit être fluide, mais lorsque l'utilisateur relâche, l'interface utilisateur doit "s'accrocher" au carrousel le plus proche (afin que l'utilisateur soit toujours sur une rangée de carrousel, pas entre deux carrousels) .

2) Le défilement horizontal sur un carrousel doit être fluide, mais lorsque l'utilisateur relâche, l'interface utilisateur doit "s'accrocher" à l'élément le plus proche du carrousel.

3) Il devrait être possible de superposer des informations supplémentaires sur un élément du carrousel

4) L'interface utilisateur doit être adaptable à n'importe quelle taille d'écran.

5) Doit être navigable avec les touches fléchées (pour les appareils sans écran tactile)

6) Devrait fonctionner sur une large gamme de Android (éventuellement via la bibliothèque de support)

7) Doit être OK à utiliser dans une application open source sous licence GPL

Les réponses acceptables NE DOIVENT PAS répondre à toutes ces exigences. Au minimum, une bonne réponse devrait impliquer la navigation dans plusieurs carrousels (contre un seul carrousel).

Voici une maquette de ce que j'envisage (je suis flexible, je n'ai pas besoin de ressembler à ça .. le point est juste de clarifier ce dont je parle - chacun la ligne contiendrait de nombreux éléments qui pourraient être défilés vers la gauche et la droite, et la page entière pourrait être défilée de haut en bas)

enter image description here

35
paulscode

Idée principale

Afin d'avoir une conception flexible et d'avoir éléments illimités , vous pouvez créer un RecyclerView comme vue racine avec un LinearLayoutManager.VERTICAL en tant que LayoutManager. pour chaque ligne, vous pouvez mettre un autre RecyclerView mais maintenant avec un LinearLayoutManager.HORIZONTAL en tant que LayoutManager.

Résultat

enter image description here

La source

Code

Exigences

1) Le défilement vertical entre les carrousels doit être fluide, mais lorsque l'utilisateur relâche, l'interface utilisateur doit "s'accrocher" au carrousel le plus proche (afin que l'utilisateur soit toujours sur une rangée de carrousels, pas entre deux carrousels).

2) Le défilement horizontal sur un carrousel doit être fluide, mais lorsque l'utilisateur le relâche, l'interface utilisateur doit "s'accrocher" à l'élément le plus proche du carrousel.

Afin d'atteindre ceux que j'ai utilisés OnScrollListener et quand les états deviennent SCROLL_STATE_IDLE, je vérifie les vues de dessus et de dessous pour voir laquelle a la région la plus visible, puis faites défiler jusqu'à cette position. pour chaque ligne, je le fais pour les vues gauche et droite pour chaque adaptateur de ligne. De cette façon, toujours un côté de vos carrousels ou rangées s'adapte. par exemple, si le haut est monté, le bas ne l'est pas ou vice versa. Je pense que si vous jouez un peu plus, vous pouvez y arriver mais vous devez connaître la dimension de la fenêtre et changer la dimension des carrousels à l'exécution.

3) Il devrait être possible de superposer des informations supplémentaires sur un élément du carrousel

Si vous utilisez RelativeLayout ou FrameLayout comme vue racine de chaque élément, vous pouvez mettre des informations les unes sur les autres. comme vous pouvez le voir, les chiffres sont en haut des images.

4) L'interface utilisateur doit être adaptable à n'importe quelle taille d'écran.

si vous savez comment prendre en charge plusieurs tailles d'écran, vous pouvez le faire facilement, si vous ne savez pas lire le document. Prise en charge de plusieurs écrans

5) Doit être navigable avec les touches fléchées (pour les appareils sans écran tactile)

utiliser la fonction ci-dessous

mRecyclerView.scrollToPosition(position);

6) Devrait fonctionner sur une large gamme de versions Android (éventuellement via la bibliothèque de support))

importer Android. support.v7 . widget.RecyclerView;

7) Doit être autorisé à utiliser dans une application open source sous licence GPL

Ok

codage heureux !!

47
mmlooloo

Vous pouvez utiliser ListView avec un OnTouchListener personnalisé (pour accrocher des éléments) pour le défilement vertical et TwoWayGridView à nouveau avec un OnTouchListener personnalisé (pour accrocher des éléments)

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

Et le code d'activité sera quelque chose comme ce qui suit

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

Et voici les adaptateurs de test

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

}
6
Mostafa Gazar

J'avais besoin de quelque chose comme ça il y a quelque temps, je l'ai juste utilisé: https://github.com/simonrob/Android-Horizontal-ListView

Simple, puissant, personnalisable.

Exemple de ma 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;
            }
        }
    }
};
}

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

Dans 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);
2
thib_rdr

Je suggère la vue Recycler.

Vous pouvez créer une liste horizontale et verticale ou des vues de grille. À mon avis, le viseur peut parfois devenir compliqué.

Je travaille sur l'application de vidéo à la demande et cela m'a sauvé.

Dans votre cas, il sera facile à installer. Je vais vous donner du code.

Vous aurez besoin des éléments suivants:
Vue XML - Où la disposition de recyclage est déclarée.
Adaptateur - Vous aurez besoin d'une vue pour remplir l'adaptateur et remplir la vue de recyclage.

Création de la vue

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

Déclarez ceci à l'endroit où vous souhaitez afficher le carrousel.

Ensuite, vous souhaitez créer l'adaptateur:

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


        }
    }

C'est là que vous transférez les données à l'adaptateur pour remplir chaque élément du carrousel.
Enfin, déclarez-le et appelez l'adaptateur:

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

Vous pouvez créer une vue de liste avec des vues de recyclage pour obtenir ce que vous voulez.
Cette classe est idéale pour un défilement fluide et une optimisation de la mémoire.

Voici le lien pour cela:

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

J'espère que ceci vous aide.

2
Victor Du Preez

Vous pouvez utiliser un ScrollView comme parent dans ce ScrollView placer un Vertical LinearLayout dans for loop gonfle une mise en page composée de coverflow pour effet carrousel

2
Kaushik