web-dev-qa-db-fra.com

Ajout d'un arrière-plan coloré avec du texte / une icône sous la ligne glissée lors de l'utilisation d'Android RecyclerView

EDIT: Le vrai problème était que mon LinearLayout était enveloppé dans une autre disposition, ce qui provoquait un comportement incorrect. La réponse acceptée par Sanvywell a un meilleur exemple, plus complet, de comment dessiner une couleur sous une vue balayée que l'extrait de code que j'ai fourni dans la question.

Maintenant que le widget RecyclerView prend en charge nativement le balayage des lignes à l'aide de la classe ItemTouchHelper , j'essaie de l'utiliser dans une application où les lignes se comporteront de manière similaire à l'application Inbox de Google . Autrement dit, un balayage vers la gauche effectue une action et un balayage vers la droite en fait une autre.

L'implémentation des actions elles-mêmes était facile en utilisant la méthode ItemTouchHelper.SimpleCallback de onSwiped. Cependant, je n'ai pas réussi à trouver un moyen simple de définir la couleur et l'icône qui devraient apparaître sous la vue en cours de balayage (comme dans l'application Inbox de Google).

Pour ce faire, j'essaie de remplacer ItemTouchHelper.SimpleCallback la méthode onChildDraw comme ceci:

@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView,
                        RecyclerView.ViewHolder viewHolder, float dX, float dY,
                        int actionState, boolean isCurrentlyActive) {
    RecyclerViewAdapter.ViewHolder vh = (RecyclerViewAdapter.ViewHolder) viewHolder;
    LinearLayout ll = vh.linearLayout;

    Paint p = new Paint();
    if(dX > 0) {
        p.setARGB(255, 255, 0, 0);
    } else {
        p.setARGB(255, 0, 255, 0);
    }

    c.drawRect(ll.getLeft(), ll.getTop(), ll.getRight(), ll.getBottom(), p);

    super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}

La détermination de la direction de balayage à partir de dX et la définition de la couleur appropriée fonctionnent comme prévu, mais les coordonnées que j'obtiens à partir du ViewHolder correspondent toujours à l'endroit où le premier LinearLayout a été gonflé.

Comment obtenir les coordonnées correctes pour le LinearLayout qui se trouve dans la ligne actuellement balayée? Existe-t-il un moyen plus simple (qui ne nécessite pas de remplacer onChildDraw) de définir la couleur et l'icône d'arrière-plan?

27
Manvis

J'avais du mal à implémenter cette fonctionnalité également, mais vous m'avez dirigé dans la bonne direction.

@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
    if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
        // Get RecyclerView item from the ViewHolder
        View itemView = viewHolder.itemView;

        Paint p = new Paint();
        if (dX > 0) {
            /* Set your color for positive displacement */

            // Draw Rect with varying right side, equal to displacement dX
            c.drawRect((float) itemView.getLeft(), (float) itemView.getTop(), dX,
                    (float) itemView.getBottom(), p);
        } else {
            /* Set your color for negative displacement */

            // Draw Rect with varying left side, equal to the item's right side plus negative displacement dX
            c.drawRect((float) itemView.getRight() + dX, (float) itemView.getTop(),
                    (float) itemView.getRight(), (float) itemView.getBottom(), p);
        }

        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
    }
}
49
Sanvywell

La réponse acceptée fait un excellent travail de coloration de l'arrière-plan, mais n'a pas abordé le dessin de l'icône.

Cela a fonctionné pour moi car il a à la fois défini la couleur d'arrière-plan et dessiné l'icône, sans que l'icône ne soit étirée pendant le balayage, ou laissant un écart entre les éléments précédents et suivants après le balayage.

public static final float ALPHA_FULL = 1.0f;

