自制控件2 —— 自制控件 仿qq側滑菜單

自定義控件——初識自定義控件里面,我們已經對自定義控件進行描述和分類。其分類分別是

  • 自制控件
  • 組合控件
  • 拓展控件

這篇博文里面,我們繼續進行自制控件。

我們想要繼續的是一個簡單的仿造qq側滑菜單。

先來看一下效果圖

效果圖.gif

在(初識自定義控件)中,我們知道了自定義控件分為三種

  • 自制控件
  • 組合控件
  • 拓展控件

在(自制控件1)中,我們自制了一個開關按鈕View,這次,我們來做自制的ViewGroup,一個簡單的仿qq策劃菜單。

在(自制控件1)我們利用View.layout(l,t,r,b)這個api讓View動起來。在本次的側滑菜單里面,我們使用
ScrollTo和ScrollBy讓View動起來
而且使用Scroller做彈性滑動。

如果對自制繼承自ViewGroup的控件還沒有一個大概的概念,可以通過(初識自定義控件)這篇博文里面的demo,進行一個大概的了解。

一、造起來一個ViewGroup

新建一個類,比如叫做SlideMenu,繼承自ViewGroup

public class SlideMenu extends ViewGroup{
    public SlideMenu(Context context) {
        super(context);
    }
    public SlideMenu(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public SlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    }
}

.
.
然后我們在 activity_main 利用控件的全路徑名引入這個控件

<?xml version="1.0" encoding="utf-8"?>
<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="com.amqr.diyslidemenu.MainActivity">
    <com.amqr.diyslidemenu.view.SlideMenu
        android:id="@+id/mSmenu"
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        >
    </com.amqr.diyslidemenu.view.SlideMenu>
</RelativeLayout>

.
.

二、弄兩個布局文件,一個左側菜單的,一個是主頁部分的。SlideView里面把這兩個布局加載出來

.
1、準備兩個布局文件,左側菜單的布局文件需要控制寬度,這里我們設置為200dp

左側菜單 slide_left.xml

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="200dp"
    android:layout_height="match_parent"
    android:background="#ff0000"
    >
    <!--左側的菜單限定為200dp-->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        >
        <TextView
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:text="本地"
            android:textSize="26sp"
            android:gravity="center"
            android:layout_marginTop="10dp"
            android:background="#689342"
            />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:text="體育"
            android:textSize="26sp"
            android:gravity="center"
            android:layout_marginTop="10dp"
            android:background="#689342"
            />
    </LinearLayout>
    
</ScrollView>
左側菜單.png

主頁部分 slide_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    >
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="主頁內容區域"
        android:layout_gravity="center"
        android:textSize="30dp"
        android:layout_centerInParent="true"
        />
</RelativeLayout>
主頁部分的.png

2、引用xml的布局代碼里面include進來左側菜單和布局文件

這個include的先后順序需要嚴格區分
因為待會需要結合SlideView的onFinishInflate相互結合

<?xml version="1.0" encoding="utf-8"?>
<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="com.amqr.diyslidemenu.MainActivity">
    <com.amqr.diyslidemenu.view.SlideMenu
        android:id="@+id/mSmenu"
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        >
        <!--這個include的先后順序需要嚴格區分,
       因為待會需要結合SlideView的onFinishInflate相互結合 -->
        <!--左側菜單-->
        <include layout="@layout/slide_left"/>
        <!--主頁部分-->
        <include layout="@layout/slide_main"/>
    </com.amqr.diyslidemenu.view.SlideMenu>
</RelativeLayout>

三、利用SlideView的onFinishInflate方法加載view

利用SlideView的onFinishInflate方法加載view

public class SlideMenu extends ViewGroup{
    private View mLeftMenu;
    private View mMainPage;
    public SlideMenu(Context context) {
        super(context);
    }
    public SlideMenu(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public SlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    // 當SlideView被xml引用加載之后完成,這個方法就會調用。
    /**
     * Finalize inflating a view from XML.  This is called as the last phase
     * of inflation, after all child views have been added.
     */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate(); // ???
        //  getChildAt 作用 Returns the view at the specified position in the group.
        //  精確地返回在ViewGroup里面的View的位置 所以我們的include順序很重要
        mLeftMenu = getChildAt(0); // 左側菜單
        mMainPage = getChildAt(1);
    }
}

看一下onFinishInflate這個方法,屬于View類下一個方法

    /**
     * Finalize inflating a view from XML.  This is called as the last phase
     * of inflation, after all child views have been added.
     *
     * <p>Even if the subclass overrides onFinishInflate, they should always be
     * sure to call the super method, so that we get called.
     */
    @CallSuper
    protected void onFinishInflate() {
    }

getChildAt的作用是 精確地返回在ViewGroup里面的View的位置 所以我們的include順序很重要

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate(); // ???
        //  getChildAt 作用 Returns the view at the specified position in the group.
        //  精確地返回在ViewGroup里面的View的位置 所以我們的include順序很重要
        mLeftMenu = getChildAt(0); // 左側菜單
        mMainPage = getChildAt(1);
    }

