刷卡到喜欢的项目

Swipe card to favourite item

当我偶然发现 this video 时,我正在查看来自 Google 的 Material 文档。它显示了一个卡片项目,可以向右滑动以收藏一个项目。

我想模仿这种行为,但多次失败。我能找到的所有库和教程都大约 swipe-to-delete。我试图让两个视图相互堆叠,应该滑动顶部的视图,以便下面的视图可见。我试图用 ItemTouchHelper 来实现这一点,但是这个 class 似乎只能促进 swipe-to-deletemove 重新排序列表操作。

这个滑动动作如何实现?

默认 ItemTouchHelperonSwiped 提供回调,其中可以包含您选择的任何逻辑,而不仅仅是删除。您绝对可以拥有将商品标记为收藏夹的代码。但是,我认为这需要完全滑动该项目,而不是像您的视频所示的部分滑动。

下面的两种方法都使用 Canvas 和图形 类 作为 fine-grained 控件,您应该能够反映这些行为。

This article explains how to display action buttons when an item is swiped. It modifies the SwipeRevealLayout library 并删除了不必要的滑动方向处理。

更详细的step-by-step解释,您也可以查看this article。当它显示 'Edit' 和 'Delete' 按钮时,可以替换这些按钮的 onClick 回调中的代码以将项目标记为收藏。

你需要使用ItemTouchHelper

  • 这是一个实用程序 class,用于向 dismiss 添加滑动和向 RecyclerView 拖放支持。
  • 它与 RecyclerView 和回调 class 配合使用,后者配置启用的交互类型,并在用户执行这些操作时接收事件。

这里是如何使用 ItemTouchHelperRecyclerView

的示例代码

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>

以上示例代码的输出

https://www.youtube.com/watch?v=GKD-SDPWD3k&feature=youtu.be

For Information you can check this below article

And if you want to go with any library than check this

这是一个很好的效果,但是没有标准的开箱即用的方式来做到这一点 AFAIK。 Android 确实提供了一组创建效果的工具,所以让我们看看它是如何完成的。

接近

  1. 定义一个有两层的布局:底层包含一个装有心形的容器。这是将被激活的心脏。顶层将是向右滑动显示底层的层。

  2. 为心脏创建动画。下面我介绍了一种创建 "heart beat" 动画的方法,该方法基于 azizbekian 对 Stack Overflow 问题的回答.

  3. 创建一个扩展 ItemTouchHelper.SimpleCallback 的 class: 在此 class 中,您需要覆盖 onChildDraw() 来处理在上面 1) 的布局中设置的滑动面板的移动。 onChildDraw()也是执行动画的好地方。当滑动视图滑动到您将定义的 "trigger point" 时触发动画。此 class 具有其他需要覆盖的方法。见下文。

item_card.xml

这是 RecyclerVIew 项的两层布局。这里我使用 FrameLayouts,但也可以使用其他视图组。这就是它的样子。你看到的心脏在顶层。跳动的心脏在下面。顶心可以设置 visible/invisible 取决于该项目是否是最喜欢的。这是它的样子:

然后打开(我手动设置的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>

心跳动画
这是 azizbekian 动画的封装版本。目标视图将是具有 id=heart 的视图。要为背景着色,您可以使用以心为中心的 circular reveal 动画。

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()
请参阅 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);
}

其他方法在ItemTouchHelper.SimpleCallback

中被覆盖
  • onSelectedChanged()clearView
  • onMove()onSwiped() - 在这些方法中什么都不做
  • isItemViewSwipeEnabled()
  • isLongPressDragEnabled()

还有很多细节,但这是重要部分的大致轮廓。