Swipe card to favourite item

こ雲淡風輕ζ 提交于 2021-02-07 10:57:42

问题


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

  1. 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.

  2. 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.

  3. 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() and clearView
  • onMove() and onSwiped() - do nothing in these methods
  • isItemViewSwipeEnabled()
  • 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 to RecyclerView.
  • 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!