四、利用onMeasure來孩子測量大小

首先記住,不管干嘛,首先先把現在onMeasure里面把setMeasuredDimension方法給寫上。

說在測量之前的第一點

我們的ViewGroup也是View這點我們都知道,其實到最終,ViewGroup到最后還是給他的父親調用,他的父親就是使用使用measure來測量ViewGroup的大小的。說了這么多,我們還是看一下代碼吧。

<?xml version="1.0" encoding="utf-8"?>
<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="com.amqr.diyslidemenu.MainActivity">



    <!--這個ViewGroup,他的父親就是上面的RelativeLayout,RelativeLayout就是使用measure來測量這個SlideView的大小的-->
    <com.amqr.diyslidemenu.view.SlideMenu
        android:id="@+id/mSmenu"
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        >
        <!--這個include的先后順序需要嚴格區分,
       因為待會需要結合SlideView的onFinishInflate相互結合 -->
        <!--左側菜單-->
        <include layout="@layout/slide_left"/>
        <!--主頁部分-->
        <include layout="@layout/slide_main"/>
    </com.amqr.diyslidemenu.view.SlideMenu>
</RelativeLayout>

這個SlideMenu是ViewGroup,他的父親就是上面的RelativeLayout,RelativeLayout就是使用measure來測量這個SlideView的大小的

明白了onMeasure是給measure調用的之后,我們就應該清楚地知道,onMeasure是父親給孩子用的寬高(父親把自己所能給的都給了,也就是最大的,我們可以采用父親的寬高,我們可以自己指定寬高。但是孩子的自由發揮的空間沒有辦法超出父親所能給的最大值,但是可以比父親小)

說在測量之前的第二點

怎么得到一個View在xml布局文件里面寬?
利用view.getLayoutParams().width,高類似

有了前面的兩點說明,現在我們可以真正式在SlideMenu里面復寫onMeasere方法并且進行測量了

代碼如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // makeMeasureSpec 的時候,第一個參數是大小,第二個參數是模式
        // 寬度我們使用左側菜單的在xml里面的200dp,因為指定大小所以模式是MeasureSpec.EXACTLY
        // 怎么得到一個View在xml布局文件里面寬,利用view.getLayoutParams().width,高類似
        int leftViewMeasureSpecWidth = MeasureSpec.
                makeMeasureSpec(mLeftMenu.getLayoutParams().width, MeasureSpec.EXACTLY);
        //左側菜單的的高度我們希望填充父窗體,而當前onMeasure里面的heightMeasureSpec根據我們的布局顯然就是填充父窗體
        // 所以一直接用父親傳過來的這個32位參數就好
        mLeftMenu.measure(leftViewMeasureSpecWidth,heightMeasureSpec);
        // 至于主頁頁面,我們的希望他寬高都是填充父窗體,所以直接用onMeasure里面傳過來的參數就好啦
        mMainPage.measure(widthMeasureSpec,heightMeasureSpec);
        // onMeasure一開始什么都不管就應該復寫setMeasuredDimension,不然報錯。
        // 除非我們的自定義控件是寬高都是填充父窗體,那么我們就留著下面這句super的代碼就可以
        // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(widthMeasureSpec,heightMeasureSpec);
    }

關于測量的View的measure方法的參數可以參考(初識自定義控件)

五、利用onLayout給自孩子擺放位置

在onMeasure里面給孩子布局采用的方法是 child.measure方法
利用onLayout給自己孩子擺放位置相應來說用的是 child.layout方法

擺放之前,了解getMeasuredWidth();和getWidth()的區別

我們下面說的layout的前提是已經進行onMeasure被執行之后(onMeasure里面必須執行setMeasuredDimension)
getWidth()必須在控件的 layout(l,t,r,b) 被執行過后才能獲取到有效的值,也就在View被繪制好之后才有效
getMeasuredWidth(); 是 layout(l,t,r,b)執行之前就會獲取到View的寬度的,也就是在View被繪制好之前就可以生效的。

先測量,后擺放。

也就是 先onMeasure,后onLayout。

有了這些了解,我們可以來很好地擺放位置了

代碼如下

 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int leftViewWidth = mLeftMenu.getMeasuredWidth();
        int leftViewHeight = mLeftMenu.getMeasuredHeight();
        // l t r b  左上右下  左上一點,右下一點,兩點確定了一個矩形的大小
        mLeftMenu.layout(-leftViewWidth,0,0,leftViewHeight);
        int mainViewWidth = mMainPage.getMeasuredWidth();
        int mainViewHeight = mMainPage.getMeasuredHeight();
        mMainPage.layout(0,0,mainViewWidth,mainViewHeight);
    }

到此為止先停一下,看一下SlideView里面目前的代碼:

SlideMenu

