在前面的一篇文章我們了解到了ViewDragHelper的一些基本使用——http://www.itdecent.cn/p/d296cfa2f902,知道通過(guò)調(diào)用他的api可以簡(jiǎn)單的實(shí)現(xiàn)一個(gè)浮動(dòng)的屏幕上的控件,那么本篇文章我們玩點(diǎn)更有意思,實(shí)現(xiàn)一個(gè)類(lèi)似與QQ的側(cè)滑菜單。


首先,第一步,創(chuàng)建一個(gè)類(lèi)繼承View或者ViewGroup,這里繼承哪個(gè)玩意呢,我們?nèi)ソ馄室幌聜?cè)滑菜單。

殘忍解剖之后發(fā)現(xiàn)我們應(yīng)該是繼承容器,我們常用的布局LinearLayout和RelativeLayout以及FramLayout他們都是繼承自ViewGroup,這里的話(huà)我們繼承FramLayout,就完成了第一步。
第二步:重寫(xiě)構(gòu)造方法,在構(gòu)造方法中做一些初始化操作。

為什么通常是重寫(xiě)三個(gè)構(gòu)造方法呢,我寫(xiě)一個(gè)好不好,隨你開(kāi)心,不寫(xiě)都可以,關(guān)電腦睡覺(jué)!謝謝大家,本文到此結(jié)束~~
寫(xiě)三個(gè)當(dāng)然是有原因的?。。。?!重要的事情說(shuō)三遍,重要的自定義寫(xiě)三個(gè)構(gòu)造方法?。。。。。。。。?!
1,在代碼中直接new一個(gè)Custom View實(shí)例的時(shí)候,會(huì)調(diào)用第一個(gè)構(gòu)造函數(shù).這個(gè)沒(méi)有任何爭(zhēng)議.
2,在xml布局文件中調(diào)用Custom View的時(shí)候,會(huì)調(diào)用第二個(gè)構(gòu)造函數(shù).這個(gè)也沒(méi)有爭(zhēng)議.
3,在xml布局文件中調(diào)用Custom View,并且Custom View標(biāo)簽中還有自定義屬性時(shí),這里調(diào)用的還是第二個(gè)構(gòu)造函數(shù).
系統(tǒng)默認(rèn)只會(huì)調(diào)用Custom View的前兩個(gè)構(gòu)造函數(shù),至于第三個(gè)構(gòu)造函數(shù)的調(diào)用,通常是我們自己在構(gòu)造函數(shù)中主動(dòng)調(diào)用的(例如,在第二個(gè)構(gòu)造函數(shù)中調(diào)用第三個(gè)構(gòu)造函數(shù)).
所以我們的寫(xiě)法是:

好了我們接下來(lái)就不按步驟了,畢竟思維不是固定的。

我們先想想,實(shí)現(xiàn)它大概需要那些屬性,同樣的我們畫(huà)圖來(lái)理解,更加簡(jiǎn)單。

這樣的分析再給我來(lái)一份!?。。。?!

好了,分析的差不多了,思路異常情緒,該開(kāi)始動(dòng)手操作了。

private static final String TAG="SwipeLayout";
private ViewDragHelper dragHelper;//主角
private Status status= Status.CLOSE;//拖拽狀態(tài),默認(rèn)關(guān)閉
private View backView;//側(cè)滑所出現(xiàn)的菜單
private View frontView;//正常內(nèi)容區(qū)域
private int height;//自定義控件高
private int width;//自定義控件寬
private int range;//可滑動(dòng)范圍
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status= status;
}
//枚舉三種狀態(tài)
public static enumStatus{
OPEN,CLOSE,DRAGING
}
我們的主角應(yīng)該登場(chǎng)了,ViewDragHelper.CallBack,我們對(duì)于所有手勢(shì)操作,監(jiān)聽(tīng)處理等等都在其內(nèi)部。


