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?
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);
}
}
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));
}
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 ViewHolder
s 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.
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);
}
}
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.
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
Afin de l'implémenter, j'ai utilisé l'exemple de code créé par Marcin Kitowicz ici .
Avantages de cette solution:
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)
}
}
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.
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.