public class SlideMenu extends ViewGroup{
    private View mLeftMenu;
    private View mMainPage;
    public SlideMenu(Context context) {
        super(context);
    }
    public SlideMenu(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public SlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    // 當SlideView被xml引用加載之后完成,這個方法就會調用。
    /**
     * Finalize inflating a view from XML.  This is called as the last phase
     * of inflation, after all child views have been added.
     */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate(); // ???
        //  getChildAt 作用 Returns the view at the specified position in the group.
        //  精確地返回在ViewGroup里面的View的位置 所以我們的include順序很重要
        mLeftMenu = getChildAt(0); // 左側菜單
        mMainPage = getChildAt(1);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // makeMeasureSpec 的時候,第一個參數是大小,第二個參數是模式
        // 寬度我們使用左側菜單的在xml里面的200dp,因為指定大小所以模式是MeasureSpec.EXACTLY
        // 怎么得到一個View在xml布局文件里面寬,利用view.getLayoutParams().width,高類似
        int leftViewMeasureSpecWidth = MeasureSpec.
                makeMeasureSpec(mLeftMenu.getLayoutParams().width, MeasureSpec.EXACTLY);
        //左側菜單的的高度我們希望填充父窗體,而當前onMeasure里面的heightMeasureSpec根據我們的布局顯然就是填充父窗體
        // 所以一直接用父親傳過來的這個32位參數就好
        mLeftMenu.measure(leftViewMeasureSpecWidth,heightMeasureSpec);
        // 至于主頁頁面,我們的希望他寬高都是填充父窗體,所以直接用onMeasure里面傳過來的參數就好啦
        mMainPage.measure(widthMeasureSpec,heightMeasureSpec);
        // onMeasure一開始什么都不管就應該復寫setMeasuredDimension,不然報錯。
        // 除非我們的自定義控件是寬高都是填充父窗體,那么我們就留著下面這句super的代碼就可以
        // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(widthMeasureSpec,heightMeasureSpec);
    }
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int leftViewWidth = mLeftMenu.getMeasuredWidth();
        int leftViewHeight = mLeftMenu.getMeasuredHeight();
        Log.d("Slide","getWidth:"+mLeftMenu.getWidth()+"   mLeftMenu.getHeight():"+mLeftMenu.getHeight());
        Log.d("Slide","getMeasuredWidth:"+mLeftMenu.getMeasuredWidth()+"   getMeasuredHeight:"+mLeftMenu.getMeasuredHeight());
        // l t r b  左上右下  左上一點,右下一點,兩點確定了一個矩形的大小
        mLeftMenu.layout(-leftViewWidth,0,0,leftViewHeight);
        int mainViewWidth = mMainPage.getMeasuredWidth();
        int mainViewHeight = mMainPage.getMeasuredHeight();
        mMainPage.layout(0, 0, mainViewWidth, mainViewHeight);
    }
}

MainActivity

package com.amqr.diyslidemenu;

import android.app.Activity;

import android.os.Bundle;

public class MainActivity extends Activity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

    }

}

運行效果:

當前還無法拉動,但是我們已經過能讓界面顯示出來了。

暫時效果.png

六、開始做移動效果

移動的方式有很多種,這次我們采用的是ScrollTo+ScrollBy方式

說明1: ScrollTo和ScrollBy 的了解

ScrollTo和ScrollBy就是手機屏幕的左上角動,而不是View或者ViewGroup動。

區別是,ScrollTo每次都是都是想比較于最開始的左上角(0,0)
ScrollBy每次的移動是累計的

比如,調用ScollTo(20,0),的時候,那么手機屏幕回向右移動20個單位,但是再次調用ScollTo(20,0),的時候是不動的,因為每次都是跟最開始的(0,0)做比較;然后我們調用ScrollTo(-20,0)的時候,就回到最開的原點。

ScrollBy是累計的,第一次調用ScrollBy(20,0),向右移動20 個單位,再次調用ScrollBy(20,0),那么屏幕的左上角就會移動到(40,0)的位置,因為累計嘛。

說明2: getX和getRawX的了解

getX()是觸摸的點與控件自身的距離
getRawX()是觸摸的點與屏幕的距離
結論:當你觸到按鈕時,x,y是相對于該按鈕左上點(控件本身)的相對位置。而rawx,rawy始終是相對于屏幕的位置。

圖片摘自csdn

說明3:getScrollX()和getScrollY()的了解

getScrollX(): 手機屏幕顯示區域左上角 與 你指定的View的左上角的橫向距離getScrollY(): 手機屏幕顯示區域左上角 與 你指定的View的左上角的垂直距離(因為子視圖的高度和手機屏幕高度一樣)

六.1、簡單的移動,不會產生越界現象

