Android 學(xué)習(xí)筆記 自定義控件之側(cè)滑菜單


  • 這是自定義控件的第二篇學(xué)習(xí)筆記,側(cè)滑菜單,也叫抽屜菜單,在大多數(shù)的應(yīng)用中都有用到,而側(cè)滑的滑字很關(guān)鍵,說(shuō)白了就是移動(dòng);移動(dòng)誰(shuí)呢,自定義控件當(dāng)然是移動(dòng)View,移動(dòng)View有幾種方法:
    • 1.通過(guò)改變view在父View的layout位置來(lái)移動(dòng),但是只能移動(dòng)指定的View:
      view.layout(l,t,r,b);
      view.offsetLeftAndRight(offset);//同時(shí)改變left和right
      view.offsetTopAndBottom(offset);//同時(shí)改變top和bottom
    • 2.通過(guò)改變scrollX和scrollY來(lái)移動(dòng),但是可以移動(dòng)所有的子View;
      scrollTo(x,y);
      scrollBy(xOffset,yOffset);
    • 3.通過(guò)改變Canvas繪制的位置來(lái)移動(dòng)View的內(nèi)容:
      canvas.drawBitmap(bitmap, left, top, paint)

  • 本文所寫(xiě)的自定義控件沒(méi)有用到上面的移動(dòng)方法,而是使用ViewDragHelper這個(gè)類(lèi)來(lái)處理移動(dòng)。
    • 該類(lèi)是谷歌在2013年開(kāi)發(fā)者大會(huì)上提出的,谷歌能在開(kāi)發(fā)者大會(huì)上提出一個(gè)類(lèi),想必這個(gè)類(lèi)一定非常的強(qiáng)大,他主要用于封裝對(duì)View的觸摸位置,觸摸速度,移動(dòng)距離等的檢測(cè),并通過(guò)接口回調(diào)的方式告訴調(diào)用者,處理ViewGroup中子View的拖拽處理,該類(lèi)的本質(zhì)是一個(gè)對(duì)觸摸事件的解析類(lèi)。
  • 對(duì)于ViewDragHelper的使用首先要知道他是在高版本的V4包中(Android 4.4以上的V4包中),其次要明白我們需要用到哪些回調(diào)方法:
    • 首先是 boolean tryCaptureView(View child, int pointerId),它用于判斷是否捕獲當(dāng)前子View的觸摸事件,返回值true:就捕獲并解析 false:不處理
  • int getViewHorizontalDragRange(View child),獲取view水平方向的拖拽范圍,返回的值用在手指抬起的時(shí)候view緩慢移動(dòng)的動(dòng)畫(huà)計(jì)算上面,最好不要返回0
  • int clampViewPositionHorizontal(View child, int left, int dx),控制子View在水平方向的移動(dòng),可以在該方法中控制子View的移動(dòng)范圍,left 表示ViewDragHelper認(rèn)為你想讓當(dāng)前子View的left改變的值(left=child.getLeft()+dx),dx 表示子View水平方向移動(dòng)的距離.
  • onViewPositionChanged(View changedView, int left, int top, int dx, int dy),表示當(dāng)子View的位置改變的時(shí)候執(zhí)行,一般用來(lái)做其他子View的伴隨移動(dòng)
  • onViewReleased(View releasedChild, float xvel, float yvel),手指抬起的執(zhí)行該方法,xvel:表示x方向的移動(dòng)的速度 正:向右移動(dòng), 負(fù):向左移動(dòng); yvel: 同理表示y方向移動(dòng)的速度 正:向上移動(dòng), 負(fù):向下移動(dòng)

下里面來(lái)一發(fā)控件效果圖:

主界面(側(cè)滑菜單關(guān)閉).png
主界面和側(cè)滑菜單(側(cè)滑菜單打開(kāi)).png

看完效果,開(kāi)始擼這個(gè)控件:

  • 自定義控件,自定義View中有子View一般都是繼承ViewGroup,但是我們這個(gè)自定義控件對(duì)于子View擺放位置沒(méi)有特殊的需求,本質(zhì)就是將兩個(gè)子View疊放在一起,這時(shí)候我們就不必要去ViewGroup,然后又去重寫(xiě)OnMessure()方法測(cè)量,onLayout()擺放這么麻煩,直接繼承系統(tǒng)已有的控件FrameLayout就可以幫我把事情給做好了。

  • 繼承FrameLayout,添加必要的構(gòu)造方法,首先重寫(xiě)onFinishInflate()方法,在該方法中獲取兩個(gè)子View對(duì)象,并初始化ViewDragHelper,并且將事件的攔截處理移交給ViewDragHelper,該控件只能有兩個(gè)子View,一個(gè)作為側(cè)邊欄菜單頁(yè),一個(gè)為主界面,不等于兩個(gè)則報(bào)出異常,

