自定義控制元件:側拉刪除
SwipeLayout 側拉刪除
- 掌握ViewDragHelper 的用法
- 掌握平滑動畫的原理及狀態更新事件回撥
應用場景:QQ 聊天記錄,郵件管理,需要對條目進行功能擴充套件的場景,效果圖:
ViewDragHelper 初始化
建立自定義控制元件SwipeLayout 繼承FrameLayout
public class SwipeLayout extends FrameLayout {
private ViewDragHelper mHelper;
public SwipeLayout(Context context) {
this (context,null);
}
public SwipeLayout(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public SwipeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
//1.建立ViewDragHelper
mHelper = ViewDragHelper.create(this , mCallback);
}
//2.轉交觸控事件,攔截判斷,處理觸控事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mHelper.shouldInterceptTouchEvent(ev);
};
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
//多點觸控有一些小bug,最好catch 一下
mHelper.processTouchEvent(event);
} catch (Exception e) {
}
//消費事件,返回true
return true;
};
//3.處理回撥事件
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return false;
}
};
}
第3-8 行通過構造方法互調,將三個構造方法串連起來,這樣初始化程式碼只需要寫在第三個構造方法中即可
第11-37 行ViewDragHelper 使用三步曲
介面初始化
將SwipeLayout 佈局到activity_main.xml 中
<RelativeLayout 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"
tools:context=".MainActivity" >
<com.example.swipe.SwipeLayout
android:layout_width="match_parent"
android:layout_height="60dp" >
<LinearLayout
android:layout_width="wrap_content"
android:layout_gravity="right"
android:layout_height="match_parent" >
<TextView
android:layout_width="60dp"
android:layout_height="match_parent"
android:background="#666666"
android:gravity="center"
android:text="Call"
android:textColor="#FFFFFF" />
<TextView
android:layout_width="60dp"
android:layout_height="match_parent"
android:background="#FF0000"
android:gravity="center"
android:text="Delete"
android:textColor="#FFFFFF" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#33000000"
android:gravity="center_vertical" >
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginLeft="10dp"
android:src="@drawable/head_1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text="宋江" />
</LinearLayout>
</com.example.swipe.SwipeLayout>
</RelativeLayout>
重寫SwipeLayout 中mCallback 方法,實現簡單的拖拽
//3.處理回撥事件
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
//返回值決定了child 是否可以被拖拽
@Override
public boolean tryCaptureView(View child, int pointerId) {
//child 被使用者拖拽的孩子
return true;
}
//返回值決定將要移動到的位置,此時還沒有發生真正的移動
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//left 建議移動到的位置
return left;
}
};
拖拽事件的傳遞
限定拖拽範圍
第一個子view 命名為後佈局,第二個子view 命名為前佈局
private View mBackView;
private View mFrontView;
//此方法中查詢控制元件
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mBackView = getChildAt(0);
mFrontView = getChildAt(1);
};
獲取控制元件寬高及拖拽範圍
private int mRange;
private int mWidth;
private int mHeight;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//mBackView 的寬度就是mFrontView 的拖拽範圍
mRange = mBackView.getMeasuredWidth();
//控制元件的寬
mWidth = getMeasuredWidth();
//控制元件的高
mHeight = getMeasuredHeight();
}
重寫mCallback 回撥方法
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
//返回值決定了child 是否可以被拖拽
@Override
public boolean tryCaptureView(View child, int pointerId) {
return true;
}
//返回拖拽的範圍,返回一個大於0 的值,計算動畫執行的時長,水平方向是否可以被滑開
@Override
public int getViewHorizontalDragRange(View child) {
return mRange;
}
//返回值決定將要移動到的位置,此時還沒有發生真正的移動
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
// left 建議移動到位置
if (child == mFrontView) {
//限定前佈局的拖拽範圍
if (left < -mRange) {
//前佈局最小的左邊位置不能小於-mRange
left = -mRange;
} else if (left > 0) {
//前佈局最大的左邊位置不能大於0
left = 0;
}
} else if (child == mBackView) {
//限定後佈局的拖拽範圍
if (left < mWidth - mRange) {
//後佈局最小左邊位置不能小於mWidth - mRange
left = mWidth - mRange;
} else if (left > mWidth) {
//後佈局最大的左邊位置不能大於mWidth
left = mWidth;
}
}
return left;
}
};
第7-11 行需要返回一個大於0 的拖拽範圍
第14-37 行通過mRange 分別計算前後佈局的拖拽範圍
傳遞拖拽事件
初始化前後佈局的位置,重寫SwipeLayout 的onLayout()方法
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
//預設是關閉狀態
layoutContent(false);
};
private void layoutContent(boolean isOpen) {
//設定前佈局位置
Rect rect = computeFrontRect(isOpen);
mFrontView.layout(rect.left, rect.top, rect.right, rect.bottom);
//根據前佈局位置計算後佈局位置
Rect backRect = computeBackRectViaFront(rect);
mBackView.layout(backRect.left, backRect.top, backRect.right, backRect.bottom);
}
private Rect computeBackRectViaFront(Rect rect) {
int left = rect.right;
return new Rect(left, 0, left + mRange, mHeight);
}
/**
* 計算佈局所在矩形區域
* @param isOpen
* @return
*/
private Rect computeFrontRect(boolean isOpen) {
int left = 0;
if(isOpen){
left = -mRange;
}
return new Rect(left, 0, left + mWidth, mHeight);
}
第2-7 行重新擺放子view 的位置
第8-15 行由於後佈局是連線在前佈局後面一起滑動的,所以可以通過前佈局的位置計算後佈局的位置
前後佈局在拖拽過程中互相傳遞變化量
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
// 返回值決定了child 是否可以被拖拽
@Override
public boolean tryCaptureView(View child, int pointerId) {
return true;
}
// 返回拖拽的範圍,返回一個大於0 的值,計算動畫執行的時長,水平方向是否可以被滑開
@Override
public int getViewHorizontalDragRange(View child) {
return mRange;
}
// 返回值決定將要移動到的位置,此時還沒有發生真正的移動
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
// left 建議移動到位置
if (child == mFrontView) {
// 限定前佈局的拖拽範圍
if (left < -mRange) {
// 前佈局最小的左邊位置不能小於-mRange
left = -mRange;
} else if (left > 0) {
// 前佈局最大的左邊位置不能大於0
left = 0;
}
} else if (child == mBackView) {
// 限定後佈局的拖拽範圍
if (left < mWidth - mRange) {
// 後佈局最小左邊位置不能小於mWidth - mRange
left = mWidth - mRange;
} else if (left > mWidth) {
// 後佈局最大的左邊位置不能大於mWidth
left = mWidth;
}
}
return left;
}
//位置發生改變時,前後佈局的變化量互相傳遞
@Override
public void onViewPositionChanged(View changedView, int left, int top,
int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
//left 最新的水平位置
//dx 剛剛發生的水平變化量
//位置變化時,把水平變化量傳遞給另一個佈局
if(changedView == mFrontView){
//拖拽的是前佈局,把剛剛發生的變化量dx 傳遞給後佈局
mBackView.offsetLeftAndRight(dx);
}else if(changedView == mBackView){
//拖拽的是後佈局,把剛剛發生的變化量dx 傳遞給前佈局
mFrontView.offsetLeftAndRight(dx);
}
//相容低版本,重繪一次介面
invalidate();
}
};
第38-54 行拖拽前佈局時,將前佈局的變化量傳遞給後佈局,拖拽後佈局時,把後佈局的變化量傳遞給前佈局,這樣前後佈局就可以連動起來
結束動畫
跳轉動畫
// 3.處理回撥事件
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
// 返回值決定了child 是否可以被拖拽
@Override
public boolean tryCaptureView(View child, int pointerId) {
return true;
}
// 返回拖拽的範圍,返回一個大於0 的值,計算動畫執行的時長,水平方向是否可以被滑開
@Override
public int getViewHorizontalDragRange(View child) {
return mRange;
}
// 返回值決定將要移動到的位置,此時還沒有發生真正的移動
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
// left 建議移動到位置
if (child == mFrontView) {
// 限定前佈局的拖拽範圍
if (left < -mRange) {
// 前佈局最小的左邊位置不能小於-mRange
left = -mRange;
} else if (left > 0) {
// 前佈局最大的左邊位置不能大於0
left = 0;
}
} else if (child == mBackView) {
// 限定後佈局的拖拽範圍
if (left < mWidth - mRange) {
// 後佈局最小左邊位置不能小於mWidth - mRange
left = mWidth - mRange;
} else if (left > mWidth) {
// 後佈局最大的左邊位置不能大於mWidth
left = mWidth;
}
}
return left;
}
@Override
public void onViewPositionChanged(View changedView, int left, int top,
int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
//left 最新的水平位置
//dx 剛剛發生的水平變化量
//位置變化時,把水平變化量傳遞給另一個佈局
if(changedView == mFrontView){
//拖拽的是前佈局,把剛剛發生的變化量dx 傳遞給後佈局
mBackView.offsetLeftAndRight(dx);
}else if(changedView == mBackView){
//拖拽的是後佈局,把剛剛發生的變化量dx 傳遞給前佈局
mFrontView.offsetLeftAndRight(dx);
}
//相容低版本,重繪一次介面
invalidate();
}
//鬆手時會被呼叫
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
//xvel 水平方向上的速度,向左為-,向右為+
if(xvel == 0 && mFrontView.getLeft() < -mRange * 0.5f){
//xvel 變0 時,並且前佈局的左邊位置小於-mRange 的一半
open();
}else if (xvel < 0){
//xvel 為-時,開啟
open();
}else{
//其它情況為關閉
close();
}
}
};
public void close() {
//呼叫之前佈局子view 的方法直接跳轉到關閉位置
layoutContent(false);
}
public void open() {
//呼叫之前佈局子view 的方法直接跳轉到開啟位置
layoutContent(true);
}
第55-70 行重寫Callback 的onViewReleased()方法,該方法在鬆手後被呼叫,結束動畫需要在此處做
平滑動畫
public void close() {
close(true);
}
public void open() {
open(true);
}
public void close(boolean isSmooth) {
int finalLeft = 0;
if(isSmooth){
if(mHelper.smoothSlideViewTo(mFrontView, finalLeft, 0)){
ViewCompat.postInvalidateOnAnimation(this);
};
}else{
layoutContent(false);
}
}
public void open(boolean isSmooth) {
int finalLeft = -mRange;
if (isSmooth) {
//mHelper.smoothSlideViewTo(child, finalLeft, finalTop)開啟一個平滑動畫將child
//移動到finalLeft,finalTop 的位置上。此方法返回true 說明當前位置不是最終位置需要重繪
if(mHelper.smoothSlideViewTo(mFrontView, finalLeft, 0)){
//呼叫重繪方法
//invalidate();可能會丟幀,此處推薦使用ViewCompat.postInvalidateOnAnimation()
//引數一定要傳child 所在的容器,因為只有容器才知道child 應該擺放在什麼位置
ViewCompat.postInvalidateOnAnimation(this);
};
} else {
layoutContent(true);
}
}
//重繪時computeScroll()方法會被呼叫
@Override
public void computeScroll() {
super.computeScroll();
//mHelper.continueSettling(deferCallbacks)維持動畫的繼續,返回true 表示還需要重繪
if(mHelper.continueSettling(true)){
ViewCompat.postInvalidateOnAnimation(this);
}
}
第1-31 行過載open(),close()方法,保留跳轉動畫,新增平滑動畫
第32-39 行重寫computeScroll()方法維持動畫的繼續,此處必須重寫,否則沒有動畫效果
監聽回撥
定義回撥介面
在SwipeLayout 中定義公開的介面
//控制元件有三種狀態
public enum Status{
Open,Close,Swiping
}
//初始狀態為關閉
private Status status = Status.Close;
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public interface OnSwipeListener{
//通知外界已經開啟
public void onOpen();
//通知外界已經關閉
public void onClose();
//通知外界將要開啟
public void onStartOpen();
//通知外界將要關閉
public void onStartClose();
}
private OnSwipeListener onSwipeListener;
public OnSwipeListener getOnSwipeListener() {
return onSwipeListener;
}
public void setOnSwipeListener(OnSwipeListener onSwipeListener) {
this.onSwipeListener = onSwipeListener;
}
第20-23 行SwipeLayout 做為ListView 的item 時將要開啟或關閉時需要通知其它item 做相應的處理,所以增加這兩個方法
更新狀態及回撥監聽
修改Callback 的onViewPositionChanged()方法
@Override
public void onViewPositionChanged(View changedView, int left, int top,
int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
//left 最新的水平位置
//dx 剛剛發生的水平變化量
//位置變化時,把水平變化量傳遞給另一個佈局
if(changedView == mFrontView){
//拖拽的是前佈局,把剛剛發生的變化量dx 傳遞給後佈局
mBackView.offsetLeftAndRight(dx);
}else if(changedView == mBackView){
//拖拽的是後佈局,把剛剛發生的變化量dx 傳遞給前佈局
mFrontView.offsetLeftAndRight(dx);
}
//更新狀態及呼叫監聽
dispatchDragEvent();
//相容低版本,重繪一次介面
invalidate();
}
第15-16 行呼叫更新狀態及回撥監聽的方法
dispatchDragEvent()方法
/**
* 更新狀態回撥監聽
*/
protected void dispatchDragEvent() {
//需要記錄一下上次的狀態,對比當前狀態和上次狀態,在狀態改變時呼叫監聽
Status lastStatus = status;
//獲取更新狀態
status = updateStatus();
//在狀態改變時呼叫監聽
if(lastStatus != status && onSwipeListener != null){
if(status == Status.Open){
onSwipeListener.onOpen();
}else if(status == Status.Close){
onSwipeListener.onClose();
}else if(status == Status.Swiping){
if(lastStatus == Status.Close){
//如果上一次狀態為關閉,現在是拖拽狀態,說明正在開啟
onSwipeListener.onStartOpen();
}else if(lastStatus == Status.Open){
//如果上一次狀態為開啟,現在是拖拽狀態,說明正在關閉
onSwipeListener.onStartClose();
}
}
}
}
private Status updateStatus() {
//通過前佈局左邊的位置可以判斷當前的狀態
int left = mFrontView.getLeft();
if(left == 0){
return Status.Close;
}else if(left == -mRange){
return Status.Open;
}
return Status.Swiping;
}
修改activity_main.xml,給SwipeLayout 加上id
<RelativeLayout 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"
tools:context=".MainActivity" >
<com.example.swipe.SwipeLayout
android:id="@+id/sl"
android:layout_width="match_parent"
android:layout_height="60dp" >
<LinearLayout
android:layout_width="wrap_content"
android:layout_gravity="right"
android:layout_height="match_parent" >
<TextView
android:layout_width="60dp"
android:layout_height="match_parent"
android:background="#666666"
android:gravity="center"
android:text="Call"
android:textColor="#FFFFFF" />
<TextView
android:layout_width="60dp"
android:layout_height="match_parent"
android:background="#FF0000"
android:gravity="center"
android:text="Delete"
android:textColor="#FFFFFF" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#33000000"
android:gravity="center_vertical" >
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginLeft="10dp"
android:src="@drawable/head_1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text="宋江" />
</LinearLayout>
</com.example.swipe.SwipeLayout>
</RelativeLayout>
MainActivity 中設定監聽回撥
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SwipeLayout swipeLayout = (SwipeLayout) findViewById(R.id.sl);
swipeLayout.setOnSwipeListener(new OnSwipeListener() {
@Override
public void onStartOpen() {
Utils.showToast(getApplicationContext(), "要去打開了");
}
@Override
public void onStartClose() {
Utils.showToast(getApplicationContext(), "要去關閉了");
}
@Override
public void onOpen() {
Utils.showToast(getApplicationContext(), "已經打開了");
}
@Override
public void onClose() {
Utils.showToast(getApplicationContext(), "已經關閉了");
}
});
}
}
Utils 提供單例Toast 方法
public class Utils {
private static Toast toast;
public static void showToast(Context context, String msg) {
if (toast == null) {
toast = Toast.makeText(context, "", Toast.LENGTH_SHORT);
}
toast.setText(msg);
toast.show();
}
}
整合到ListView
修改activity_main.xml
<RelativeLayout 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"
tools:context=".MainActivity" >
<ListView
android:id="@+id/lv"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</ListView>
</RelativeLayout>
ListView 需要的資料
public class Cheeses {
public static final String[] NAMES = new String[]{"宋江", "盧俊義", "吳用",
"公孫勝", "關勝", "林沖", "秦明", "呼延灼", "花榮", "柴進", "李應", "朱仝", "魯智 深",
"武松", "董平", "張清", "楊志", "徐寧", "索超", "戴宗", "劉唐", "李逵", "史進", " 穆弘",
"雷橫", "李俊", "阮小二", "張橫", "阮小五", " 張順", "阮小七", "楊雄", "石秀", " 解珍",
" 解寶", "燕青", "朱武", "黃信", "孫立", "宣贊", "郝思文", "韓滔", "彭玘", "單廷珪 ",
"魏定國", "蕭讓", "裴宣", "歐鵬", "鄧飛", " 燕順", "楊林", "凌振", "蔣敬", "呂方 ",
"郭盛", "安道全", "皇甫端", "王英", "扈三娘", "鮑旭", "樊瑞", "孔明", "孔亮", " 項充",
"李袞", "金大堅", "馬麟", "童威", "童猛", "孟康", "侯健", "陳達", "楊春", "鄭天壽 ",
"陶宗旺", "宋清", "樂和", "龔旺", "丁得孫", "穆春", "曹正", "宋萬", "杜遷", "薛永 ", "施恩",
"周通", "李忠", "杜興", "湯隆", "鄒淵", "鄒潤", "朱富", "朱貴", "蔡福", "蔡慶", " 李立",
"李雲", "焦挺", "石勇", "孫新", "顧大嫂", "張青", "孫二孃", " 王定六", "鬱保四", " 白勝",
"時遷", "段景柱"};
}
修改SwipeLayout 的OnSwipeListener 介面,在回撥介面方法時把自己傳出去
public interface OnSwipeListener{
//通知外界已經開啟
public void onOpen(SwipeLayout swipeLayout);
//通知外界已經關閉
public void onClose(SwipeLayout swipeLayout);
//通知外界將要開啟
public void onStartOpen(SwipeLayout swipeLayout);
//通知外界將要關閉
public void onStartClose(SwipeLayout swipeLayout);
}
ListView 的Adapter
public class MyAdapter extends BaseAdapter {
private Context context;
//記錄上一次被開啟item
private SwipeLayout lastOpenedSwipeLayout;
public MyAdapter(Context context) {
super();
this.context = context;
}
@Override
public int getCount() {
return Cheeses.NAMES.length;
}
@Override
public Object getItem(int position) {
return Cheeses.NAMES[position];
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if(convertView == null){
convertView = View.inflate(context, R.layout.list_item, null);
}
TextView name = (TextView) convertView.findViewById(R.id.name);
name.setText(Cheeses.NAMES[position]);
SwipeLayout swipeLayout = (SwipeLayout) convertView;
swipeLayout.setOnSwipeListener(new OnSwipeListener() {
@Override
public void onOpen(SwipeLayout swipeLayout) {
//當前item 被開啟時,記錄下此item
lastOpenedSwipeLayout = swipeLayout;
}
@Override
public void onClose(SwipeLayout swipeLayout) {
}
@Override
public void onStartOpen(SwipeLayout swipeLayout) {
//當前item 將要開啟時關閉上一次開啟的item
if(lastOpenedSwipeLayout != null){
lastOpenedSwipeLayout.close();
}
}
@Override
public void onStartClose(SwipeLayout swipeLayout) {
}
});
return convertView;
}
}