public class SlideMenu extends ViewGroup{
    private View mLeftMenu;
    private View mMainPage;
    private int downX;
    public SlideMenu(Context context) {
        super(context);
    }
    public SlideMenu(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public SlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    // 當SlideView被xml引用加載之后完成,這個方法就會調用。
    /**
     * Finalize inflating a view from XML.  This is called as the last phase
     * of inflation, after all child views have been added.
     */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate(); // ???
        //  getChildAt 作用 Returns the view at the specified position in the group.
        //  精確地返回在ViewGroup里面的View的位置 所以我們的include順序很重要
        mLeftMenu = getChildAt(0); // 左側菜單
        mMainPage = getChildAt(1);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // makeMeasureSpec 的時候,第一個參數是大小,第二個參數是模式
        // 寬度我們使用左側菜單的在xml里面的200dp,因為指定大小所以模式是MeasureSpec.EXACTLY
        // 怎么得到一個View在xml布局文件里面寬,利用view.getLayoutParams().width,高類似
        int leftViewMeasureSpecWidth = MeasureSpec.
                makeMeasureSpec(mLeftMenu.getLayoutParams().width, MeasureSpec.EXACTLY);
        //左側菜單的的高度我們希望填充父窗體,而當前onMeasure里面的heightMeasureSpec根據我們的布局顯然就是填充父窗體
        // 所以一直接用父親傳過來的這個32位參數就好
        mLeftMenu.measure(leftViewMeasureSpecWidth,heightMeasureSpec);
        // 至于主頁頁面,我們的希望他寬高都是填充父窗體,所以直接用onMeasure里面傳過來的參數就好啦
        mMainPage.measure(widthMeasureSpec,heightMeasureSpec);
        // onMeasure一開始什么都不管就應該復寫setMeasuredDimension,不然報錯。
        // 除非我們的自定義控件是寬高都是填充父窗體,那么我們就留著下面這句super的代碼就可以
        // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(widthMeasureSpec,heightMeasureSpec);
    }
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int leftViewWidth = mLeftMenu.getMeasuredWidth();
        int leftViewHeight = mLeftMenu.getMeasuredHeight();
        Log.d("Slide","getWidth:"+mLeftMenu.getWidth()+"   mLeftMenu.getHeight():"+mLeftMenu.getHeight());
        Log.d("Slide","getMeasuredWidth:"+mLeftMenu.getMeasuredWidth()+"   getMeasuredHeight:"+mLeftMenu.getMeasuredHeight());
        // l t r b  左上右下  左上一點,右下一點,兩點確定了一個矩形的大小
        mLeftMenu.layout(-leftViewWidth,0,0,leftViewHeight);
        int mainViewWidth = mMainPage.getMeasuredWidth();
        int mainViewHeight = mMainPage.getMeasuredHeight();
        mMainPage.layout(0, 0, mainViewWidth, mainViewHeight);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //return super.onTouchEvent(event);
        int action = event.getAction();
        switch (action){
            case MotionEvent.ACTION_DOWN:
                downX = (int)(event.getX()+0.5f);
                break;
            case MotionEvent.ACTION_MOVE:
                int moveX = (int)(event.getX()+0.5f);
                // 用減法,比如按下是0,最終移動到了20,那么屏幕向左邊移動20個單位,左側菜單就可顯示出來了
                int distanceX =  downX - moveX;
                int scrollX = getScrollX();  // 注意getScaleX()不要寫成getScaleX()
                // 解決越界問題
                if(scrollX+distanceX < (-mLeftMenu.getMeasuredWidth())){ // 左側越界臨界點
                    scrollTo(-mLeftMenu.getMeasuredWidth(),0);
                }else if(scrollX+distanceX>0){ // 右側越界臨界點
                    scrollTo(0,0);
                }else{  // 在兩個臨界點之間的可移動范圍
                    scrollBy(distanceX,0);
                }
                downX = moveX;
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        //關鍵一步,返回true,代表消費當前的偷吃事件
        return true;
    }
}
簡單的移動,不會產生越界現象.gif

六.2、判斷松手后應該停留在哪一個界面

原理圖.png

做的事情很簡單,其實也就是在 case MotionEvent.ACTION_UP: 里面添加幾行代碼

   case MotionEvent.ACTION_UP:
        int upScrollX = getScrollX();
        if(upScrollX<-(mLeftMenu.getMeasuredWidth()/2)){
            scrollTo(-mLeftMenu.getMeasuredWidth(),0);
        }else{
            scrollTo(0,0);
        }
        break;
判斷松手后應該停留在哪一個界面.gif

六.3、使用Scoller彈性滑動,讓滑動產生過渡效果

自定義View做動畫有很多做法,Scroller是其中一種,也是我們這次要采用的做法。

Scoller彈性滑動是使用過程:
1、實例化Scroller
實例化一個Scroller

@Override
protected void onFinishInflate() {
    super.onFinishInflate(); 
    //  getChildAt 作用 Returns the view at the specified position in the group.
    //  精確地返回在ViewGroup里面的View的位置 所以我們的include順序很重要
    mLeftMenu = getChildAt(0); // 左側菜單
    mMainPage = getChildAt(1);

    scroller = new Scroller(getContext()); // 實例化一個Scroll,需要context
}