/**
 * Created by 毛麒添 on 2017/2/23 0023.
 * 自定義側(cè)滑菜單控件
 */

public class MySlideMenu extends FrameLayout {

    private View leftMenu;//左邊欄對(duì)象
    private View mainMenu;//主界面對(duì)象
    private FloatEvaluator floatEvaluator;//浮點(diǎn)數(shù)計(jì)算器
    private IntEvaluator intEvaluator;//整數(shù)計(jì)算器
    private ViewDragHelper viewDragHelper;

    public MySlideMenu(Context context) {
        super(context);
        init();
    }

    public MySlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public MySlideMenu(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init(){
        viewDragHelper=ViewDragHelper.create(this,callback);
        floatEvaluator=new FloatEvaluator();
        intEvaluator=new IntEvaluator();
    }

@Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        //檢測(cè)控件異常
        if(getChildCount()!=2){
            throw new IllegalArgumentException("MySlideMenu only have two childView!");
        }
        leftMenu = getChildAt(0);
        mainMenu = getChildAt(1);
    }

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //讓viewDragHelper幫助我們判斷是否攔截
        return viewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //讓觸摸事件交給viewDragHelper來(lái)處理
        viewDragHelper.processTouchEvent(event);
        return true;
    }

 private ViewDragHelper.Callback  callback=new ViewDragHelper.Callback() {
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return false;

        }

        @Override
        public int getViewHorizontalDragRange(View child) {
            return super.getViewHorizontalDragRange(child);
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return super.clampViewPositionHorizontal(child, left, dx);
        }

        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
        }
    };
}
  • 接下來(lái)首先需要在boolean tryCaptureView(View child, int pointerId)方法中設(shè)置捕獲兩個(gè)子view的觸摸事件,并設(shè)置側(cè)滑菜單的拖拽范圍
    private int width;//控件寬度
    private float dragRange;//拖拽范圍
    
    /**
     * 該方法在onMeasure執(zhí)行完成后執(zhí)行,可以在該方法中初始化自己和子View的寬高
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        width = getMeasuredWidth();
        dragRange = width*0.6f;
    }

   private ViewDragHelper.Callback callback=new ViewDragHelper.Callback() {
      @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return child==leftMenu||child==mainMenu;
        }
        @Override
        public int getViewHorizontalDragRange(View child) {
            return (int) dragRange;
        }
};
  • 這樣ViewDragHelper已經(jīng)可以獲取兩個(gè)子View的觸摸事件,則要讓觸摸可以移動(dòng),則應(yīng)該在clampViewPositionHorizontal(View child, int left, int dx)方法中返回left,前面已經(jīng)解釋過(guò),該返回值就是你真正想讓子View移動(dòng)的距離,但是這時(shí)候返回這個(gè)值還不夠,你需要做出限制,不讓他可以完全移動(dòng)出屏幕,具體思想就是當(dāng)你的觸摸的是主界面的子View時(shí)候,該子View的左邊left小于0的時(shí)候,說(shuō)明這是已經(jīng)跑出左邊界,則強(qiáng)制等于0,同理右邊大于主界面的可以拖拽范圍的時(shí)候,則強(qiáng)制等于最大拖拽范圍;然后是onViewPositionChanged(View changedView, int left, int top, int dx, int dy),該方法做子View的伴隨移動(dòng),當(dāng)我們移動(dòng)側(cè)滑菜單子View的時(shí)候,希望可以拖動(dòng)側(cè)滑菜單也能讓主界面的子View伴隨側(cè)滑菜單一起移動(dòng),這樣才能顯示出側(cè)滑效果,要不然只有移動(dòng)主界面才能側(cè)滑就不夠生動(dòng);該方法的思想為當(dāng)時(shí)移動(dòng)側(cè)滑菜單的時(shí)候,側(cè)滑菜單固定,并在同第一個(gè)方法的限制方位邏輯下讓主界面子View的位置伴隨側(cè)滑面板一起移動(dòng),說(shuō)了一大段,還是上代碼實(shí)在,將這兩個(gè)方法改造成:
private ViewDragHelper.Callback callback=new ViewDragHelper.Callback() {
 @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            if(child==mainMenu){
                if(left<0) left=0;//限制左邊界
                if(left>dragRange)left= (int) dragRange;//限制右邊界
            }
            return left;
        }

      
        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
           if(changedView==leftMenu){
               //固定側(cè)滑菜單
               leftMenu.layout(0,0,mainMenu.getMeasuredWidth(),mainMenu.getMeasuredHeight());
               int newLeft=mainMenu.getLeft()+dx;
               if(newLeft<0) newLeft=0;//限制左邊界
               if(newLeft>dragRange) newLeft= (int) dragRange;//限制右邊界
               //兩個(gè)菜單一起伴隨滑動(dòng)
               mainMenu.layout(newLeft,mainMenu.getTop()+dy,mainMenu.getRight()+dx,mainMenu.getBottom()+dy);
           }
};
  • 到此該側(cè)滑菜單控件已經(jīng)初步成型,但是這個(gè)側(cè)滑菜單還是太生硬了,拉出或者關(guān)閉側(cè)滑菜單不順滑,這時(shí)候可以使用Scroller來(lái)是移動(dòng)順滑,但是ViewDragHelper就是這么強(qiáng)大,他已經(jīng)將Scroller集成好,我么直接使用就可以,這時(shí)思想為當(dāng)主界面的移動(dòng)范圍小于拖拽的范圍的二分之一,則側(cè)滑菜單自動(dòng)關(guān)閉,大于二分之一,則自動(dòng)打開(kāi),而這些都是手指抬起后的動(dòng)作,所以在 onViewReleased(View releasedChild, float xvel, float yvel)方法做處理:
 private ViewDragHelper.Callback callback=new ViewDragHelper.Callback() {
@Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
           if(mainMenu.getLeft()<dragRange/2){//在拖拽范圍的左邊,關(guān)閉
               close();
           }else {//在拖拽范圍的右邊,打開(kāi)
               open();
           }
       }
};
//側(cè)滑面板打開(kāi)
    public void open() {
        viewDragHelper.smoothSlideViewTo(mainMenu, (int) dragRange,mainMenu.getTop());
        ViewCompat.postInvalidateOnAnimation(MySlideMenu.this);//刷新
    }
    //側(cè)滑面板關(guān)閉
    public void close() {
        viewDragHelper.smoothSlideViewTo(mainMenu,0,mainMenu.getTop());
        ViewCompat.postInvalidateOnAnimation(MySlideMenu.this);//刷新
    }

@Override
    public void computeScroll() {
        if(viewDragHelper.continueSettling(true)){
            ViewCompat.postInvalidateOnAnimation(MySlideMenu.this);//刷新
        }
    }
  • 到此,側(cè)滑面板大概樣子已經(jīng)初步完成,但是為了向抽屜更加形象,則可以在拖拽的過(guò)程中加入動(dòng)畫(huà)效果,我們可以這樣,可以計(jì)算出拖拽過(guò)程的程度除與最大拖拽長(zhǎng)度,就可以得出百分比,根據(jù)這個(gè)百分比來(lái)執(zhí)行伴隨動(dòng)畫(huà),對(duì),就是這樣,因?yàn)閛nViewPositionChanged(View changedView, int left, int top, int dx, int dy)是在View位置變化的時(shí)候執(zhí)行的方法,所以繼續(xù)對(duì)其改造,并加入放大,透明,遮罩等動(dòng)畫(huà):
 @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
           if(changedView==leftMenu){
               //固定側(cè)滑菜單
               leftMenu.layout(0,0,mainMenu.getMeasuredWidth(),mainMenu.getMeasuredHeight());
               int newLeft=mainMenu.getLeft()+dx;
               if(newLeft<0) newLeft=0;//限制左邊界
               if(newLeft>dragRange) newLeft= (int) dragRange;//限制右邊界
               //兩個(gè)菜單一起伴隨滑動(dòng)
               mainMenu.layout(newLeft,mainMenu.getTop()+dy,mainMenu.getRight()+dx,mainMenu.getBottom()+dy);
           }
            //計(jì)算滑動(dòng)百分比
            float fraction=mainMenu.getLeft()/dragRange;
            //執(zhí)行伴隨動(dòng)畫(huà)
            executeAnim(fraction);
}

private void executeAnim(float fraction) {
        //移動(dòng)側(cè)邊欄
        ViewHelper.setTranslationX(leftMenu,intEvaluator.evaluate(fraction,-leftMenu.getMeasuredWidth()/2,0));
        //放大側(cè)邊欄
        ViewHelper.setScaleX(leftMenu,floatEvaluator.evaluate(fraction,0.5f,1f));
        ViewHelper.setScaleY(leftMenu,floatEvaluator.evaluate(fraction,0.5f,1f));
        //改變側(cè)邊欄的透明度
        ViewHelper.setAlpha(leftMenu,floatEvaluator.evaluate(fraction,0.3f,1f));
        //給側(cè)邊欄背景添加黑色遮罩效果
        getBackground().setColorFilter((Integer) ColorUtil.evaluateColor(fraction, Color.BLACK,Color.TRANSPARENT), PorterDuff.Mode.SRC_OVER);
    }
  • ViewHelper是在一個(gè)版本兼容包中,找不到可以下載我的源碼拿(源碼地址在最下面):
ViewHelper所在兼容包.png
  • 接下里就是設(shè)置外部監(jiān)聽(tīng)回調(diào),回調(diào)說(shuō)白了就是空間內(nèi)部發(fā)生的事情需要讓使用者知道,比如你是老板,吩咐員工去外地辦事,員工在外地辦好事打電話給你,就相當(dāng)于回調(diào)(感覺(jué)這個(gè)例子很摳腳);回調(diào)接口定義步驟一般為:
    • 1.定義一個(gè)回調(diào)接口,在接口中定義為實(shí)現(xiàn)的邏輯方法
    • 2.傳遞一個(gè)實(shí)現(xiàn)了此接口類(lèi)的對(duì)象,并且實(shí)現(xiàn)上述接口中未實(shí)現(xiàn)的方法
    • 3.在需要告知外部的地方調(diào)用接口中需要告知的方法
/**
     * 設(shè)置外部監(jiān)聽(tīng)回調(diào)
     */
    private onDragStateChangeListener listener;

    public void setOnDragStateChangeListener(onDragStateChangeListener listener){
        this.listener=listener;
    }

    public interface onDragStateChangeListener{
        /**
         * 側(cè)滑菜單打開(kāi)
         */
        void onOpen();

        /**
         * 側(cè)滑菜單處于關(guān)閉
         */
        void onClose();

        /**
         *正在拖拽,將此時(shí)的百分比隨時(shí)暴露給調(diào)用者
         */
        void Draging(float fraction);
    }
  • 定義好回調(diào)接口,則在需要告知外部的地方使用邏輯方法,也就是在滑動(dòng)拖拽的過(guò)程中根據(jù)拖拽的百分比來(lái)確定側(cè)滑菜單是打開(kāi)或者關(guān)閉,這時(shí)候就可以枚舉出兩個(gè)打開(kāi)或者關(guān)閉的狀態(tài),方便操作;所以,再次改造onViewPositionChanged(View changedView, int left, int top, int dx, int dy)方法:
