自己使用ViewDragHelper擼一個(gè)類(lèi)似與QQ的側(cè)滑菜單

在前面的一篇文章我們了解到了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ě)法是:


一目了然的層級(jí)調(diào)用


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


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


臥槽,這波分析很有道理

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


說(shuō)來(lái)就來(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)部。



各種操作的回調(diào)函數(shù)


不要著急?。。?!一步一步來(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;

}

}



到此結(jié)束

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


以上時(shí)Activity和Adapter代碼



布局代碼

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


比較尷尬的時(shí)每次在csdn上傳資源時(shí)都默認(rèn)必選要積分,如果有知道如何取消積分的朋友還望告知,謝謝

這里把源碼的下載鏈接共享給大家:http://download.csdn.net/download/xxsyzszq/10020809

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 沒(méi)有找到自己想要的效果的側(cè)滑菜單,花了些時(shí)間研究了一下能完成項(xiàng)目需求就行了。效果如下: 因?yàn)檫壿嫳容^簡(jiǎn)單,總代碼量...
    Kerry202閱讀 879評(píng)論 0 4
  • 背景 一年多以前我在知乎上答了有關(guān)LeetCode的問(wèn)題, 分享了一些自己做題目的經(jīng)驗(yàn)。 張土汪:刷leetcod...
    土汪閱讀 12,921評(píng)論 0 33
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法,類(lèi)相關(guān)的語(yǔ)法,內(nèi)部類(lèi)的語(yǔ)法,繼承相關(guān)的語(yǔ)法,異常的語(yǔ)法,線(xiàn)程的語(yǔ)...
    子非魚(yú)_t_閱讀 34,692評(píng)論 18 399
  • 一. Java基礎(chǔ)部分.................................................
    wy_sure閱讀 4,011評(píng)論 0 11
  • 翻開(kāi)手機(jī)相冊(cè),回憶著2015-2016年發(fā)生的點(diǎn)點(diǎn)滴滴,苦中有樂(lè),喜憂(yōu)參半。 歡喜的是我有一幫不離不棄的小...
    蓋穎閱讀 266評(píng)論 0 1

友情鏈接更多精彩內(nèi)容