2、調用startScroll(startX, startY, dx, dy, durationTime);模擬數據變化,接著調用invalidate(); 觸發computeScroll()

注意點:startScroll 只是模擬數據的變化,想要看到效果還需要調用invalidate重新刷新UI,其實就是調用onDraw

注意點:invalidate();經過輾轉會去computeScroll();
invalidate(); ---> draw()-->onDraw()--> computeScroll();

3、在computeScroll()里面復寫真正讓讓模擬數據生效的代碼,調用invalidate()

注意點:
scroller.computeScrollOffset()
computeScrollOffset()為true代表模擬數據還沒有完成

注意點:scroller.getCurrX()當前時刻正模擬到的數據

代碼如下(其實這幾乎是模板代碼):

@Override
    public void computeScroll() { // 如果數據模擬沒有完成,那么繼續更新
        //super.computeScroll();
        // computeScrollOffset
        if(scroller.computeScrollOffset()){
            scrollTo(scroller.getCurrX(),0);
            invalidate(); // 注意這里還調用了invalidate,這樣才會產生效果
        }
    }

.
.
上面幾點已經說完,具體看看在代碼中怎么使用吧

computeScroll();和computeScrollOffset()的結合使用

        case MotionEvent.ACTION_UP:
            int upScrollX = getScrollX();
            choosePage(upScrollX<-(mLeftMenu.getMeasuredWidth()/2));
            break;
    }
    //關鍵一步,返回true,代表消費當前的偷吃事件
    return true;
}
private void choosePage(boolean isMainPage){
    if(isMainPage){
        startScrollNow(-mLeftMenu.getMeasuredWidth());
        //scrollTo(0,0); 可以實現但是無動畫過渡
    }else{
        startScrollNow(0);
        //scrollTo(-mLeftMenu.getMeasuredWidth(),0);  可以實現但是無動畫過渡
    }
}
private void startScrollNow(int endX){
    int startX = getScrollX();  // 起始X
    int startY = 0;  // 起始Y
    int dx = endX - startX;   // X方向的增量值,可以理解為距離
    int dy = 0;   // Y方向的增量值,可以理解為距離
    int time = Math.abs(dx) * 10;
    int durationTime = (time>600)?600:time;
    //startScroll(int startX, int startY, int dx, int dy, int duration)
    // 注意: startScroll 只是模擬數據的變化,想要看到效果還需要調用invalidate重新刷新UI,其實就是調用onDraw
    scroller.startScroll(startX, startY, dx, dy, durationTime);
    invalidate();  // 關鍵代碼,invalidate和computeScroll才會有動畫效果,scroller.startScroll只是模擬數據
}
@Override
public void computeScroll() { // 如果數據模擬沒有完成,那么繼續更新
    //super.computeScroll();
    // computeScrollOffset
    if(scroller.computeScrollOffset()){
        scrollTo(scroller.getCurrX(),0);
        invalidate(); // 注意這里還調用了invalidate,這樣才會產生效果
    }
}
彈性滑動.gif

六、4、添加是 否處于主頁的方法 和 展示那個頁面的方法

其實也就是添加了isAtMainPage、showMainPage()和showLeftPage()這三個方法


