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