private DragState currentState=DragState.STATE_CLOSE;//默認(rèn)是關(guān)閉狀態(tài)

    /**
     * 枚舉側(cè)滑菜單的開(kāi)關(guān)狀態(tài)
     */
    public enum DragState{
        STATE_OPEN,STATE_CLOSE
    }

/**
     * 獲取側(cè)滑菜單狀態(tài)
     */
    public DragState getDragState(){
        return currentState;
    }

@Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
           if(changedView==leftMenu){
               //固定側(cè)滑菜單
               leftMenu.layout(0,0,mainMenu.getMeasuredWidth(),mainMenu.getMeasuredHeight());
               int newLeft=mainMenu.getLeft()+dx;
               if(newLeft<0) newLeft=0;//限制左邊界
               if(newLeft>dragRange) newLeft= (int) dragRange;//限制右邊界
               //兩個(gè)菜單一起伴隨滑動(dòng)
               mainMenu.layout(newLeft,mainMenu.getTop()+dy,mainMenu.getRight()+dx,mainMenu.getBottom()+dy);
           }
            //計(jì)算滑動(dòng)百分比
            float fraction=mainMenu.getLeft()/dragRange;
            //執(zhí)行伴隨動(dòng)畫(huà)
            executeAnim(fraction);
            //根據(jù)百分比來(lái)值來(lái)確定側(cè)滑菜單是打開(kāi)還是關(guān)閉
            if(fraction==0&&currentState!=DragState.STATE_CLOSE){//如果百分比是0,且當(dāng)前狀態(tài)不是關(guān)閉
                currentState=DragState.STATE_CLOSE;
                //調(diào)用回調(diào)方法
                listener.onClose();
            }else if(fraction==1&&currentState!=DragState.STATE_OPEN){
                currentState=DragState.STATE_OPEN;
                //調(diào)用回調(diào)方法
                listener.onOpen();
            }
            if(listener!=null){
                //只要listener存在,就將百分比暴露出去
                listener.Draging(fraction);
            }
        }