public class SlideMenu extends ViewGroup{
    private static final int MAIN_PAGE = 0;
    private static final int LEFT_PAGE = 1;
    private View mLeftMenu;
    private View mMainPage;
    private int downX;
    private Scroller scroller;
    private int pageIndex;
    public SlideMenu(Context context) {
        super(context);
    }
    public SlideMenu(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public SlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    // 當SlideView被xml引用加載之后完成,這個方法就會調用。
    /**
     * Finalize inflating a view from XML.  This is called as the last phase
     * of inflation, after all child views have been added.
     */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate(); // ???
        //  getChildAt 作用 Returns the view at the specified position in the group.
        //  精確地返回在ViewGroup里面的View的位置 所以我們的include順序很重要
        mLeftMenu = getChildAt(0); // 左側菜單
        mMainPage = getChildAt(1);
        scroller = new Scroller(getContext()); // 實例化一個Scroll,需要context
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // makeMeasureSpec 的時候,第一個參數是大小,第二個參數是模式
        // 寬度我們使用左側菜單的在xml里面的200dp,因為指定大小所以模式是MeasureSpec.EXACTLY
        // 怎么得到一個View在xml布局文件里面寬,利用view.getLayoutParams().width,高類似
        int leftViewMeasureSpecWidth = MeasureSpec.
                makeMeasureSpec(mLeftMenu.getLayoutParams().width, MeasureSpec.EXACTLY);
        //左側菜單的的高度我們希望填充父窗體,而當前onMeasure里面的heightMeasureSpec根據我們的布局顯然就是填充父窗體
        // 所以一直接用父親傳過來的這個32位參數就好
        mLeftMenu.measure(leftViewMeasureSpecWidth,heightMeasureSpec);
        // 至于主頁頁面,我們的希望他寬高都是填充父窗體,所以直接用onMeasure里面傳過來的參數就好啦
        mMainPage.measure(widthMeasureSpec, heightMeasureSpec);
        // onMeasure一開始什么都不管就應該復寫setMeasuredDimension,不然報錯。
        // 除非我們的自定義控件是寬高都是填充父窗體,那么我們就留著下面這句super的代碼就可以
        // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(widthMeasureSpec,heightMeasureSpec);
    }
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int leftViewWidth = mLeftMenu.getMeasuredWidth();
        int leftViewHeight = mLeftMenu.getMeasuredHeight();
        Log.d("Slide", "getWidth:" + mLeftMenu.getWidth() + "   mLeftMenu.getHeight():" + mLeftMenu.getHeight());
        Log.d("Slide", "getMeasuredWidth:" + mLeftMenu.getMeasuredWidth() + "   getMeasuredHeight:" + mLeftMenu.getMeasuredHeight());
        // l t r b  左上右下  左上一點,右下一點,兩點確定了一個矩形的大小
        mLeftMenu.layout(-leftViewWidth,0,0,leftViewHeight);
        int mainViewWidth = mMainPage.getMeasuredWidth();
        int mainViewHeight = mMainPage.getMeasuredHeight();
        mMainPage.layout(0, 0, mainViewWidth, mainViewHeight);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //return super.onTouchEvent(event);
        int action = event.getAction();
        switch (action){
            case MotionEvent.ACTION_DOWN:
                downX = (int)(event.getX()+0.5f);
                break;
            case MotionEvent.ACTION_MOVE:
                int moveX = (int)(event.getX()+0.5f);
                // 用減法,比如按下是0,最終移動到了20,那么屏幕向左邊移動20個單位,左側菜單就可顯示出來了
                int distanceX =  downX - moveX;
                int scrollX = getScrollX();  // 注意getScaleX()不要寫成getScaleX()
                // 解決越界問題
                if(scrollX+distanceX < (-mLeftMenu.getMeasuredWidth())){ // 左側越界臨界點
                    scrollTo(-mLeftMenu.getMeasuredWidth(),0);
                }else if(scrollX+distanceX>0){ // 右側越界臨界點
                    scrollTo(0,0);
                }else{  // 在兩個臨界點之間的可移動范圍
                    scrollBy(distanceX,0);
                }
                downX = moveX;
                break;
            case MotionEvent.ACTION_UP:
                int upScrollX = getScrollX();
                choosePage(upScrollX<-(mLeftMenu.getMeasuredWidth()/2));
                break;
        }
        //關鍵一步,返回true,代表消費當前的touch事件
        return true;
    }
    private void choosePage(boolean isMainPage){
        if(isMainPage){
            pageIndex = MAIN_PAGE;
            startScrollNow(-mLeftMenu.getMeasuredWidth());
            //scrollTo(0,0); 可以實現但是無動畫過渡
        }else{
            pageIndex = LEFT_PAGE;
            startScrollNow(0);
            //scrollTo(-mLeftMenu.getMeasuredWidth(),0);  可以實現但是無動畫過渡
        }
    }
    private void startScrollNow(int endX){
        int startX = getScrollX();  // 起始X
        int startY = 0;  // 起始Y
        int dx = endX - startX;   // X方向的增量值,可以理解為距離
        int dy = 0;   // Y方向的增量值,可以理解為距離
        int time = Math.abs(dx) * 10;
        int durationTime = (time>600)?600:time;
        //startScroll(int startX, int startY, int dx, int dy, int duration)
        // 注意: startScroll 只是模擬數據的變化,想要看到效果還需要調用invalidate重新刷新UI,其實就是調用onDraw
        scroller.startScroll(startX, startY, dx, dy, durationTime);
        invalidate();  // 關鍵代碼,invalidate和computeScroll才會有動畫效果,scroller.startScroll只是模擬數據
    }
    @Override
    public void computeScroll() { // 如果數據模擬沒有完成,那么繼續更新
        //super.computeScroll();
        // computeScrollOffset
        if(scroller.computeScrollOffset()){
            scrollTo(scroller.getCurrX(),0);
            invalidate(); // 注意這里還調用了invalidate,這樣才會產生效果
        }
    }
    public boolean isAtMainPage(){
        return (pageIndex == MAIN_PAGE)?true:false;
    }
    public void showMainPage(){
        choosePage(true);
    }
    public void showLeftPage(){
        choosePage(false);
    }
}
頁面判斷.gif

六、5、我們發現當我們滑動左側菜單的時候,無法拉動菜單。

這就涉及到一個View的傳遞機制了。

關于View的傳遞機制,可以參考文章 ——————————

為什么拉動左側的菜單無法拖動,這肯定是屬于SlideMenu的左側菜單在拉動的時候,SlideMenu的onTouchEvent沒有被執行,為了確保我們的SlideMenu不管是在左側菜單還是主頁菜單的都能夠順利拖動,我們就在SlideMenu里面復寫 onInterceptTouchEvent 方法,然后判斷一下,如果是橫向滑動,就攔截下來,自己就消費掉這個touch