public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
    if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
        // Get RecyclerView item from the ViewHolder
        View itemView = viewHolder.itemView;

        Paint p = new Paint();
        Bitmap icon;

        if (dX > 0) {
            /* Note, ApplicationManager is a helper class I created 
               myself to get a context outside an Activity class - 
               feel free to use your own method */

            icon = BitmapFactory.decodeResource(
                    ApplicationManager.getContext().getResources(), R.drawable.myleftdrawable);

            /* Set your color for positive displacement */
            p.setARGB(255, 255, 0, 0);

            // Draw Rect with varying right side, equal to displacement dX
            c.drawRect((float) itemView.getLeft(), (float) itemView.getTop(), dX,
                    (float) itemView.getBottom(), p);

            // Set the image icon for Right swipe
            c.drawBitmap(icon,
                    (float) itemView.getLeft() + convertDpToPx(16),
                    (float) itemView.getTop() + ((float) itemView.getBottom() - (float) itemView.getTop() - icon.getHeight())/2,
                    p);
        } else {
            icon = BitmapFactory.decodeResource(
                    ApplicationManager.getContext().getResources(), R.drawable.myrightdrawable);

            /* Set your color for negative displacement */
            p.setARGB(255, 0, 255, 0);

            // Draw Rect with varying left side, equal to the item's right side
            // plus negative displacement dX
            c.drawRect((float) itemView.getRight() + dX, (float) itemView.getTop(),
                    (float) itemView.getRight(), (float) itemView.getBottom(), p);

            //Set the image icon for Left swipe
            c.drawBitmap(icon,
                    (float) itemView.getRight() - convertDpToPx(16) - icon.getWidth(),
                    (float) itemView.getTop() + ((float) itemView.getBottom() - (float) itemView.getTop() - icon.getHeight())/2,
                    p);
        }

        // Fade out the view as it is swiped out of the parent's bounds
        final float alpha = ALPHA_FULL - Math.abs(dX) / (float) viewHolder.itemView.getWidth();
        viewHolder.itemView.setAlpha(alpha);
        viewHolder.itemView.setTranslationX(dX);

    } else {
        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
    }
}

private int convertDpToPx(int dp){
    return Math.round(dp * (getResources().getDisplayMetrics().xdpi / DisplayMetrics.DENSITY_DEFAULT));
}
26
HappyKatz

Je ne sais pas comment ces solutions (par @Sanvywell, @HappyKatz et @ user2410066) fonctionnent pour vous, mais dans mon cas, j'avais besoin d'une autre vérification de la méthode onChildDraw.

Il semble que ItemTouchHelper conserve ViewHolders des lignes supprimées au cas où elles devraient être restaurées. Il appelle également onChildDraw pour ces VH en plus du VH en cours de balayage. Je ne suis pas sûr des implications de gestion de la mémoire de ce comportement mais j'avais besoin d'une vérification supplémentaire au début de onChildDraw pour éviter de dessiner des lignes "fantom".

if (viewHolder.getAdapterPosition() == -1) {
    return;
}

PARTIE BONUS:

J'ai également voulu continuer à dessiner pendant que d'autres lignes s'animent vers leurs nouvelles positions après la suppression d'une ligne, et je ne pouvais pas le faire dans ItemTouchHelper et onChildDraw. À la fin, j'ai dû ajouter un autre décorateur d'objets pour le faire. Cela va dans ce sens:

public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    if (parent.getItemAnimator().isRunning()) {
        // find first child with translationY > 0
        // draw from it's top to translationY whatever you want

        int top = 0;
        int bottom = 0;

        int childCount = parent.getLayoutManager().getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getLayoutManager().getChildAt(i);
            if (child.getTranslationY() != 0) {
                top = child.getTop();
                bottom = top + (int) child.getTranslationY();                    
                break;
            }
        }

        // draw whatever you want

        super.onDraw(c, parent, state);
    }
}

MISE À JOUR: J'ai écrit un article de blog sur le balayage de la vue du recycleur pour supprimer la fonctionnalité. Quelqu'un pourrait trouver cela utile. Aucune bibliothèque tierce n'est nécessaire.

