1. 程式人生 > >Android中實現類似探探中圖片左右滑動切換效果

Android中實現類似探探中圖片左右滑動切換效果

      偶然之間發現探探的左右滑動的圖片挺好玩,試著去做了下,再到後來,看到許多大神也推出了同樣仿探探效果的部落格,從頭到尾閱讀下來,寫得通俗易懂,基本上沒什麼問題。於是,實現仿探探效果的想法再次出現在腦海中。那麼,還猶豫什麼,趁熱來一發吧!就這麼愉快地決定了。

        RecyclerView 是最佳選擇!
        RecyclerView 是最佳選擇!
        RecyclerView 是最佳選擇!
        重要的話講三遍!!!


        究其原因,第一,RecyclerView 是自帶 Item View 回收和重用功能的,就不需要我們考慮這個問題了;第二,RecyclerView 的佈局方式是通過設定 LayoutManager 來實現的,這樣就充分地把佈局和 RecyclerView “解耦”開來了。而 LayoutManager 是可以通過自定義的方式來實現的。這恰恰是我們想要的!!!再說一點,這也正是不選用 ListView 的原因之一。

        下面,我們就開始動手了。帶你見證奇蹟的時刻。


1.建立 CardLayoutManager 並繼承自 RecyclerView.LayoutManager 。需要我們自己實現 generateDefaultLayoutParams() 方法:

@Overridepublic 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 佈局的:

@Overridepublic 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 。

publicclass CardItemTouchHelperCallback<T> extends ItemTouchHelper.Callback {    
   privatefinal 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;    }    

   @Overridepublic 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);    }    
   
   @Overridepublic boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {        
       returnfalse;    }    

   @Overridepublic 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;    }    

   @Overridepublic boolean isItemViewSwipeEnabled() {        
   returnfalse;    }    

   @Overridepublic 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 或 -1if (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);    }    

   @Overridepublic 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;
      
   pu