自定义控件:侧拉删除
SwipeLayout 側(cè)拉刪除
- 掌握ViewDragHelper 的用法
- 掌握平滑動畫的原理及狀態(tài)更新事件回調(diào)
應(yīng)用場景:QQ 聊天記錄,郵件管理,需要對條目進(jìn)行功能擴(kuò)展的場景,效果圖:
ViewDragHelper 初始化
創(chuàng)建自定義控件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.創(chuàng)建ViewDragHelpermHelper = ViewDragHelper.create(this, mCallback);}//2.轉(zhuǎn)交觸摸事件,攔截判斷,處理觸摸事件@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {return mHelper.shouldInterceptTouchEvent(ev);};@Overridepublic boolean onTouchEvent(MotionEvent event) {try {//多點(diǎn)觸摸有一些小bug,最好catch 一下mHelper.processTouchEvent(event);} catch (Exception e) {}//消費(fèi)事件,返回truereturn true;};//3.處理回調(diào)事件ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {@Overridepublic boolean tryCaptureView(View child, int pointerId) {return false;}}; }第3-8 行通過構(gòu)造方法互調(diào),將三個(gè)構(gòu)造方法串連起來,這樣初始化代碼只需要寫在第三個(gè)構(gòu)造方法中即可
第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 方法,實(shí)現(xiàn)簡單的拖拽
//3.處理回調(diào)事件 ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {//返回值決定了child 是否可以被拖拽@Overridepublic boolean tryCaptureView(View child, int pointerId) {//child 被用戶拖拽的孩子return true;}//返回值決定將要移動到的位置,此時(shí)還沒有發(fā)生真正的移動@Overridepublic int clampViewPositionHorizontal(View child, int left, int dx) {//left 建議移動到的位置return left;} };拖拽事件的傳遞
限定拖拽范圍
第一個(gè)子view 命名為后布局,第二個(gè)子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 回調(diào)方法
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {//返回值決定了child 是否可以被拖拽@Overridepublic boolean tryCaptureView(View child, int pointerId) {return true;}//返回拖拽的范圍,返回一個(gè)大于0 的值,計(jì)算動畫執(zhí)行的時(shí)長,水平方向是否可以被滑開@Overridepublic int getViewHorizontalDragRange(View child) {return mRange;}//返回值決定將要移動到的位置,此時(shí)還沒有發(fā)生真正的移動@Overridepublic int clampViewPositionHorizontal(View child, int left, int dx) {// left 建議移動到位置if (child == mFrontView) {//限定前布局的拖拽范圍if (left < -mRange) {//前布局最小的左邊位置不能小于-mRangeleft = -mRange;} else if (left > 0) {//前布局最大的左邊位置不能大于0left = 0;}} else if (child == mBackView) {//限定后布局的拖拽范圍if (left < mWidth - mRange) {//后布局最小左邊位置不能小于mWidth - mRangeleft = mWidth - mRange;} else if (left > mWidth) {//后布局最大的左邊位置不能大于mWidthleft = mWidth;}}return left;} };第7-11 行需要返回一個(gè)大于0 的拖拽范圍
第14-37 行通過mRange 分別計(jì)算前后布局的拖拽范圍
傳遞拖拽事件
初始化前后布局的位置,重寫SwipeLayout 的onLayout()方法
@Override protected void onLayout(boolean changed, int left, int top, int right,int bottom) {super.onLayout(changed, left, top, right, bottom);//默認(rèn)是關(guān)閉狀態(tài)layoutContent(false); }; private void layoutContent(boolean isOpen) {//設(shè)置前布局位置Rect rect = computeFrontRect(isOpen);mFrontView.layout(rect.left, rect.top, rect.right, rect.bottom);//根據(jù)前布局位置計(jì)算后布局位置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); } /*** 計(jì)算布局所在矩形區(qū)域* @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 行由于后布局是連接在前布局后面一起滑動的,所以可以通過前布局的位置計(jì)算后布局的位置
前后布局在拖拽過程中互相傳遞變化量
第38-54 行拖拽前布局時(shí),將前布局的變化量傳遞給后布局,拖拽后布局時(shí),把后布局的變化量傳遞給前布局,這樣前后布局就可以連動起來
結(jié)束動畫
跳轉(zhuǎn)動畫
// 3.處理回調(diào)事件 ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {// 返回值決定了child 是否可以被拖拽@Overridepublic boolean tryCaptureView(View child, int pointerId) {return true;}// 返回拖拽的范圍,返回一個(gè)大于0 的值,計(jì)算動畫執(zhí)行的時(shí)長,水平方向是否可以被滑開@Overridepublic int getViewHorizontalDragRange(View child) {return mRange;}// 返回值決定將要移動到的位置,此時(shí)還沒有發(fā)生真正的移動@Overridepublic int clampViewPositionHorizontal(View child, int left, int dx) {// left 建議移動到位置if (child == mFrontView) {// 限定前布局的拖拽范圍if (left < -mRange) {// 前布局最小的左邊位置不能小于-mRangeleft = -mRange;} else if (left > 0) {// 前布局最大的左邊位置不能大于0left = 0;}} else if (child == mBackView) {// 限定后布局的拖拽范圍if (left < mWidth - mRange) {// 后布局最小左邊位置不能小于mWidth - mRangeleft = mWidth - mRange;} else if (left > mWidth) {// 后布局最大的左邊位置不能大于mWidthleft = mWidth;}}return left;}@Overridepublic void onViewPositionChanged(View changedView, int left, int top,int dx, int dy) {super.onViewPositionChanged(changedView, left, top, dx, dy);//left 最新的水平位置//dx 剛剛發(fā)生的水平變化量//位置變化時(shí),把水平變化量傳遞給另一個(gè)布局if(changedView == mFrontView){//拖拽的是前布局,把剛剛發(fā)生的變化量dx 傳遞給后布局mBackView.offsetLeftAndRight(dx);}else if(changedView == mBackView){//拖拽的是后布局,把剛剛發(fā)生的變化量dx 傳遞給前布局mFrontView.offsetLeftAndRight(dx);}//兼容低版本,重繪一次界面invalidate();}//松手時(shí)會被調(diào)用@Overridepublic void onViewReleased(View releasedChild, float xvel, float yvel) {super.onViewReleased(releasedChild, xvel, yvel);//xvel 水平方向上的速度,向左為-,向右為+if(xvel == 0 && mFrontView.getLeft() < -mRange * 0.5f){//xvel 變0 時(shí),并且前布局的左邊位置小于-mRange 的一半open();}else if (xvel < 0){//xvel 為-時(shí),打開open();}else{//其它情況為關(guān)閉close();}} }; public void close() {//調(diào)用之前布局子view 的方法直接跳轉(zhuǎn)到關(guān)閉位置layoutContent(false); }public void open() {//調(diào)用之前布局子view 的方法直接跳轉(zhuǎn)到打開位置layoutContent(true); }第55-70 行重寫Callback 的onViewReleased()方法,該方法在松手后被調(diào)用,結(jié)束動畫需要在此處做
平滑動畫
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)開啟一個(gè)平滑動畫將child//移動到finalLeft,finalTop 的位置上。此方法返回true 說明當(dāng)前位置不是最終位置需要重繪if(mHelper.smoothSlideViewTo(mFrontView, finalLeft, 0)){//調(diào)用重繪方法//invalidate();可能會丟幀,此處推薦使用ViewCompat.postInvalidateOnAnimation()//參數(shù)一定要傳child 所在的容器,因?yàn)橹挥腥萜鞑胖纁hild 應(yīng)該擺放在什么位置ViewCompat.postInvalidateOnAnimation(this);};} else {layoutContent(true);} } //重繪時(shí)computeScroll()方法會被調(diào)用 @Override public void computeScroll() {super.computeScroll();//mHelper.continueSettling(deferCallbacks)維持動畫的繼續(xù),返回true 表示還需要重繪if(mHelper.continueSettling(true)){ViewCompat.postInvalidateOnAnimation(this);} }第1-31 行重載open(),close()方法,保留跳轉(zhuǎn)動畫,添加平滑動畫
第32-39 行重寫computeScroll()方法維持動畫的繼續(xù),此處必須重寫,否則沒有動畫效果
監(jiān)聽回調(diào)
定義回調(diào)接口
在SwipeLayout 中定義公開的接口
//控件有三種狀態(tài) public enum Status{Open,Close,Swiping } //初始狀態(tài)為關(guān)閉 private Status status = Status.Close; public Status getStatus() {return status; }public void setStatus(Status status) {this.status = status; }public interface OnSwipeListener{//通知外界已經(jīng)打開public void onOpen();//通知外界已經(jīng)關(guān)閉public void onClose();//通知外界將要打開public void onStartOpen();//通知外界將要關(guān)閉public void onStartClose(); } private OnSwipeListener onSwipeListener; public OnSwipeListener getOnSwipeListener() {return onSwipeListener; } public void setOnSwipeListener(OnSwipeListener onSwipeListener) {this.onSwipeListener = onSwipeListener; }第20-23 行SwipeLayout 做為ListView 的item 時(shí)將要打開或關(guān)閉時(shí)需要通知其它item 做相應(yīng)的處理,所以增加這兩個(gè)方法
更新狀態(tài)及回調(diào)監(jiān)聽
修改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 剛剛發(fā)生的水平變化量//位置變化時(shí),把水平變化量傳遞給另一個(gè)布局if(changedView == mFrontView){//拖拽的是前布局,把剛剛發(fā)生的變化量dx 傳遞給后布局mBackView.offsetLeftAndRight(dx);}else if(changedView == mBackView){//拖拽的是后布局,把剛剛發(fā)生的變化量dx 傳遞給前布局mFrontView.offsetLeftAndRight(dx);}//更新狀態(tài)及調(diào)用監(jiān)聽dispatchDragEvent();//兼容低版本,重繪一次界面invalidate(); }第15-16 行調(diào)用更新狀態(tài)及回調(diào)監(jiān)聽的方法
dispatchDragEvent()方法
修改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 中設(shè)置監(jiān)聽回調(diào)
public class MainActivity extends Activity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);SwipeLayout swipeLayout = (SwipeLayout) findViewById(R.id.sl);swipeLayout.setOnSwipeListener(new OnSwipeListener() {@Overridepublic void onStartOpen() {Utils.showToast(getApplicationContext(), "要去打開了");}@Overridepublic void onStartClose() {Utils.showToast(getApplicationContext(), "要去關(guān)閉了");}@Overridepublic void onOpen() {Utils.showToast(getApplicationContext(), "已經(jīng)打開了");}@Overridepublic void onClose() {Utils.showToast(getApplicationContext(), "已經(jīng)關(guān)閉了");}});} }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" ><ListViewandroid:id="@+id/lv"android:layout_width="match_parent"android:layout_height="match_parent" ></ListView> </RelativeLayout>ListView 需要的數(shù)據(jù)
public class Cheeses {public static final String[] NAMES = new String[]{"宋江", "盧俊義", "吳用","公孫勝", "關(guān)勝", "林沖", "秦明", "呼延灼", "花榮", "柴進(jìn)", "李應(yīng)", "朱仝", "魯智 深","武松", "董平", "張清", "楊志", "徐寧", "索超", "戴宗", "劉唐", "李逵", "史進(jìn)", " 穆弘","雷橫", "李俊", "阮小二", "張橫", "阮小五", " 張順", "阮小七", "楊雄", "石秀", " 解珍"," 解寶", "燕青", "朱武", "黃信", "孫立", "宣贊", "郝思文", "韓滔", "彭玘", "單廷珪 ","魏定國", "蕭讓", "裴宣", "歐鵬", "鄧飛", " 燕順", "楊林", "凌振", "蔣敬", "呂方 ","郭盛", "安道全", "皇甫端", "王英", "扈三娘", "鮑旭", "樊瑞", "孔明", "孔亮", " 項(xiàng)充","李袞", "金大堅(jiān)", "馬麟", "童威", "童猛", "孟康", "侯健", "陳達(dá)", "楊春", "鄭天壽 ","陶宗旺", "宋清", "樂和", "龔?fù)?#34;, "丁得孫", "穆春", "曹正", "宋萬", "杜遷", "薛永 ", "施恩","周通", "李忠", "杜興", "湯隆", "鄒淵", "鄒潤", "朱富", "朱貴", "蔡福", "蔡慶", " 李立","李云", "焦挺", "石勇", "孫新", "顧大嫂", "張青", "孫二娘", " 王定六", "郁保四", " 白勝","時(shí)遷", "段景柱"}; }修改SwipeLayout 的OnSwipeListener 接口,在回調(diào)接口方法時(shí)把自己傳出去
public interface OnSwipeListener{//通知外界已經(jīng)打開public void onOpen(SwipeLayout swipeLayout);//通知外界已經(jīng)關(guān)閉public void onClose(SwipeLayout swipeLayout);//通知外界將要打開public void onStartOpen(SwipeLayout swipeLayout);//通知外界將要關(guān)閉public void onStartClose(SwipeLayout swipeLayout); }ListView 的Adapter
public class MyAdapter extends BaseAdapter {private Context context;//記錄上一次被打開itemprivate SwipeLayout lastOpenedSwipeLayout;public MyAdapter(Context context) {super();this.context = context;}@Overridepublic int getCount() {return Cheeses.NAMES.length;}@Overridepublic Object getItem(int position) {return Cheeses.NAMES[position];}@Overridepublic long getItemId(int position) {return position;}@Overridepublic 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() {@Overridepublic void onOpen(SwipeLayout swipeLayout) {//當(dāng)前item 被打開時(shí),記錄下此itemlastOpenedSwipeLayout = swipeLayout;}@Overridepublic void onClose(SwipeLayout swipeLayout) {}@Overridepublic void onStartOpen(SwipeLayout swipeLayout) {//當(dāng)前item 將要打開時(shí)關(guān)閉上一次打開的itemif(lastOpenedSwipeLayout != null){lastOpenedSwipeLayout.close();}}@Overridepublic void onStartClose(SwipeLayout swipeLayout) {}});return convertView;} }總結(jié)
以上是生活随笔為你收集整理的自定义控件:侧拉删除的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 自定义控件:视差特效
- 下一篇: 自定义控件:QQ气泡效果粘性控件的实现