article de bloggit repo

7
Nemanja Kovacevic

La solution HappyKatz a un bug délicat. Y a-t-il une raison pour dessiner un bitmap lorsque dX == 0 ?? Dans certains cas, cela entraîne une visibilité permanente des icônes au-dessus de l'élément de liste. Les icônes deviennent également visibles au-dessus de l'élément de liste lorsque vous touchez simplement l'élément de liste et dX == 1. Pour résoudre ces problèmes:

        if (dX > rectOffset) {
            c.drawRect((float) itemView.getLeft(), (float) itemView.getTop(), dX,
                    (float) itemView.getBottom(), leftPaint);
            if (dX > iconOffset) {
                c.drawBitmap(leftBitmap,
                        (float) itemView.getLeft() + padding,
                        (float) itemView.getTop() + ((float) itemView.getBottom() - (float) itemView.getTop() - leftBitmap.getHeight()) / 2,
                        leftPaint);
            }
        } else if (dX < -rectOffset) {
            c.drawRect((float) itemView.getRight() + dX, (float) itemView.getTop(),
                    (float) itemView.getRight(), (float) itemView.getBottom(), rightPaint);
            if (dX < -iconOffset) {
                c.drawBitmap(rightBitmap,
                        (float) itemView.getRight() - padding - rightBitmap.getWidth(),
                        (float) itemView.getTop() + ((float) itemView.getBottom() - (float) itemView.getTop() - rightBitmap.getHeight()) / 2,
                        rightPaint);
            }
        }
5
user2410066

Pour les personnes qui trouvent toujours cette valeur par défaut, c'est le moyen le plus simple.

Une classe utilitaire simple pour ajouter un arrière-plan, une icône et une étiquette à un élément RecyclerView tout en le faisant glisser vers la gauche ou la droite.

enter image description hereenter image description here

insérer dans Gradle

implementation 'it.xabaras.Android:recyclerview-swipedecorator:1.1'

Remplacer la méthode onChildDraw de la classe ItemTouchHelper

@Override
public void onChildDraw (Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,float dX, float dY,int actionState, boolean isCurrentlyActive){
    new RecyclerViewSwipeDecorator.Builder(MainActivity.this, c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
            .addBackgroundColor(ContextCompat.getColor(MainActivity.this, R.color.my_background))
            .addActionIcon(R.drawable.my_icon)
            .create()
            .decorate();

    super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}

pour plus d'informations -> https://github.com/xabaras/RecyclerViewSwipeDecorator

3
kelvin andre

Afin de l'implémenter, j'ai utilisé l'exemple de code créé par Marcin Kitowicz ici .

Avantages de cette solution:

  1. Utilise une vue d'arrière-plan avec des limites de disposition au lieu de créer un rectangle qui s'affichera au-dessus de tout bitmap ou dessinable.
  2. Utilise une image Drawable opposée à Bitmap qui est plus facile à implémenter que de convertir un Drawable en Bitmap.

Le code d'implémentation d'origine peut être trouvé ici . Afin d'implémenter le balayage vers la gauche, j'ai utilisé la logique de positionnement inverse gauche et droite.

override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
    var icon = ContextCompat.getDrawable(context!!, R.drawable.ic_save_24dp)
    var iconLeft = 0
    var iconRight = 0

    val background: ColorDrawable
    val itemView = viewHolder.itemView
    val margin = convertDpToPx(32)
    val iconWidth = icon!!.intrinsicWidth
    val iconHeight = icon.intrinsicHeight
    val cellHeight = itemView.bottom - itemView.top
    val iconTop = itemView.top + (cellHeight - iconHeight) / 2
    val iconBottom = iconTop + iconHeight

    // Right swipe.
    if (dX > 0) {
        icon = ContextCompat.getDrawable(context!!, R.drawable.ic_save_24dp)
        background = ColorDrawable(Color.RED)
        background.setBounds(0, itemView.getTop(), (itemView.getLeft() + dX).toInt(), itemView.getBottom())
        iconLeft = margin
        iconRight = margin + iconWidth
    } /*Left swipe.*/ else {
        icon = ContextCompat.getDrawable(context!!, R.drawable.ic_save_24dp)
        background = ColorDrawable(Color.BLUE)
        background.setBounds((itemView.right - dX).toInt(), itemView.getTop(), 0, itemView.getBottom())
        iconLeft = itemView.right - margin - iconWidth
        iconRight = itemView.right - margin
    }
    background.draw(c)
    icon?.setBounds(iconLeft, iconTop, iconRight, iconBottom)
    icon?.draw(c)
    super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
}
}
3
Adam Hurwitz