其實也就只是添加這么一小段代碼

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    int action = ev.getAction();
    switch (action){
        case MotionEvent.ACTION_DOWN:
            interDownX = (int)(ev.getX()+0.5f);
            interDownY = (int)(ev.getY()+0.5f);
            break;
        case MotionEvent.ACTION_MOVE:
            interMoveX = (int)(ev.getX()+0.5f);
            interMoveY = (int)(ev.getY()+0.5f);
            int diatanceInterX = Math.abs(interMoveX - interDownX);
            int distanceInterY = Math.abs(interMoveY - interDownY);
            if(diatanceInterX > distanceInterY){ // 代表是水平滑動,(水平滑動的距離比垂直滑動的距離大)
                return true; // 攔截之后就肯定執行onTouchEvent
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
    }
    return super.onInterceptTouchEvent(ev); // 如果不是水平滑動就不攔截
}
完成.gif

好啦,該做的差不多都做啦。

最后附上所有代碼:
SlideMenu完整代碼

public class SlideMenu extends ViewGroup{


    private static final int MAIN_PAGE = 0;
    private static final int LEFT_PAGE = 1;
    private View mLeftMenu;
    private View mMainPage;
    private int downX;
    private Scroller scroller;
    private int pageIndex;
    private int interDownX;
    private int interDownY;
    private int interMoveX;
    private int interMoveY;

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

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

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


    // 當SlideView被xml引用加載之后完成,這個方法就會調用。
    /**
     * Finalize inflating a view from XML.  This is called as the last phase
     * of inflation, after all child views have been added.
     */

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate(); // ???

        //  getChildAt 作用 Returns the view at the specified position in the group.
        //  精確地返回在ViewGroup里面的View的位置 所以我們的include順序很重要
        mLeftMenu = getChildAt(0); // 左側菜單
        mMainPage = getChildAt(1);
        scroller = new Scroller(getContext()); // 實例化一個Scroll,需要context

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // makeMeasureSpec 的時候,第一個參數是大小,第二個參數是模式
        // 寬度我們使用左側菜單的在xml里面的200dp,因為指定大小所以模式是MeasureSpec.EXACTLY

        // 怎么得到一個View在xml布局文件里面寬,利用view.getLayoutParams().width,高類似
        int leftViewMeasureSpecWidth = MeasureSpec.
                makeMeasureSpec(mLeftMenu.getLayoutParams().width, MeasureSpec.EXACTLY);

        //左側菜單的的高度我們希望填充父窗體,而當前onMeasure里面的heightMeasureSpec根據我們的布局顯然就是填充父窗體
        // 所以一直接用父親傳過來的這個32位參數就好
        mLeftMenu.measure(leftViewMeasureSpecWidth,heightMeasureSpec);

        // 至于主頁頁面,我們的希望他寬高都是填充父窗體,所以直接用onMeasure里面傳過來的參數就好啦
        mMainPage.measure(widthMeasureSpec, heightMeasureSpec);

        // onMeasure一開始什么都不管就應該復寫setMeasuredDimension,不然報錯。
        // 除非我們的自定義控件是寬高都是填充父窗體,那么我們就留著下面這句super的代碼就可以
        // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(widthMeasureSpec,heightMeasureSpec);

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int leftViewWidth = mLeftMenu.getMeasuredWidth();
        int leftViewHeight = mLeftMenu.getMeasuredHeight();

        Log.d("Slide", "getWidth:" + mLeftMenu.getWidth() + "   mLeftMenu.getHeight():" + mLeftMenu.getHeight());
        Log.d("Slide", "getMeasuredWidth:" + mLeftMenu.getMeasuredWidth() + "   getMeasuredHeight:" + mLeftMenu.getMeasuredHeight());

        // l t r b  左上右下  左上一點,右下一點,兩點確定了一個矩形的大小
        mLeftMenu.layout(-leftViewWidth,0,0,leftViewHeight);


        int mainViewWidth = mMainPage.getMeasuredWidth();
        int mainViewHeight = mMainPage.getMeasuredHeight();

        mMainPage.layout(0, 0, mainViewWidth, mainViewHeight);

    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        int action = ev.getAction();
        switch (action){
            case MotionEvent.ACTION_DOWN:
                interDownX = (int)(ev.getX()+0.5f);
                interDownY = (int)(ev.getY()+0.5f);

                break;
            case MotionEvent.ACTION_MOVE:
                interMoveX = (int)(ev.getX()+0.5f);
                interMoveY = (int)(ev.getY()+0.5f);

                int diatanceInterX = Math.abs(interMoveX - interDownX);
                int distanceInterY = Math.abs(interMoveY - interDownY);

                if(diatanceInterX > distanceInterY){ // 代表是水平滑動,(水平滑動的距離比垂直滑動的距離大)
                    return true; // 攔截之后就肯定執行onTouchEvent
                }
                break;

            case MotionEvent.ACTION_UP:

                break;

        }
        return super.onInterceptTouchEvent(ev); // 如果不是水平滑動就不攔截
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //return super.onTouchEvent(event);

