使用RecyclerView打造QQ條目側滑效果
阿新 • • 發佈:2018-12-30
說起這個功能,先吐槽一下,剛來不久的一個產品經理,自己雖然使用的是IOS手機,但也不能什麼效果都是說人家IOS的效果互動設計的號,我們就按照它的效果做!
IOS自帶的這個破側滑功能,看著醜的要死,幹嘛非要這個幹,幹,幹……….啊…………啊………….
如果她不是個女的,我就……………………………….
好了!看看效果吧!
大致效果就是模仿QQ條目側滑自己寫了一個:
一、首先我們要自定義一個SwipeLayout繼承自FrameLayout
來包裝item的子view,子view分為兩部分,一部分實在預設狀態下(就是沒側滑)我們看的的內容,另一部分被隱藏在最右側。
- 那好我們來看看SwipeLayout具體內容:
package cn.hnshangyu.swipelayout.view;
import android.content.Context;
import android.graphics.Rect;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import cn.hnshangyu.swipelayout.R;
/**
* =========================================
* 版權所有 違法必究
* 作者: huangxiaoguo.
* =========================================
*/
public class SwipeLayout extends FrameLayout {
/**
* 滑動狀態
*/
public enum SwipeState {
OPEN, CLOSE, SWIPING
}
private SwipeState swipeState = SwipeState.CLOSE;//預設關閉狀態
private OnSwipeChangeListener onSwipeChangeListener;
public OnSwipeChangeListener getOnSwipeChangeListener() {
return onSwipeChangeListener;
}
public void setOnSwipeChangeListener(OnSwipeChangeListener onSwipeChangeListener) {
this.onSwipeChangeListener = onSwipeChangeListener;
}
public interface OnSwipeChangeListener {
void onOpen(SwipeLayout layout);
void onClose(SwipeLayout layout);
void onSwiping(SwipeLayout layout);
// 將要開啟 當前是 關閉狀態 ----> 拖動
void onStartOpen(SwipeLayout layout);
//將要關閉 當前是 開啟i狀態--->拖動
void onStartClose(SwipeLayout layout);
}
private ViewDragHelper mViewDragHelper;
private ViewGroup mBackLayout;
private ViewGroup mFrontLayout;
private int mWidth;
private int mHeight;
private int mRange;
public SwipeLayout(Context context) {
this(context, null);
}
public SwipeLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SwipeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//1 .初始化 ViewDragHelper物件
mViewDragHelper = ViewDragHelper.create(this, 1.0f, callBack);
}
//2. 將touch 事件 轉交給 mViewDragHelper
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 交給 mViewDragHelper 決定是否攔截
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
Log.e("Log","onTouchEvent---ACTION_UP");
if (swipeState == SwipeState.CLOSE) {
Log.e("Log","onTouchEvent---ACTION_UP--swipeState");
return false;
}
break;
}
// 讓 mViewDragHelper 接收到 觸控事件
try {
mViewDragHelper.processTouchEvent(event);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
//測量 會呼叫很多次
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
// 測量完成後 值改變後才會呼叫
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
//拖動範圍
mRange = mBackLayout.getMeasuredWidth();
}
/**
* 放置 子view
*
* @param changed
* @param left
* @param top
* @param right
* @param bottom
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
layoutInit(false);
}
private void layoutInit(boolean isOpen) {
Rect frontRect = computeFrontRect(isOpen);
//f放置 mFrontLayout
mFrontLayout.layout(frontRect.left, frontRect.top, frontRect.right, frontRect.bottom);
Rect backRect = computeBackRect(frontRect);
mBackLayout.layout(backRect.left, backRect.top, backRect.right, backRect.bottom);
//將 控制元件前置
bringChildToFront(mFrontLayout);
}
/**
* 計算 mBackLayout 矩形位置
*
* @param frontRect
* @return
*/
private Rect computeBackRect(Rect frontRect) {
int left = frontRect.right;
return new Rect(left, frontRect.top, left + mRange, frontRect.bottom);
}
/**
* 計算 mFrontLayout矩形位置
*
* @param isOpen
* @return
*/
private Rect computeFrontRect(boolean isOpen) {
int left = 0;
if (isOpen) {
left = -mRange;
} else {
left = 0;
}
return new Rect(left, 0, left + mWidth, 0 + mHeight);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//新增健壯性 判斷
//1 比如有 兩個 或者 兩個以上子view
int childCount = getChildCount();
if (childCount < 2) {
throw new IllegalStateException("You must have 2 children at least!! 你得有 至少兩個子view!!");
}
//2 校驗都是viewGroup
if (getChildAt(0) == null || !(getChildAt(0) instanceof ViewGroup) || getChildAt(1) == null && !(getChildAt(1) instanceof ViewGroup)) {
throw new IllegalArgumentException("your child must be instance of ViewGroup! 你的view 必須是 viewgroup 的子類 ");
}
// 後邊選單
mBackLayout = (ViewGroup) findViewById(R.id.layout_back);
//前置條目
mFrontLayout = (ViewGroup) findViewById(R.id.layout_front);
}
// 3 mViewDragHelper 解析完 touch事件 ----》CallBack
ViewDragHelper.Callback callBack = new ViewDragHelper.Callback() {
/**
*返回值決定是否 可以拖動
* @param child 拖拽的view物件 子view
* @param pointerId 多指 手指的id
* @return
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
// return child == mFrontLayout;
return true;
}
/**
* 當 view 被捕獲的時候呼叫
* @param capturedChild
* @param activePointerId
*/
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
}
/**
* 獲取橫向 拖拽範圍 不決定 能否拖動
* 做伴隨動畫 計算執行時長 ,計算敏感度 >0
* @param child
* @return
*/
@Override
public int getViewHorizontalDragRange(View child) {
//返回實際的拖動範圍
return mRange;
}
/**
* 1. 修正 位置 left 2. 沒有 開始真正的移動
* @param child
* @param left
* @param dx
* @return
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//child 正在拖動的子view
//left 建議達到的位置
//dx deltaX 水平方向的瞬間變化量
// int currentLeft = mFrontLayout.getLeft();
// System.out.println( "currentLeft = "+currentLeft+"dx"+dx+" =? "+left);
if (child == mFrontLayout) {
left = fixedFrontLeft(left);
} else if (child == mBackLayout) {
left = fixedBackLeft(left);
}
return left;
}
private int fixedFrontLeft(int left) {
if (left < -mRange) {
left = -mRange;
} else if (left > 0) {
left = 0;
}
return left;
}
private int fixedBackLeft(int left) {
if (left < (mWidth - mRange)) {
left = mWidth - mRange;
} else if (left > mWidth) {
left = mWidth;
}
return left;
}
/**
* 位置改變的時候呼叫 1. 伴隨動畫 2. 狀態變化 3. 添加回調
* @param changedView
* @param left
* @param top
* @param dx
* @param dy
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
// System.out.println("onViewPositionChanged>>>mBackLayout " + mBackLayout.getLeft());
//changedView 當前正在拖動的子view
//left clampViewPositionHorizontal 的返回值
// top
// dx 橫向的瞬間變化量
if (changedView == mFrontLayout) { //拖動mFrontLayout 讓 mBackLayout跟著出來
mBackLayout.offsetLeftAndRight(dx);
} else if (changedView == mBackLayout) {//mBackLayout 轉交 給mFrontLayout
mFrontLayout.offsetLeftAndRight(dx);
}
dispatchEvent();
//手動 重新整理 重新繪製
invalidate();
}
// @Override
// public int clampViewPositionVertical(View child, int top, int dy) {
// return top;
// }
// 當 拖動的view 釋放的時候呼叫
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//releasedChild 釋放的view
//xvel 釋放時橫向的速度 向左 - + 停止後釋放 0
//yvel 釋放時縱向的速度
// System.out.println(" releasedChild = " + releasedChild + "::xvel = " + xvel);
// 釋放時 位置小於 -mRange 丙炔速度為0
if (mFrontLayout.getLeft() < -mRange * 0.5f && xvel == 0) {
open();
} else if (xvel < 0) { // 向左快速滑動
open();
} else {
close();
}
}
};
/**
* 1. 更新狀態 2.添加回調
*/
private void dispatchEvent() {
SwipeState preState = swipeState;
swipeState = updateState();
if (onSwipeChangeListener != null) {
onSwipeChangeListener.onSwiping(this);
if (swipeState != preState) { //當前狀態和上一個狀態不一樣
if (swipeState == SwipeState.OPEN) {
onSwipeChangeListener.onOpen(this);
} else if (swipeState == SwipeState.CLOSE) {
onSwipeChangeListener.onClose(this);
} else if (preState == SwipeState.OPEN) {
onSwipeChangeListener.onStartClose(this);
} else if (preState == SwipeState.CLOSE) {
onSwipeChangeListener.onStartOpen(this);
}
}
}
}
/**
* 獲取當前 最新狀態
*
* @return
*/
private SwipeState updateState() {
if (mFrontLayout.getLeft() == -mRange) { // 開啟
return SwipeState.OPEN;
} else if (mFrontLayout.getLeft() == 0) {
return SwipeState.CLOSE;
}
return SwipeState.SWIPING;
}
// scroller 執行會呼叫此方法 computeScroll 會呼叫很多次
@Override
public void computeScroll() {
super.computeScroll();
// 是否繼續觸發動畫
if (mViewDragHelper.continueSettling(true)) {
//執行動畫
ViewCompat.postInvalidateOnAnimation(this);
}
}
public void open(boolean isSmooth) {
if (isSmooth) {
int finalLeft = -mRange;// 最終的位置
// 返回值 決定是否觸發動畫
boolean b = mViewDragHelper.smoothSlideViewTo(mFrontLayout, finalLeft, 0);
if (b) {
// 執行動畫
ViewCompat.postInvalidateOnAnimation(this);
}
} else {
layoutInit(true);
}
}
/**
* 開啟
*/
public void open() {
open(true);//預設平滑狀態
}
public void close(boolean isSmooth) {
if (isSmooth) {
int finalLeft = 0;// 最終的位置
// 返回值 決定是否觸發動畫
boolean b = mViewDragHelper.smoothSlideViewTo(mFrontLayout, finalLeft, 0);
if (b) {
// 執行動畫
ViewCompat.postInvalidateOnAnimation(this);
}
} else {
layoutInit(false);
}
}
/**
* 關閉
*/
public void close() {
close(true);// 預設平滑關閉
}
}
程式碼裡面解釋的很詳細,再讓我解釋,都不知道該說啥了!
- 我們再來看看item的佈局
<cn.hnshangyu.swipelayout.view.SwipeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipelayout"
android:layout_width="match_parent"
android:layout_height="60dp">
<LinearLayout
android:id="@+id/layout_back"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:descendantFocusability="blocksDescendants"
android:orientation="horizontal">
<TextView
android:id="@+id/placed_top"
android:layout_width="60dp"
android:layout_height="match_parent"
android:background="#bdbdbd"
android:gravity="center"
android:text="置頂"
android:textColor="@android:color/white"
android:textSize="16dp" />
<TextView
android:id="@+id/no_read"
android:layout_width="100dp"
android:layout_height="match_parent"
android:background="#e6be62"
android:gravity="center"
android:text="標為未讀"
android:textColor="@android:color/white"
android:textSize="16dp" />
<TextView
android:id="@+id/delete"
android:layout_width="60dp"
android:layout_height="match_parent"
android:background="#e92f2f"
android:gravity="center"
android:text="刪除"
android:textColor="@android:color/white"
android:textSize="16dp" />
</LinearLayout>
<RelativeLayout
android:id="@+id/layout_front"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:descendantFocusability="blocksDescendants"
android:padding="8dp">
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:gravity="center_vertical"
android:src="@mipmap/icon_head" />
<TextView
android:id="@+id/textview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="8dp"
android:layout_toRightOf="@+id/icon"
android:gravity="center_vertical"
android:text="姓名"
android:textSize="16dp" />
</RelativeLayout>
</cn.hnshangyu.swipelayout.view.SwipeLayout>
可以看到item分為前後兩部分,
- mainActivity的佈局很簡單隻有一個RecyclerView
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="cn.hnshangyu.swipelayout.MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
好了佈局我們完成了,現在要看看我們的adapter了
package cn.hnshangyu.swipelayout.adapter;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import java.util.concurrent.CopyOnWriteArrayList;
import butterknife.ButterKnife;
import butterknife.InjectView;
import cn.hnshangyu.swipelayout.R;
import cn.hnshangyu.swipelayout.view.SwipeLayout;
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
private Context mContext;
private CopyOnWriteArrayList<String> mNameList;
private SwipeLayout preLayout;//記錄上一個開啟
public SwipeLayout getPreLayout() {
return preLayout;
}
public MyAdapter(Context context, CopyOnWriteArrayList<String> nameList) {
this.mContext = context;
this.mNameList = nameList;
}
private OnItemClickListener onItemClickListener;
public interface OnItemClickListener {
void onOpen(SwipeLayout layout);
void onClose(SwipeLayout layout);
void onSwiping(SwipeLayout layout);
void onStartOpen(SwipeLayout layout);
void onStartClose(SwipeLayout layout);
void onpLacedTop(int position);
void onNoRead(int position);
void onDelete(int position);
void onItemClick(int position);
}
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(mContext).inflate(R.layout.item_swipe, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, final int position) {
holder.textview.setText(mNameList.get(position));
holder.swipelayout.setOnSwipeChangeListener(new SwipeLayout.OnSwipeChangeListener() {
@Override
public void onOpen(SwipeLayout layout) {
preLayout = layout;
if (onItemClickListener != null) {
onItemClickListener.onOpen(layout);
}
}
@Override
public void onClose(SwipeLayout layout) {
if (onItemClickListener != null) {
onItemClickListener.onClose(layout);
}
}
@Override
public void onSwiping(SwipeLayout layout) {
if (onItemClickListener != null) {
onItemClickListener.onSwiping(layout);
}
}
@Override
public void onStartOpen(SwipeLayout layout) {
if (preLayout != null) {
preLayout.close();
}
if (onItemClickListener != null) {
onItemClickListener.onStartOpen(layout);
}
}
@Override
public void onStartClose(SwipeLayout layout) {
if (onItemClickListener != null) {
onItemClickListener.onStartClose(layout);
}
}
});
holder.layoutFront.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
holder.placedTop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (onItemClickListener != null) {
onItemClickListener.onpLacedTop(position);
}
}
});
holder.noRead.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (onItemClickListener != null) {
onItemClickListener.onNoRead(position);
}
}
});
holder.delete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (onItemClickListener != null) {
onItemClickListener.onDelete(position);
}
}
});
holder.layoutFront.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (onItemClickListener != null) {
onItemClickListener.onItemClick(position);
}
}
});
}
@Override
public int getItemCount() {
return mNameList.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
@InjectView(R.id.placed_top)
TextView placedTop;
@InjectView(R.id.no_read)
TextView noRead;
@InjectView(R.id.delete)
TextView delete;
@InjectView(R.id.layout_back)
LinearLayout layoutBack;
@InjectView(R.id.icon)
ImageView icon;
@InjectView(R.id.textview)
TextView textview;
@InjectView(R.id.layout_front)
RelativeLayout layoutFront;
@InjectView(R.id.swipelayout)
SwipeLayout swipelayout;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.inject(this, itemView);
}
}
}
在adapter中自定義監聽,便於與Activity的互動
- Activity的實現
package cn.hnshangyu.swipelayout;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.widget.AbsListView;
import java.util.concurrent.CopyOnWriteArrayList;
import butterknife.ButterKnife;
import butterknife.InjectView;
import cn.hnshangyu.swipelayout.adapter.MyAdapter;
import cn.hnshangyu.swipelayout.utils.ToastUtil;
import cn.hnshangyu.swipelayout.view.RecycleViewDivider;
import cn.hnshangyu.swipelayout.view.SwipeLayout;
public class MainActivity extends AppCompatActivity {
@InjectView(R.id.recyclerView)
RecyclerView mRecyclerView;
private LinearLayoutManager manager;
private Context mContext;
private MyAdapter mAdapter;
private CopyOnWriteArrayList<String> NameList = new CopyOnWriteArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContext = this;
ButterKnife.inject(this);
initData();
initView();
initListener();
}
private void initData() {
for (int i = 0; i < 108; i++) {
NameList.add("huangxiaoguo" + i);
}
}
private void initView() {
manager = new LinearLayoutManager(this);
manager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(manager);
int mColor = ContextCompat.getColor(mContext, R.color.light_gray);
mRecyclerView.addItemDecoration(new RecycleViewDivider(mContext, LinearLayoutManager.HORIZONTAL, 2, mColor));
mAdapter = new MyAdapter(mContext, NameList);
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (newState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
SwipeLayout preLayout = mAdapter.getPreLayout();
if (preLayout != null) {
preLayout.close();
}
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
}
});
}
private void initListener() {
mAdapter.setOnItemClickListener(new MyAdapter.OnItemClickListener() {
@Override
public void onOpen(SwipeLayout layout) {
ToastUtil.showToast(mContext, "開啟");
}
@Override
public void onClose(SwipeLayout layout) {
ToastUtil.showToast(mContext, "關閉");
}
@Override
public void onSwiping(SwipeLayout layout) {
ToastUtil.showToast(mContext, "正在移動");
}
@Override
public void onStartOpen(SwipeLayout layout) {
ToastUtil.showToast(mContext, "開始開啟");
}
@Override
public void onStartClose(SwipeLayout layout) {
ToastUtil.showToast(mContext, "開始關閉");
}
@Override
public void onpLacedTop(int position) {
ToastUtil.showToast(mContext, "置頂"+NameList.get(position));
}
@Override
public void onNoRead(int position) {
ToastUtil.showToast(mContext, "標記未讀"+NameList.get(position));
}
@Override
public void onDelete(int position) {
ToastUtil.showToast(mContext, "刪除"+NameList.get(position));
}
@Override
public void onItemClick(int position) {
ToastUtil.showToast(mContext, NameList.get(position));
}
});
}
}
Activity中有相對應的監聽回撥,這樣就可以進行我們下一步操作了!