模仿探探的左右滑动切换卡片功能
偶然之間發現探探的左右滑動的圖片挺好玩,試著去做了下,再到后來,看到許多大神也推出了同樣仿探探效果的博客,從頭到尾閱讀下來,寫得通俗易懂,基本上沒什么問題。于是,實現仿探探效果的想法再次出現在腦海中。那么,還猶豫什么,趁熱來一發吧!就這么愉快地決定了。
RecyclerView 是最佳選擇!
RecyclerView 是最佳選擇!
RecyclerView 是最佳選擇!
重要的話講三遍!!!
究其原因,第一,RecyclerView 是自帶 Item View 回收和重用功能的,就不需要我們考慮這個問題了;第二,RecyclerView 的布局方式是通過設置 LayoutManager 來實現的,這樣就充分地把布局和 RecyclerView “解耦”開來了。而 LayoutManager 是可以通過自定義的方式來實現的。這恰恰是我們想要的!!!再說一點,這也正是不選用 ListView 的原因之一。
下面,我們就開始動手了。帶你見證奇跡的時刻。
1. 創建 CardLayoutManager 并繼承自 RecyclerView.LayoutManager 。需要我們自己實現 generateDefaultLayoutParams() 方法:@Override public RecyclerView.LayoutParams generateDefaultLayoutParams() {return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT); } 2. 一般情況下,像上面這樣寫即可。
下面這個方法就是我們的重點了。 onLayoutChildren(final RecyclerView.Recycler recycler, RecyclerView.State state) 方法就是用來實現 Item View 布局的:
@Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {super.onLayoutChildren(recycler, state); removeAllViews(); detachAndScrapAttachedViews(recycler); int itemCount = getItemCount(); if(itemCount>CardConfig.DEFAULT_SHOW_ITEM){for(int position = CardConfig.DEFAULT_SHOW_ITEM;position>=0;position--){final View view = recycler.getViewForPosition(position); addView(view); measureChildWithMargins(view,0,0); int widthSpace = getWidth() -getDecoratedMeasuredWidth(view); int heightSpace = getHeight() - getBottomDecorationHeight(view); layoutDecoratedWithMargins(view,widthSpace/2,heightSpace/2,widthSpace/2+getDecoratedMeasuredWidth(view),heightSpace/2+getBottomDecorationHeight(view)); if(position == CardConfig.DEFAULT_SHOW_ITEM){view.setScaleX(1-(position-1)*CardConfig.DEFAULT_SCALE); view.setScaleY(1-(position-1)*CardConfig.DEFAULT_SCALE); view.setTranslationY((position-1)*view.getMeasuredHeight()/CardConfig.DEFAULT_TRANSLATE_Y); }else if(position>0){view.setScaleX(1 - position * CardConfig.DEFAULT_SCALE); view.setScaleY(1 - position * CardConfig.DEFAULT_SCALE); view.setTranslationY(position * view.getMeasuredHeight() / CardConfig.DEFAULT_TRANSLATE_Y); }else{view.setOnTouchListener(mOnTouchListener); }}}else{// 當數據源個數小于或等于最大顯示數時 for (int position = itemCount - 1; position >= 0; position--) {final View view = recycler.getViewForPosition(position); addView(view); measureChildWithMargins(view, 0, 0); int widthSpace = getWidth() - getDecoratedMeasuredWidth(view); int heightSpace = getHeight() - getDecoratedMeasuredHeight(view); // recyclerview 布局 layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2, widthSpace / 2 + getDecoratedMeasuredWidth(view), heightSpace / 2 + getDecoratedMeasuredHeight(view)); if (position > 0) {view.setScaleX(1 - position * CardConfig.DEFAULT_SCALE); view.setScaleY(1 - position * CardConfig.DEFAULT_SCALE); view.setTranslationY(position * view.getMeasuredHeight() / CardConfig.DEFAULT_TRANSLATE_Y); } else {view.setOnTouchListener(mOnTouchListener); }}}}
3.創建onTouchListener
private View.OnTouchListener mOnTouchListener = new View.OnTouchListener() {@Override public boolean onTouch(View v, MotionEvent event) {RecyclerView.ViewHolder childViewHolder = mRecyclerView.getChildViewHolder(v); if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) {mItemTouchHelper.startSwipe(childViewHolder); }return false; } };
4.完整CardLayoutManager代碼如下
public class CardLayoutManager extends RecyclerView.LayoutManager {private RecyclerView mRecyclerView; private ItemTouchHelper mItemTouchHelper; public CardLayoutManager(@NonNull RecyclerView recyclerView, @NonNull ItemTouchHelper itemTouchHelper) {this.mRecyclerView = recyclerView; this.mItemTouchHelper = itemTouchHelper; }@Override public RecyclerView.LayoutParams generateDefaultLayoutParams() {return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT); }@Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {super.onLayoutChildren(recycler, state); removeAllViews(); detachAndScrapAttachedViews(recycler); int itemCount = getItemCount(); if(itemCount>CardConfig.DEFAULT_SHOW_ITEM){for(int position = CardConfig.DEFAULT_SHOW_ITEM;position>=0;position--){final View view = recycler.getViewForPosition(position); addView(view); measureChildWithMargins(view,0,0); int widthSpace = getWidth() -getDecoratedMeasuredWidth(view); int heightSpace = getHeight() - getBottomDecorationHeight(view); layoutDecoratedWithMargins(view,widthSpace/2,heightSpace/2,widthSpace/2+getDecoratedMeasuredWidth(view),heightSpace/2+getBottomDecorationHeight(view)); if(position == CardConfig.DEFAULT_SHOW_ITEM){view.setScaleX(1-(position-1)*CardConfig.DEFAULT_SCALE); view.setScaleY(1-(position-1)*CardConfig.DEFAULT_SCALE); view.setTranslationY((position-1)*view.getMeasuredHeight()/CardConfig.DEFAULT_TRANSLATE_Y); }else if(position>0){view.setScaleX(1 - position * CardConfig.DEFAULT_SCALE); view.setScaleY(1 - position * CardConfig.DEFAULT_SCALE); view.setTranslationY(position * view.getMeasuredHeight() / CardConfig.DEFAULT_TRANSLATE_Y); }else{view.setOnTouchListener(mOnTouchListener); }}}else{// 當數據源個數小于或等于最大顯示數時 for (int position = itemCount - 1; position >= 0; position--) {final View view = recycler.getViewForPosition(position); addView(view); measureChildWithMargins(view, 0, 0); int widthSpace = getWidth() - getDecoratedMeasuredWidth(view); int heightSpace = getHeight() - getDecoratedMeasuredHeight(view); // recyclerview 布局 layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2, widthSpace / 2 + getDecoratedMeasuredWidth(view), heightSpace / 2 + getDecoratedMeasuredHeight(view)); if (position > 0) {view.setScaleX(1 - position * CardConfig.DEFAULT_SCALE); view.setScaleY(1 - position * CardConfig.DEFAULT_SCALE); view.setTranslationY(position * view.getMeasuredHeight() / CardConfig.DEFAULT_TRANSLATE_Y); } else {view.setOnTouchListener(mOnTouchListener); }}}}private View.OnTouchListener mOnTouchListener = new View.OnTouchListener() {@Override public boolean onTouch(View v, MotionEvent event) {RecyclerView.ViewHolder childViewHolder = mRecyclerView.getChildViewHolder(v); if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) {mItemTouchHelper.startSwipe(childViewHolder); }return false; }}; }
可以看出,大致的效果已經有了。缺少的就是處理觸摸滑動事件了。
5.CardConfig
public class CardConfig {/** * 顯示可見的卡片數量 */ public static final int DEFAULT_SHOW_ITEM = 3; /** * 默認縮放的比例 */ public static final float DEFAULT_SCALE = 0.1f; /** * 卡片Y軸偏移量時按照14等分計算 */ public static final int DEFAULT_TRANSLATE_Y = 14; /** * 卡片滑動時默認傾斜的角度 */ public static final float DEFAULT_ROTATE_DEGREE = 15f; /** * 卡片滑動時不偏左也不偏右 */ public static final int SWIPING_NONE = 1; /** * 卡片向左滑動時 */ public static final int SWIPING_LEFT = 1 << 2; /** * 卡片向右滑動時 */ public static final int SWIPING_RIGHT = 1 << 3; /** * 卡片從左邊滑出 */ public static final int SWIPED_LEFT = 1; /** * 卡片從右邊滑出 */ public static final int SWIPED_RIGHT = 1 << 2; }
6.OnSwipeListener
在看滑動事件的代碼之前,我們先定義一個監聽器。主要用于監聽卡片滑動事件,代碼就如下所示,注釋也給出來了。應該都看得懂吧:
public interface OnSwipeListener<T> {/** * 卡片還在滑動時回調 * * @param viewHolder 該滑動卡片的viewHolder * @param ratio 滑動進度的比例 * @param direction 卡片滑動的方向,CardConfig.SWIPING_LEFT 為向左滑,CardConfig.SWIPING_RIGHT 為向右滑, * CardConfig.SWIPING_NONE 為不偏左也不偏右 */ void onSwiping(RecyclerView.ViewHolder viewHolder, float ratio, int direction); /** * 卡片完全滑出時回調 * * @param viewHolder 該滑出卡片的viewHolder * @param t 該滑出卡片的數據 * @param direction 卡片滑出的方向,CardConfig.SWIPED_LEFT 為左邊滑出;CardConfig.SWIPED_RIGHT 為右邊滑出 */ void onSwiped(RecyclerView.ViewHolder viewHolder, T t, int direction); /** * 所有的卡片全部滑出時回調 */ void onSwipedClear(); } 7. CardItemTouchHelperCallback
現在,我們可以回過頭來看看卡片滑動了。對于 ItemTouchHelper 來處理 Item View 的觸摸滑動事件相必都不陌生吧!
我們暫且命名為 CardItemTouchHelperCallback 。對于 ItemTouchHelper.Callback 而言,需要在 getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) 方法中配置 swipeFlags 和 dragFlags 。
public class CardItemTouchHelperCallback<T> extends ItemTouchHelper.Callback {private final RecyclerView.Adapter adapter; private List<T> dataList; private OnSwipeListener<T> mListener; public CardItemTouchHelperCallback(@NonNull RecyclerView.Adapter adapter, @NonNull List<T> dataList) {this.adapter = adapter; this.dataList = dataList; }public CardItemTouchHelperCallback(@NonNull RecyclerView.Adapter adapter, @NonNull List<T> dataList, OnSwipeListener<T> listener) {this.adapter = adapter; this.dataList = dataList; this.mListener = listener; }@Override public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {int dragFlags = 0; int swipeFlags = 0; RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if (layoutManager instanceof CardLayoutManager) {swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; }return makeMovementFlags(dragFlags, swipeFlags); }@Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {return false; }@Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {// 移除之前設置的 onTouchListener, 否則觸摸滑動會亂了 viewHolder.itemView.setOnTouchListener(null); // 刪除相對應的數據 int layoutPosition = viewHolder.getLayoutPosition(); T remove = dataList.remove(layoutPosition); adapter.notifyDataSetChanged(); // 卡片滑出后回調 OnSwipeListener 監聽器 if (mListener != null) {mListener.onSwiped(viewHolder, remove, direction == ItemTouchHelper.LEFT ? CardConfig.SWIPED_LEFT : CardConfig.SWIPED_RIGHT); }// 當沒有數據時回調 OnSwipeListener 監聽器 if (adapter.getItemCount() == 0) {if (mListener != null) {mListener.onSwipedClear(); }}}public void setOnSwipedListener(OnSwipeListener<T> mListener) {this.mListener = mListener; }@Override public boolean isItemViewSwipeEnabled() {return false; }@Override public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); View itemView = viewHolder.itemView; if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {// 得到滑動的閥值 float ratio = dX / getThreshold(recyclerView, viewHolder); // ratio 最大為 1 或 -1 if (ratio > 1) {ratio = 1; } else if (ratio < -1) {ratio = -1; }// 默認最大的旋轉角度為 15 度 itemView.setRotation(ratio * CardConfig.DEFAULT_ROTATE_DEGREE); int childCount = recyclerView.getChildCount(); // 當數據源個數大于最大顯示數時 if (childCount > CardConfig.DEFAULT_SHOW_ITEM) {for (int position = 1; position < childCount - 1; position++) {int index = childCount - position - 1; View view = recyclerView.getChildAt(position); // 和之前 onLayoutChildren 是一個意思,不過是做相反的動畫 view.setScaleX(1 - index * CardConfig.DEFAULT_SCALE + Math.abs(ratio) * CardConfig.DEFAULT_SCALE); view.setScaleY(1 - index * CardConfig.DEFAULT_SCALE + Math.abs(ratio) * CardConfig.DEFAULT_SCALE); view.setTranslationY((index - Math.abs(ratio)) * itemView.getMeasuredHeight() / CardConfig.DEFAULT_TRANSLATE_Y); }} else {// 當數據源個數小于或等于最大顯示數時 for (int position = 0; position < childCount - 1; position++) {int index = childCount - position - 1; View view = recyclerView.getChildAt(position); view.setScaleX(1 - index * CardConfig.DEFAULT_SCALE + Math.abs(ratio) * CardConfig.DEFAULT_SCALE); view.setScaleY(1 - index * CardConfig.DEFAULT_SCALE + Math.abs(ratio) * CardConfig.DEFAULT_SCALE); view.setTranslationY((index - Math.abs(ratio)) * itemView.getMeasuredHeight() / CardConfig.DEFAULT_TRANSLATE_Y); }}// 回調監聽器 if (mListener != null) {if (ratio != 0) {mListener.onSwiping(viewHolder, ratio, ratio < 0 ? CardConfig.SWIPING_LEFT : CardConfig.SWIPING_RIGHT); } else {mListener.onSwiping(viewHolder, ratio, CardConfig.SWIPING_NONE); }}}}private float getThreshold(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {return recyclerView.getWidth() * getSwipeThreshold(viewHolder); }@Override public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {super.clearView(recyclerView, viewHolder); viewHolder.itemView.setRotation(0f); } }
8.在Activity中引用
public class MainActivity extends AppCompatActivity {private List<Integer> list = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); initData(); }private void initView() {final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView); recyclerView.setItemAnimator(new DefaultItemAnimator()); recyclerView.setAdapter(new MyAdapter()); CardItemTouchHelperCallback cardCallback = new CardItemTouchHelperCallback(recyclerView.getAdapter(), list); cardCallback.setOnSwipedListener(new OnSwipeListener<Integer>() {@Override public void onSwiping(RecyclerView.ViewHolder viewHolder, float ratio, int direction) {MyAdapter.MyViewHolder myHolder = (MyAdapter.MyViewHolder) viewHolder; viewHolder.itemView.setAlpha(1 - Math.abs(ratio) * 0.2f); if (direction == CardConfig.SWIPING_LEFT) {myHolder.dislikeImageView.setAlpha(Math.abs(ratio)); } else if (direction == CardConfig.SWIPING_RIGHT) {myHolder.likeImageView.setAlpha(Math.abs(ratio)); } else {myHolder.dislikeImageView.setAlpha(0f); myHolder.likeImageView.setAlpha(0f); }}@Override public void onSwiped(RecyclerView.ViewHolder viewHolder, Integer o, int direction) {MyAdapter.MyViewHolder myHolder = (MyAdapter.MyViewHolder) viewHolder; viewHolder.itemView.setAlpha(1f); myHolder.dislikeImageView.setAlpha(0f); myHolder.likeImageView.setAlpha(0f); Toast.makeText(MainActivity.this, direction == CardConfig.SWIPED_LEFT ? "swiped left" : "swiped right", Toast.LENGTH_SHORT).show(); }@Override public void onSwipedClear() {Toast.makeText(MainActivity.this, "data clear", Toast.LENGTH_SHORT).show(); recyclerView.postDelayed(new Runnable() {@Override public void run() {initData(); recyclerView.getAdapter().notifyDataSetChanged(); }}, 3000L); }}); final ItemTouchHelper touchHelper = new ItemTouchHelper(cardCallback); final CardLayoutManager cardLayoutManager = new CardLayoutManager(recyclerView, touchHelper); recyclerView.setLayoutManager(cardLayoutManager); touchHelper.attachToRecyclerView(recyclerView); }private void initData() {list.add(R.drawable.img_avatar_01); list.add(R.drawable.img_avatar_02); list.add(R.drawable.img_avatar_03); list.add(R.drawable.img_avatar_04); list.add(R.drawable.img_avatar_05); list.add(R.drawable.img_avatar_06); list.add(R.drawable.img_avatar_07); }private class MyAdapter extends RecyclerView.Adapter {@Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false); return new MyViewHolder(view); }@Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {ImageView avatarImageView = ((MyViewHolder) holder).avatarImageView; avatarImageView.setImageResource(list.get(position)); }@Override public int getItemCount() {return list.size(); }class MyViewHolder extends RecyclerView.ViewHolder {ImageView avatarImageView; ImageView likeImageView; ImageView dislikeImageView; MyViewHolder(View itemView) {super(itemView); avatarImageView = (ImageView) itemView.findViewById(R.id.iv_avatar); likeImageView = (ImageView) itemView.findViewById(R.id.iv_like); dislikeImageView = (ImageView) itemView.findViewById(R.id.iv_dislike); }}}
9.MainActiviy布局文件
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 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/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>10.item布局
<?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" android:layout_width="336dp" android:layout_height="426dp" android:background="@drawable/img_card_background" android:gravity="center" android:orientation="vertical"> <RelativeLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> <com.example.admin.tantanuse.RoundImageView android:id="@+id/iv_avatar" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" app:radius="7.5dp" android:src="@drawable/img_avatar_01" /> <ImageView android:id="@+id/iv_dislike" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_marginRight="15dp" android:layout_marginTop="15dp" android:alpha="0" android:src="@drawable/img_dislike" /> <ImageView android:id="@+id/iv_like" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="15dp" android:layout_marginTop="15dp" android:alpha="0" android:src="@drawable/img_like" /> </RelativeLayout> <RelativeLayout android:layout_width="match_parent" android:layout_height="100dp" android:paddingLeft="14dp" android:paddingTop="15dp"> <TextView android:id="@+id/tv_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:text="小姐姐" android:textColor="@android:color/black" android:textSize="16sp" /> <TextView android:id="@+id/tv_age" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/tv_name" android:layout_marginTop="5dp" android:background="@drawable/shape_age" android:gravity="center" android:text="♀ 23" android:textColor="#FFFFFF" android:textSize="14sp" /> <TextView android:id="@+id/tv_constellation" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/tv_name" android:layout_marginLeft="4dp" android:layout_marginTop="5dp" android:layout_toRightOf="@id/tv_age" android:background="@drawable/shape_constellation" android:gravity="center" android:text="獅子座" android:textColor="#FFFFFF" android:textSize="14sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/tv_age" android:layout_marginTop="5dp" android:gravity="center" android:text="IT/互聯網" android:textColor="#cbcbcb" /> </RelativeLayout> </LinearLayout>11.帶圓角的ImagView
public class RoundImageView extends ImageView {private Path mPath; private RectF mRectF; private float[] rids = new float[8]; private PaintFlagsDrawFilter paintFlagsDrawFilter; public RoundImageView(Context context) {this(context, null); }public RoundImageView(Context context, AttributeSet attrs) {this(context, attrs, 0); }public RoundImageView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr); TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RoundImageView); float mRadius = array.getDimension(R.styleable.RoundImageView_radius, 10); rids[0] = mRadius; rids[1] = mRadius; rids[2] = mRadius; rids[3] = mRadius; rids[4] = 0f; rids[5] = 0f; rids[6] = 0f; rids[7] = 0f; array.recycle(); mPath = new Path(); paintFlagsDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); setLayerType(View.LAYER_TYPE_HARDWARE, null); }@Override protected void onDraw(Canvas canvas) {mPath.reset(); mPath.addRoundRect(mRectF, rids, Path.Direction.CW); canvas.setDrawFilter(paintFlagsDrawFilter); canvas.save(); canvas.clipPath(mPath); super.onDraw(canvas); canvas.restore(); }@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh); mRectF = new RectF(0, 0, w, h); } }12.attrs.XML
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="RoundImageView"> <attr name="radius" format="reference|dimension" /> </declare-styleable> </resources>與50位技術專家面對面20年技術見證,附贈技術全景圖
總結
以上是生活随笔為你收集整理的模仿探探的左右滑动切换卡片功能的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android-实现View滑动的6种方
- 下一篇: Android自定义View实现滴滴验证