Voici comment je le fais sans bibliothèque tierce.

La vue de premier plan sera toujours visible dans la vue du recycleur, et lorsque le balayage est effectué, l'arrière-plan sera visible en restant dans une position statique.

enter image description here

Créez votre élément RecyclerView personnalisé et ajoutez votre icône, texte et couleur d'arrière-plan personnalisés à la disposition d'arrière-plan de l'élément. Notez que j'ai mis un identifiant à RelativeLayout avec id=foreground Et id=background.

Voici le mien recylerview_item.xml.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
    Android:layout_width="match_parent"
    Android:layout_height="wrap_content"
    xmlns:app="http://schemas.Android.com/apk/res-auto"
    Android:orientation="vertical">

    <RelativeLayout
        Android:id="@+id/background"
        Android:layout_width="match_parent"
        Android:layout_height="match_parent"
        Android:background="@color/colorPrimary"> <!--Add your background color here-->

        <ImageView
            Android:id="@+id/delete_icon"
            Android:layout_width="30dp"
            Android:layout_height="30dp"
            Android:layout_alignParentRight="true"
            Android:layout_centerVertical="true"
            Android:layout_marginRight="10dp"
            app:srcCompat="@drawable/ic_delete"/>

        <TextView
            Android:layout_width="wrap_content"
            Android:layout_height="wrap_content"
            Android:layout_centerVertical="true"
            Android:layout_marginRight="10dp"
            Android:layout_toLeftOf="@id/delete_icon"
            Android:text="Swipe to delete"
            Android:textColor="#fff"
            Android:textSize="13dp" />
    </RelativeLayout>

    <RelativeLayout
        Android:padding="20dp"
        Android:id="@+id/foreground"
        Android:layout_width="match_parent"
        Android:layout_height="wrap_content"
        Android:background="@color/colorWhite">

            <TextView
                Android:id="@+id/textView"
                Android:text="HelloWorld"
                Android:layout_width="wrap_content"
                Android:layout_height="wrap_content" />

    </RelativeLayout>
</FrameLayout>

et à partir de votre ViewHolder définissez vos RelativeLayout foreground et background view et rendez-les publics. Créez également une méthode qui supprimera l'élément. Dans mon cas, mon ViewHolder est sous mon RecyclerViewAdapter.class, Alors ...

