问题
I was viewing the Material docs from Google when I stumbled upon this video. It shows a card item which can be swiped to the right to favourite an item.
I want to mirror this behaviour but have failed multiple times. All libraries and tutorials I can find are about swipe-to-delete
. I tried to have two views stacked upon each other where the one on top should be swiped so the one below would become visible. I tried to achieve this with the ItemTouchHelper
, but this class seems to only be able to facilitate the swipe-to-delete
and move
to reorder a list actions.
How can this swipe action be achieved?
回答1:
That is a nice effect, but there is no standard out-of-the-box way to do that AFAIK. Android does provide a set of tools to create the effect, so let's look at how it can be done.
Approach
Define a layout that has two layers: The bottom layer contains a container that holds a heart shape. This is the heart that will be animated. The top layer will be the layer that slides to the right to show the underlying layer.
Create the animation for the heart. Below I present a method that create the "heart beat" animation based upon azizbekian's answer to a Stack Overflow question here.
Create a class that extends ItemTouchHelper.SimpleCallback: Within this class you will need to override
onChildDraw()
to take care of the movement of the sliding panel set up in the layout of 1) above.onChildDraw()
is also a good place to execute the animation. The animation is triggered when the sliding view slides to a "trigger point" that you will define. This class has other methods that will need to be overridden. See below.
item_card.xml
This is a two-layer layout for the RecyclerVIew
items. Here I use FrameLayouts
, but other view groups could also be used. This is what it looks like. The heart you see is on the top layer. The beating heart is underneath. The top heart can be set visible/invisible depending upon whether the item is a favorite or not. Here is what it looks like:
And open (I manually set translationX
.)
<FrameLayout android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:id="@+id/heartFrame"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:visibility="gone">
<ImageView
android:id="@+id/heart"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="top|center_horizontal"
android:padding="8dp"
card_view:srcCompat="@drawable/heart" />
</FrameLayout>
<androidx.cardview.widget.CardView
android:id="@+id/slidingPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="visible"
card_view:cardBackgroundColor="@android:color/background_light"
card_view:cardElevation="5dp"
card_view:cardUseCompatPadding="true">
<ImageView
android:id="@+id/imageView"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="35dp"
android:paddingTop="8dp"
card_view:srcCompat="@drawable/ic_android_green_24dp" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:paddingStart="8dp"
android:paddingBottom="8dp"
android:text="This is some text"
android:textSize="20sp" />
<ImageView
android:id="@+id/favorite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|start"
android:padding="8dp"
android:tint="@android:color/holo_red_dark"
card_view:srcCompat="@drawable/heart" />
</androidx.cardview.widget.CardView>
</FrameLayout>
Heart Beat Animation
Here is an encapsulated version of azizbekian's animation. The target view will be the view with the id=heart. To color the background, you can use a circular reveal animation centered on the heart.
private AnimatorSet getHeartBeatAnimation(View target) {
final float from = 1.0f;
final float to = 1.3f;
ObjectAnimator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, from, to);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, from, to);
ObjectAnimator translationZ = ObjectAnimator.ofFloat(target, View.TRANSLATION_Z, from, to);
AnimatorSet set1 = new AnimatorSet();
set1.playTogether(scaleX, scaleY, translationZ);
set1.setDuration(100);
set1.setInterpolator(new AccelerateInterpolator());
ObjectAnimator scaleXBack = ObjectAnimator.ofFloat(target, View.SCALE_X, to, from);
ObjectAnimator scaleYBack = ObjectAnimator.ofFloat(target, View.SCALE_Y, to, from);
ObjectAnimator translationZBack = ObjectAnimator.ofFloat(target, View.TRANSLATION_Z, to, from);
Path path = new Path();
path.moveTo(0.0f, 0.0f);
path.lineTo(0.5f, 1.3f);
path.lineTo(0.75f, 0.8f);
path.lineTo(1.0f, 1.0f);
PathInterpolator pathInterpolator = new PathInterpolator(path);
AnimatorSet set2 = new AnimatorSet();
set2.playTogether(scaleXBack, scaleYBack, translationZBack);
set2.setDuration(300);
set2.setInterpolator(pathInterpolator);
AnimatorSet animSet = new AnimatorSet();
animSet.playSequentially(set1, set2);
return animSet;
}
onChildDraw()
See documentation for onChildDraw()
here.
int maxRightShift = 400; // Example only. This should not be hard-coded and should be set elsewhere.
@Override
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
MyRecyclerViewAdapter.ItemViewHolder vh = (MyRecyclerViewAdapter.ItemViewHolder) viewHolder;
// Don't let the sliding view slide more than maxRightShift amount.
if (dX >= maxRightShift && mFavoriteChangedPosition == RecyclerView.NO_POSITION) {
// Capture the position that has changed. Only on change per sliding event.
mFavoriteChangedPosition = vh.getAdapterPosition();
// Trigger the animation and do, potentially, some housekeeping.
// setFavoriteActivation will have the animation set and triggered.
vh.setFavoriteActivation(!vh.isFavorite());
}
// Shift just the CardView and leave underlying views.
vh.mCardView.setTranslationX(dX);
}
Other methods to be overridden in ItemTouchHelper.SimpleCallback
onSelectedChanged()
andclearView
onMove()
andonSwiped()
- do nothing in these methodsisItemViewSwipeEnabled()
isLongPressDragEnabled()
There are a lot more details, but that is the general outline of the significant parts.
回答2:
You need to use ItemTouchHelper
- This is a utility class to add swipe to
dismiss
and drag & drop support toRecyclerView
. - It works with a
RecyclerView
and a Callback class, which configures what type of interactions are enabled and also receives events when user performs these actions.
Here is the sample code how to use ItemTouchHelper
with RecyclerView
StackActivity
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import java.util.ArrayList;
public class StackActivity extends AppCompatActivity {
RecyclerView myRecyclerView;
private ArrayList<ItemModel> arrayList = new ArrayList<>();
FavAdapter favAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_stack);
myRecyclerView = findViewById(R.id.myRecyclerView);
myRecyclerView.setLayoutManager(new LinearLayoutManager(this));
myRecyclerView.setHasFixedSize(true);
// here i'm adding dummy data inside list
addDataInList();
// setting adapter to RecyclerView
favAdapter = new FavAdapter(this, arrayList);
myRecyclerView.setAdapter(favAdapter);
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
return false;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
// when user swipe thr recyclerview item to right remove item from avorite list
if (direction == ItemTouchHelper.RIGHT) {
favAdapter.addToFav(viewHolder.getAdapterPosition(), false);
}
// when user swipe thr recyclerview item to left remove item from avorite list
else if (direction == ItemTouchHelper.LEFT) {
favAdapter.addToFav(viewHolder.getAdapterPosition(), true);
}
}
}).attachToRecyclerView(myRecyclerView);
}
//method to add dummy data inside ourlist
private void addDataInList() {
arrayList.add(new ItemModel("Item 1", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 2", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 3", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 4", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 5", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 6", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 7", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 8", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 9", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 10", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 11", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 12", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 13", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 14", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 15", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 16", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 17", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 18", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 19", "https://i.stack.imgur.com/1dWdI.jpg", false));
arrayList.add(new ItemModel("Item 20", "https://i.stack.imgur.com/1dWdI.jpg", false));
}
}
activity_stack layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/myRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
FavAdapter class
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import java.util.ArrayList;
public class FavAdapter extends RecyclerView.Adapter<FavAdapter.ViewHolder> {
private Context context;
private ArrayList<ItemModel> arrayList = new ArrayList<>();
public FavAdapter(Context context, ArrayList<ItemModel> arrayList) {
this.context = context;
this.arrayList = arrayList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.custom_fav_layout, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
// here i'm check that if item is already added in favorite or not
//based on boolean flag i'm managed to set weather the item is in favorite or not
// this flag is also use full to keep state of out favorite when we scroll our recyclerview
holder.ivFavImage.setImageResource(arrayList.get(position).isFavorite()
? R.drawable.ic_favorite : R.drawable.ic_fav_white);
holder.tvProductName.setText(arrayList.get(position).getItemName());
Glide.with(context)
.load(arrayList.get(position).getImageUrl())
.apply(new RequestOptions().
placeholder(R.drawable.ic_placeholder)
.error(R.drawable.ic_error))
.into(holder.ivProductImage);
}
// this method is used to add or remove item from favorite list when use swipe the recyclerview item using ItemTouchHelper
public void addToFav(int position, boolean flag) {
arrayList.get(position).setFavorite(flag);
notifyDataSetChanged();
}
@Override
public int getItemCount() {
return arrayList.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
ImageView ivProductImage, ivFavImage;
TextView tvProductName;
public ViewHolder(View itemView) {
super(itemView);
ivProductImage = itemView.findViewById(R.id.ivProductImage);
ivFavImage = itemView.findViewById(R.id.ivFavImage);
tvProductName = itemView.findViewById(R.id.tvProductName);
ivFavImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (arrayList.get(getAdapterPosition()).isFavorite()) {
arrayList.get(getAdapterPosition()).setFavorite(false);
} else {
arrayList.get(getAdapterPosition()).setFavorite(true);
}
notifyDataSetChanged();
}
});
}
}
}
custom_fav_layout layout
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="5dp"
app:cardUseCompatPadding="true">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/ivProductImage"
android:layout_width="match_parent"
android:layout_height="150dp"
android:scaleType="fitXY"
android:adjustViewBounds="true"
android:src="@color/colorNavBar" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/tvProductName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/app_name"
android:paddingStart="5dp"
android:textColor="#FFFFFF"
android:textStyle="bold" />
<ImageView
android:id="@+id/ivFavImage"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/ic_favorite" />
</LinearLayout>
</RelativeLayout>
</android.support.v7.widget.CardView>
ItemModel damodel class
public class ItemModel {
boolean isFavorite;
String ItemName,imageUrl;
public ItemModel( String itemName, String imageUrl,boolean isFavorite) {
this.isFavorite = isFavorite;
ItemName = itemName;
this.imageUrl = imageUrl;
}
public boolean isFavorite() {
return isFavorite;
}
public void setFavorite(boolean favorite) {
isFavorite = favorite;
}
public String getItemName() {
return ItemName;
}
public void setItemName(String itemName) {
ItemName = itemName;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
}
R.drawable.ic_favorite
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF00"
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z"/>
</vector>
drawable.ic_fav_white
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z"/>
</vector>
OUTPUT of above sample code
https://www.youtube.com/watch?v=GKD-SDPWD3k&feature=youtu.be
For Information you can check this below article
- Android adding RecyclerView Swipe to Delete and Undo
- Android swipe menu with RecyclerView
- RecyclerView swipe to delete easier than you thought
- Android RecyclerView Swipe To Delete Example Button And Undo
- Android RecyclerView: Swipeable Items
And if you want to go with any library than check this
- SwipeToDelete
- Swipeable-RecyclerView
- itemtouchhelper-extension
回答3:
The default ItemTouchHelper
provides a callback for onSwiped
, which can contain any logic of your choice, not just deletion. You can definitely have code that marks the item as a favourite. However, I believe that requires a complete swipe of the item, rather than a partial swipe as shown by your video.
Both the approaches below use the Canvas and graphics classes for fine-grained control, and you should be able to mirror the behaviour.
This article explains how to display action buttons when an item is swiped. It modifies the SwipeRevealLayout library and removes unnecessary swipe direction handling.
For a more detailed, step-by-step explanation, you can also check out this article. While it displays 'Edit' and 'Delete' buttons, the code in the onClick callbacks for those buttons can be replaced to mark the item as a favourite instead.
来源:https://stackoverflow.com/questions/52703823/swipe-card-to-favourite-item