打這里,側(cè)滑菜單控件類(lèi)的大部分已經(jīng)完成,但是,還差一個(gè)小問(wèn)題,那就是這時(shí)候的側(cè)滑菜單拖動(dòng)打開(kāi)或者關(guān)閉一定要大于或者小于拖動(dòng)范圍的二分之一才能打開(kāi)或者關(guān)閉,而成熟應(yīng)用的的側(cè)滑菜單都是手指一劃就可以打開(kāi)或者關(guān)閉,其實(shí)這是根據(jù)手指滑動(dòng)的速度來(lái)做,上面介紹方法的時(shí)候已介紹過(guò)onViewReleased(View releasedChild, float xvel, float yvel)方法可以獲取X軸和Y軸的速度,所以將其改造為:

@Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
           if(mainMenu.getLeft()<dragRange/2){//在拖拽范圍的左邊,關(guān)閉
               close();
           }else {//在拖拽范圍的右邊,打開(kāi)
               open();
           }
            //當(dāng)用戶稍微滑動(dòng)一下,根據(jù)X軸方向的速度來(lái)打開(kāi)或者關(guān)閉側(cè)滑菜單
            if(xvel>200&&currentState!=DragState.STATE_OPEN){
                open();
            }else if(xvel<-200&&currentState!=DragState.STATE_CLOSE){
                close();
            }
       }