public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder> {

    List<Object> listItem;

    public RecyclerViewAdapter(...) {
        ...
    } 

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = mInflater.inflate(R.layout.recyclerview_item, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(final ViewHolder holder, int position) {
        ....
    }

    @Override
    public int getItemCount() {
        ...
    }

    public class ViewHolder extends RecyclerView.ViewHolder{

        public RelativeLayout foreground, background;

        public ViewHolder(View itemView) {
            super(itemView);

            /** define your foreground and background **/

            foreground = itemView.findViewById(R.id.foreground);
            background = itemView.findViewById(R.id.background);

        }

    }

    /**Call this later to remove the item on swipe**/
    public void removeItem(int position){
        //remove the item here
        listItem.remove(position);
        notifyItemRemoved(position);
    }
}

Et créez une classe et nommez-la RecyclerItemTouchHelper.class, C'est là que la chose se produira.

public class RecyclerItemTouchHelper extends ItemTouchHelper.SimpleCallback {

    private RecyclerItemTouchHelperListener listener;

    public RecyclerItemTouchHelper(int dragDirs, int swipeDirs, RecyclerItemTouchHelperListener listener) {
        super(dragDirs, swipeDirs);
        this.listener = listener;
    }

    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
        return true;
    }

    @Override
    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
        if (viewHolder != null) {
            final View foregroundView = ((RecyclerViewAdapter.ViewHolder) viewHolder).foreground;
            getDefaultUIUtil().onSelected(foregroundView);
        }
    }

    @Override
    public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
                                RecyclerView.ViewHolder viewHolder, float dX, float dY,
                                int actionState, boolean isCurrentlyActive) {
        final View foregroundView = ((RecyclerViewAdapter.ViewHolder) viewHolder).foreground;
        getDefaultUIUtil().onDrawOver(c, recyclerView, foregroundView, dX, dY,
                actionState, isCurrentlyActive);
    }

    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        final View foregroundView = ((RecyclerViewAdapter.ViewHolder) viewHolder).foreground;
        getDefaultUIUtil().clearView(foregroundView);
    }

    @Override
    public void onChildDraw(Canvas c, RecyclerView recyclerView,
                            RecyclerView.ViewHolder viewHolder, float dX, float dY,
                            int actionState, boolean isCurrentlyActive) {
        final View foregroundView = ((RecyclerViewAdapter.ViewHolder) viewHolder).foreground;

        getDefaultUIUtil().onDraw(c, recyclerView, foregroundView, dX, dY,
                actionState, isCurrentlyActive);
    }

    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
        listener.onSwiped(viewHolder, direction, viewHolder.getAdapterPosition());
    }

    @Override
    public int convertToAbsoluteDirection(int flags, int layoutDirection) {
        return super.convertToAbsoluteDirection(flags, layoutDirection);
    }

    public interface RecyclerItemTouchHelperListener {
        void onSwiped(RecyclerView.ViewHolder viewHolder, int direction, int position);
    }
}

Maintenant, à partir de votre MainActivity.class Ou où que soit votre RecyclerView, attachez le RecyclerItemTouchHelper. Dans mon cas, le RecyclerView est dans MainActivity.class Donc j'ai implémenté RecyclerItemTouchHelper.RecyclerItemTouchHelperListener Dedans et je remplace la méthode onSwiped()...

public class MainActivity extends AppCompatActivity implements RecyclerItemTouchHelper.RecyclerItemTouchHelperListener {

    RecyclerView recyclerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        //Configure RecyclerView

        recyclerView = (RecyclerView) findViewById(R.id.recyclerView);  
        RecyclerView.LayoutManager mLyoutManager = new LinearLayoutManager(getApplicationContext());
        recyclerView.setLayoutManager(mLyoutManager);
        recyclerView.setItemAnimator(new DefaultItemAnimator());
        adapter = new RecyclerViewAdapter(this);
        adapter.setClickListener(this);
        recyclerView.setAdapter(adapter);
        recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL));

        //Attached the ItemTouchHelper
        ItemTouchHelper.SimpleCallback itemTouchHelperCallback = new RecyclerItemTouchHelper(0, ItemTouchHelper.LEFT, this);
        new ItemTouchHelper(itemTouchHelperCallback).attachToRecyclerView(recyclerView);
    }

    //define the method onSwiped()
    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction, int position) {
        if (viewHolder instanceof RecyclerViewAdapter.ViewHolder) {
            adapter.removeItem(viewHolder.getAdapterPosition()); //remove the item from the adapter
        }
    }

}

Pour plus d'informations et de clarifications ici est le blog pour cela.

0
Polar