        int action = event.getAction();
        switch (action){
            case MotionEvent.ACTION_DOWN:

                downX = (int)(event.getX()+0.5f);

                break;
            case MotionEvent.ACTION_MOVE:
                int moveX = (int)(event.getX()+0.5f);

                // 用減法,比如按下是0,最終移動到了20,那么屏幕向左邊移動20個單位,左側菜單就可顯示出來了
                int distanceX =  downX - moveX;

                int scrollX = getScrollX();  // 注意getScaleX()不要寫成getScaleX()

                // 解決越界問題
                if(scrollX+distanceX < (-mLeftMenu.getMeasuredWidth())){ // 左側越界臨界點
                    scrollTo(-mLeftMenu.getMeasuredWidth(),0);
                }else if(scrollX+distanceX>0){ // 右側越界臨界點
                    scrollTo(0,0);
                }else{  // 在兩個臨界點之間的可移動范圍
                    scrollBy(distanceX,0);
                }

                downX = moveX;
                break;

            case MotionEvent.ACTION_UP:
                int upScrollX = getScrollX();
                choosePage(upScrollX<-(mLeftMenu.getMeasuredWidth()/2));
                break;
        }
        //關鍵一步,返回true,代表消費當前的touch事件
        return true;

    }


    private void choosePage(boolean isMainPage){
        if(isMainPage){
            pageIndex = MAIN_PAGE;
            startScrollNow(-mLeftMenu.getMeasuredWidth());
            //scrollTo(0,0); 可以實現但是無動畫過渡
        }else{
            pageIndex = LEFT_PAGE;
            startScrollNow(0);
            //scrollTo(-mLeftMenu.getMeasuredWidth(),0);  可以實現但是無動畫過渡
        }


    }

    private void startScrollNow(int endX){
        int startX = getScrollX();  // 起始X
        int startY = 0;  // 起始Y
        int dx = endX - startX;   // X方向的增量值,可以理解為距離
        int dy = 0;   // Y方向的增量值,可以理解為距離

        int time = Math.abs(dx) * 10;
        int durationTime = (time>600)?600:time;

        //startScroll(int startX, int startY, int dx, int dy, int duration)
        // 注意: startScroll 只是模擬數據的變化,想要看到效果還需要調用invalidate重新刷新UI,其實就是調用onDraw
        scroller.startScroll(startX, startY, dx, dy, durationTime);
        invalidate();  // 關鍵代碼,invalidate和computeScroll才會有動畫效果,scroller.startScroll只是模擬數據
    }


    @Override
    public void computeScroll() { // 如果數據模擬沒有完成,那么繼續更新
        //super.computeScroll();

        // computeScrollOffset
        if(scroller.computeScrollOffset()){
            scrollTo(scroller.getCurrX(),0);
            invalidate(); // 注意這里還調用了invalidate,這樣才會產生效果
        }

    }



    public boolean isAtMainPage(){
        return (pageIndex == MAIN_PAGE)?true:false;
    }

    public void showMainPage(){
        choosePage(true);
    }
    public void showLeftPage(){
        choosePage(false);
    }

}

.
.
MainActivity

public class MainActivity extends Activity {

    private SlideMenu slideMenu;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        slideMenu = (SlideMenu) findViewById(R.id.mSmenu);

        findViewById(R.id.mIvBack).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(slideMenu.isAtMainPage()){
                    slideMenu.showLeftPage();
                }else{
                    slideMenu.showMainPage();
                }
            }
        });


        findViewById(R.id.mTvBd).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this,"本地",Toast.LENGTH_SHORT).show();
                if(slideMenu.isAtMainPage()){
                    slideMenu.showLeftPage();
                }else{
                    slideMenu.showMainPage();
                }
            }
        });


    }
}

組合控件仿qq側滑菜單至此結束。

組合控件1—— 設置框一文中,我們將進行組合控件的demo編寫。

本篇完。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 一、Android開發初體驗 二、Android與MVC設計模式模型對象存儲著應用的數據和業務邏輯。模型類通常用來...
    為夢想戰斗閱讀 936評論 0 3
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,372評論 25 708
  • 什么是View View 是 Android 中所有控件的基類。 View的位置參數 View 的位置由它的四個頂...
    acc8226閱讀 1,216評論 0 7
  • 20170505之察己 1、主題是什么? -- 誰在學習 2、事件是什么? -- 臨在讀書會 3、即時反應是什么?...
    tianyouyou9閱讀 320評論 0 0
  • 最近熱播的我的前半生 ,子君從一個富家太太一下子跌落到無依無靠 一切都要靠自己的普通打工者。這部劇很真實,真實...
    怡寧_f566閱讀 276評論 7 2