不要著急?。。?!一步一步來(lái)實(shí)現(xiàn),我們離勝利已經(jīng)很近了,這里就不多啰嗦了,詳細(xì)注釋獻(xiàn)上,用心去體會(huì)。這里先列出幾個(gè)注意點(diǎn):
1,在frontView向左拖動(dòng)時(shí),backView也需要通過(guò)offsetLeftAndRight或者Scroller等出現(xiàn)在滑動(dòng)區(qū)域內(nèi)(向右拖動(dòng)關(guān)閉backView時(shí)同理)
2,使用computeScroll使得拖動(dòng)效果更加平滑流暢
3,無(wú)論拖動(dòng)距離為多少,swipeLayout始終只有三種狀態(tài),open,close,draging等
4,需要計(jì)算側(cè)滑菜單(backView)的矩形區(qū)域和內(nèi)容的矩形去區(qū)域(frontView),通過(guò)所占矩形坐標(biāo)計(jì)算
5,需要?jiǎng)?chuàng)建OnSwipeChangeListener接口,用來(lái)做拖拽事件監(jiān)聽(tīng)器,也要用在列表中,實(shí)現(xiàn)打開(kāi)下一個(gè)時(shí)上一個(gè)自動(dòng)關(guān)閉。
6,攔截事件交給ViewDragHelper處理
packagecom.example.xjf.swipedemo;
importandroid.content.Context;
importandroid.graphics.Rect;
importandroid.support.v4.view.ViewCompat;
importandroid.support.v4.widget.ViewDragHelper;
importandroid.util.AttributeSet;
importandroid.util.Log;
importandroid.view.MotionEvent;
importandroid.view.View;
importandroid.view.ViewGroup;
importandroid.widget.FrameLayout;
/**
* Created by xjf on 2017/10/13.
*/
public class SwipeLayout extends FrameLayout{
public static final String TAG="SwipeLayout";
private ViewDragHelper dragHelper;
private OnSwipeChangeListener swipeChangeListener;
private Status status=Status.CLOSE;//拖拽狀態(tài) 默認(rèn)關(guān)閉
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status= status;
}
public static enum Status {
OPEN,CLOSE,DRAGING
}
//拖拽事件監(jiān)聽(tīng)器,為了更好的處理我們的滑動(dòng),需要監(jiān)聽(tīng)各種狀態(tài)
public static interface OnSwipeChangeListener {
void onDraging(SwipeLayout mSwipeLayout);
void onOpen(SwipeLayout mSwipeLayout);
void onClose(SwipeLayout mSwipeLayout);
void onStartOpen(SwipeLayout mSwipeLayout);
void onStartClose(SwipeLayout mSwipeLayout);
}
//重寫(xiě)三個(gè)構(gòu)造方法
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);
dragHelper= ViewDragHelper.create(this,1.0f,callback);
}
private ViewDragHelper.Callbackcallback=new ViewDragHelper.Callback() {
//所有子View都可拖拽
public boolean tryCaptureView(View child,int pointerId) {
return true;
}
//水平拖拽后處理,return的值為child最后要到的位置,所以我們只需要分析好幾種情況
public int clampViewPositionHorizontal(View child,int left,int dx) {
if(child ==frontView) {//當(dāng)前觸摸View為frontView時(shí)
if(left >0) {//left大于0表示的情景為frontView占滿(mǎn)item,backView處于close狀態(tài)時(shí)向右滑動(dòng)的情景
return 0;
}else if(left < -range) {//left為負(fù)數(shù)表示的情景為backView處于open狀態(tài)時(shí)向左滑動(dòng)
return -range;
}
}else if(child ==backView) {//當(dāng)前觸摸View為backView時(shí)(只有backView處于open時(shí)才能觸摸到哦)
if(left < width-range){
return width-range;
}
}
return left;
}
/**
* 子視圖位置發(fā)生變化時(shí),回調(diào)此函數(shù),在此處移動(dòng)控件位置來(lái)實(shí)現(xiàn)效果
* 必須弄清楚的一個(gè)概念,clampViewPositionHorizontal只能改變當(dāng)然觸摸的View,
* 意思就是當(dāng)你觸摸在frontView時(shí),將frontView左移,那么我們的backView在frontView
* 左移的同時(shí)也應(yīng)該更新其位置,不然的話(huà)我們的側(cè)滑菜單就不會(huì)出現(xiàn)?。?!
*@paramchangedView
*@paramleft
*@paramtop
*@paramdx
*@paramdy
*/
public void onViewPositionChanged(View changedView,int left,int top,int dx,int dy) {
if(changedView ==frontView) {
backView.offsetLeftAndRight(dx);
}else if(changedView ==backView) {
frontView.offsetLeftAndRight(dx);
}
//事件派發(fā)
dispatchSwipeEvent();
//兼容低版本,不停繪制屏幕
invalidate();
};
/**
* 在ACTION_UP后調(diào)用回調(diào)接口中的onViewReleased方法,此方法中一個(gè)重要的任務(wù)是在ACTION_UP
* 事件后實(shí)現(xiàn)view的自動(dòng)滑動(dòng),這里主要是使用了ViewDragHelper中smoothSlideViewTo方法,
* start了ViewDragHelper=中的mScroller:
*@paramreleasedChild
*@paramxvelx軸方向的速率
*@paramyvely軸方向的速率
*/
public void onViewReleased(View releasedChild,float xvel,float yvel) {
if(xvel ==0&&frontView.getLeft() < -range*0.3f) {//調(diào)整靈敏度
open();
}else if(xvel <0) {
open();
}else{
close();
}
};
//子View如果是clickable,必須重寫(xiě)的方法,返回一個(gè)大于0的數(shù)
public int getViewHorizontalDragRange(View child) {
return1;
}
public int getViewVerticalDragRange(View child) {
return1;
}
};
//根據(jù)當(dāng)前狀態(tài)判斷回調(diào)事件
protected void dispatchSwipeEvent() {
Status preStatus=status;//當(dāng)前狀態(tài)
status=updateStatus();
if(swipeChangeListener!=null){
swipeChangeListener.onDraging(this);
}
if(preStatus!=status&&swipeChangeListener!=null){
if(status==Status.CLOSE){
swipeChangeListener.onClose(this);
}else if(status==Status.OPEN){
swipeChangeListener.onOpen(this);
}else if(status==Status.DRAGING){
if(preStatus==Status.CLOSE){
swipeChangeListener.onStartOpen(this);
}else if(preStatus==Status.OPEN){
swipeChangeListener.onStartClose(this);
}
}
}
}
//更新?tīng)顟B(tài)
privateStatus updateStatus() {
intleft=frontView.getLeft();//獲取到滑動(dòng)距離
if(left==0){//為0表示關(guān)閉中
returnStatus.CLOSE;
}else if(left==-range){//觀(guān)察clampViewPositionHorizontal,此時(shí)表示打開(kāi)
returnStatus.OPEN;
}
returnStatus.DRAGING;
}
privateViewbackView;//側(cè)滑菜單
privateViewfrontView;//內(nèi)容區(qū)域
private intheight;//自定義控件布局高
private intwidth;//自定義控件布局寬
private intrange;//側(cè)滑菜單可滑動(dòng)范圍
// 持續(xù)平滑動(dòng)畫(huà) 高頻調(diào)用
public void computeScroll() {
// 如果返回true,動(dòng)畫(huà)還需要繼續(xù)
if(dragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
};
public voidopen() {
open(true);
}
//打開(kāi)backView
public void open(boolean isSmooth) {
intfinalLeft = -range;
if(isSmooth) {
//移動(dòng)時(shí)平滑效果
if(dragHelper.smoothSlideViewTo(frontView, finalLeft,0)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}else{
layoutContent(true);
}
}
public void close() {
close(true);
}
//關(guān)閉backView
public voidclose(boolean isSmooth) {
intfinalLeft =0;
if(isSmooth) {
if(dragHelper.smoothSlideViewTo(frontView, finalLeft,0)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}else{
layoutContent(false);
}
}
//布局子View
protected void onLayout(boolean changed,int left,int top,int right,int bottom) {
super.onLayout(changed, left, top, right, bottom);
layoutContent(false);
}
/**
*@paramisOpen側(cè)滑菜單是否打開(kāi)
*/
private void layoutContent(boolean isOpen) {
Rect frontRect = computeFrontViewRect(isOpen);
frontView.layout(frontRect.left, frontRect.top, frontRect.right, frontRect.bottom);
Rect backRect = computeBackViewRect(frontRect);
backView.layout(backRect.left, backRect.top, backRect.right, backRect.bottom);
//調(diào)整順序
//? ? bringChildToFront(frontView);
}
/**
* 通過(guò)內(nèi)容區(qū)域所占矩形坐標(biāo)計(jì)算側(cè)滑菜單的矩形位置區(qū)域
*@paramfrontRect內(nèi)容區(qū)域所占矩形
*@return
*/
private Rect computeBackViewRect(Rect frontRect) {
intleft = frontRect.right;
return new Rect(left,0, left +range,height);
}
/**
* 通過(guò)菜單打開(kāi)與否isOpen計(jì)算內(nèi)容區(qū)域的矩形區(qū)
*@paramisOpen
*@return
*/
private Rect computeFrontViewRect(boolean isOpen) {
intleft =0;
if(isOpen) {
left = -range;
Log.e(TAG,"left:"+left);
}
return new Rect(left,0, left +width,height);
}
//獲取兩個(gè)View
protected voidonFinishInflate() {
super.onFinishInflate();
intchildCount = getChildCount();
if(childCount <2) {
throw new IllegalStateException("you need 2 children view");
}
if(!(getChildAt(0) instanceof ViewGroup) || !(getChildAt(1) ?instanceof ViewGroup)) {
throw new IllegalArgumentException("your children must be instance of ViewGroup");
}
backView= getChildAt(0);//側(cè)滑菜單
frontView= getChildAt(1);//內(nèi)容區(qū)域
}
//初始化布局的高h(yuǎn)eight寬width以及可滑動(dòng)的范圍range
protected void onSizeChanged(int w,int h,into ldw,int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
height=frontView.getMeasuredHeight();
width=frontView.getMeasuredWidth();
range=backView.getMeasuredWidth();
}
public booleanonInterceptTouchEvent(android.view.MotionEvent ev) {
returndragHelper.shouldInterceptTouchEvent(ev);
};
@Override
public booleanonTouchEvent(MotionEvent event) {
dragHelper.processTouchEvent(event);
return true;
}
public voidsetSwipeChangeListener(OnSwipeChangeListener swipeChangeListener) {
this.swipeChangeListener= swipeChangeListener;
}
}

對(duì)了,忘了把xml布局和activity以及適配器代碼截圖貼出來(lái)了。



最后來(lái)個(gè)效果圖,不知道怎么錄制gif

比較尷尬的時(shí)每次在csdn上傳資源時(shí)都默認(rèn)必選要積分,如果有知道如何取消積分的朋友還望告知,謝謝
這里把源碼的下載鏈接共享給大家:http://download.csdn.net/download/xxsyzszq/10020809