到此,整個(gè)自定義控件側(cè)滑菜單類(lèi)已經(jīng)完成,但是還有一些小瑕疵,當(dāng)側(cè)滑面?zhèn)然姘迨谴蜷_(kāi)狀態(tài)下,發(fā)現(xiàn)主界面的ListView還是可以滑動(dòng),也就是說(shuō)側(cè)滑菜單打開(kāi)的時(shí)候主界面的點(diǎn)擊事件沒(méi)有被攔截,而主界面子View我使用根布局是LinerLayout,所有可以自定義一個(gè)LinerLayout,讓其在側(cè)滑菜單是打開(kāi)的狀態(tài)下攔截事件并消費(fèi)掉就可以了(事件分發(fā)攔截機(jī)制這里不多說(shuō)),并且在打開(kāi)的狀態(tài)下點(diǎn)擊主界面就可以關(guān)閉側(cè)滑菜單,邏輯很簡(jiǎn)單:

/**
 * Created by 毛麒添 on 2017/2/24 0024.
 * 當(dāng)自定的側(cè)滑菜單打開(kāi)的時(shí)候,右側(cè)的主界面菜單不應(yīng)該能滑動(dòng),
 * 自定義一個(gè)LinearLayout攔截并消費(fèi)該觸摸事件
 */

public class MyLinerLayout extends LinearLayout {

    private MySlideMenu mySlideMenu;

    public MyLinerLayout(Context context) {
        super(context);
    }

    public MyLinerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyLinerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void setSlideMenu(MySlideMenu mySlideMenu){
        this.mySlideMenu=mySlideMenu;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if(mySlideMenu!=null&& mySlideMenu.getDragState()== MySlideMenu.DragState.STATE_OPEN){
            //如果該側(cè)滑面板是打開(kāi),則攔截消費(fèi)觸摸事件
           return true;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(mySlideMenu!=null&& mySlideMenu.getDragState()== MySlideMenu.DragState.STATE_OPEN){
            if(event.getAction()==MotionEvent.ACTION_UP){//在側(cè)滑面板打開(kāi)的狀態(tài)時(shí)候點(diǎn)一下主界面應(yīng)該關(guān)閉側(cè)滑面板
                mySlideMenu.close();
            }
            //如果該側(cè)滑面板是打開(kāi),則攔截消費(fèi)觸摸事件
            return true;
        }
        return super.onTouchEvent(event);
    }
}

好了,扯了一大堆,總算了是把這個(gè)自定義控件完成了,布局和MainActivity比較簡(jiǎn)單,這里就不貼了。
整體一步一步走下來(lái),還是能對(duì)技術(shù)有不少提升的。如果有錯(cuò)誤,希望大家可以給我提出來(lái),大家一起學(xué)習(xí)進(jìn)步。

源碼下載地址:https://github.com/maoqitian/MySlideMenu

最后編輯于
?著作權(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)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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