1. 程式人生 > >Android 滑動詳解-思維導圖版

Android 滑動詳解-思維導圖版

滑動

版本:2018/05/12-1(11:11)

滑動-思維導圖

基礎知識

View

1、什麼是View

  1. View是所有控制元件的基類
  2. View有一個特殊子類ViewGroup,ViewGroup能包含一組View,但ViewGroup的本身也是View。
  3. 由於View和ViewGourp的存在,意味著View可以是單個控制元件也可以是一組控制元件。這種結構形成了View樹。

2、View的位置引數:top,left,right,bottom

  1. top-左上角的y軸座標(全部是相對座標,相對於父容器)
  2. left-左上角的x軸座標
  3. right-右下角的x軸座標
  4. bottom-右下角的y軸座標
  5. 在View中獲取這些成員變數的方法,是getLeft(),getRight(),getTop(),getBottom()即可

3、View從3.0開始新增的引數:x,y,translationX,translationY

  1. x,y是View當前左上角的座標
  2. translationX,translationY是在滑動/動畫後,View當前位置和View最原始位置的距離。
  3. 因此得出等式:x(View左上角當前位置) = left(View左上角初始位置) + translationX(View左上角偏移的距離)
  4. View平移時top、left等引數不變,改變的是x,y,tranlsationX和tranlsationY

座標系

4、Android座標系

  1. Android座標系以螢幕左上角為原點,向右X軸為正半軸,向下Y軸為正半軸
  2. 觸控事件中getRawX()和getRawY()獲得的就是Android座標系的座標
  3. Android中通過getLocationOnScreen(intlocation[])能獲得當前檢視的左上

5、View座標系

  1. View座標系是以當前檢視的父檢視的左上角作為原點建立的座標系,方向和Android座標系一致
  2. 觸控事件中getX()和getY()獲得的就是檢視座標系中的座標

MotionEvent

6、MotionEvent的作用

  1. MotionEvent用於記錄移動事件
  2. 包括滑鼠、手機、traceball、pen的移動事件。

7、MotionEvent包含的手指觸控事件

  1. ACTION_DOWN\MOVE\UP對應三個觸控事件。
  2. getX/getY能獲得觸控點的座標,相當於當前View左上角的(x,y)
  3. getRawX/getRawY,獲得觸控點相當於手機左上角的(x,y)座標

滑動的7種實現方法

8、View滑動的7種方法:

  1. layout:對View進行重新佈局定位。在onTouchEvent()方法中獲得控制元件滑動前後的偏移。然後通過layout方法重新設定。
  2. offsetLeftAndRight和offsetTopAndBottom:系統提供上下/左右同時偏移的API。onTouchEvent()中呼叫
  3. LayoutParams: 更改自身佈局引數
  4. scrollTo/scrollBy: 本質是移動View的內容,需要通過父容器的該方法來滑動當前View
  5. Scroller: 平滑滑動,通過過載computeScroll(),使用scrollTo/scrollBy完成滑動效果。
  6. 屬性動畫: 動畫對View進行滑動
  7. ViewDragHelper: 谷歌提供的輔助類,用於完成各種拖拽效果。

15、Layout實現滑動

/*================================*
* onTouchEvent-進行偏移計算,之後呼叫layout
*================================*/
 public boolean onTouchEvent(MotionEvent event) {
     float curX = event.getX(); //手指實時位置的X
     float curY = event.getY(); //Y
     switch(event.getAction()){
        case MotionEvent.ACTION_MOVE:
           int offsetX = (int)(curX - downX); //X偏移
           int offsetY = (int)(curY - downY); //Y偏移
    /**=============================================
     * 變化後的距離=getLeft(當前控制元件距離父控制元件左邊的距離)+偏移量——呼叫layout重新佈局
     *============================================*/
           layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
           break;
        case MotionEvent.ACTION_DOWN:
           downX = curX; //按下時的座標
           downY = curY;
           break;
     }
     return true;
 }

16、offsetLeftAndRight和offsetTopAndBottom實現滑動

/*================================*
* onTouchEvent-進行偏移計算,直接呼叫
*================================*/
 public boolean onTouchEvent(MotionEvent event) {
     float curX = event.getX(); //手指實時位置的X
     float curY = event.getY(); //Y
     switch(event.getAction()){
        case MotionEvent.ACTION_MOVE:
           int offsetX = (int)(curX - downX); //X偏移
           int offsetY = (int)(curY - downY); //Y偏移
     /**=============================================
      * 對left和right, top和bottom同時偏移
      *============================================*/
           offsetLeftAndRight(offsetX);
           offsetTopAndBottom(offsetY);
           break;
        case MotionEvent.ACTION_DOWN:
           downX = curX; //按下時的座標
           downY = curY;
           break;
     }
     return true;
 }

17、LayoutParams實現滑動:

  1. 通過父控制元件設定View在父控制元件的位置,但需要指定父佈局的型別,不好
  2. 用ViewGroup的MariginLayoutParams的方法去設定margin
//方法一:通過佈局設定在父控制元件的位置。但是必須要有父控制元件, 而且要指定父佈局的型別,不好的方法。
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);

/**===============================================
 * 方法二:用ViewGroup的MarginLayoutParams的方法去設定marign
 * 優點:相比於上面方法, 就不需要知道父佈局的型別。
 * 缺點:滑動到右側控制元件會縮小
 *===============================================*/
ViewGroup.MarginLayoutParams mlayoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
mlayoutParams.leftMargin = getLeft() + offsetX;
mlayoutParams.topMargin = getTop() + offsetY;
setLayoutParams(mlayoutParams);

18、scrollTo\scrollBy實現滑動

  1. 都是View提供的方法。
  2. scrollTo-直接到新的x,y座標處。
  3. scrollBy-基於當前位置的相對滑動。
  4. scrollBy-內部是呼叫scrollTo.
  5. scrollTo\scrollBy, 效果是移動View的內容,因此需要在View的父控制元件中呼叫。
// 1、移動到目標位置
((View)getParent()).scrollTo(dstX, dstY);
// 2、相對滑動:且scrollBy是父容器進行滑動,因此偏移量需要取負
((View)getParent()).scrollBy(-offsetX, -offsetY);

19、scrollTo/By內部的mScrollX和mScrollY的意義

  1. mScrollX的值,相當於手機螢幕相對於View左邊緣向右移動的距離,手機螢幕向右移動時,mScrollX的值為正;手機螢幕向左移動(等價於View向右移動),mScrollX的值為負。
  2. mScrollY和X的情況相似,手機螢幕向下移動,mScrollY為+正值;手機螢幕向上移動,mScrollY為-負值。
  3. mScrollX/Y是根據第一次滑動前的位置來獲得的,例如:第一次向左滑動200(等於手機螢幕向右滑動200),mScrollX = 200;第二次向右滑動50, mScrollX = 200 + (-50)= 150,而不是(-50)。

20、動畫實現滑動的方法

  1. 可以通過傳統動畫或者屬性動畫的方式實現
  2. 傳統動畫需要通過設定fillAfter為true來保留動畫後的狀態(但是無法在動畫後的位置進行點選操作,這方面還是屬性動畫好)
  3. 屬性動畫會保留動畫後的狀態,能夠點選。

21、ViewDragHelper

  1. 通過ViewDragHelper去自定義ViewGroup讓其子View具有滑動效果。

彈性滑動

Scroller

1、Scroller的作用

  1. 用於封裝滑動
  2. 提供了基於時間的滑動偏移值,但是實際滑動需要我們去負責。

1、Scroller的要點

  1. 呼叫startScroll方法時,Scroller只是單純的儲存引數
  2. 之後的invalidate方法導致的View重繪
  3. View重繪之後draw方法會呼叫自己實現的computeScroll(),才真正實現了滑動

1、Scroller的使用

// 1、初始化
Scroller mScroller = new Scroller(getContext());

// 2、重寫View的方法computeScroll
public void computeScroll() {
        super.computeScroll();
        //判斷scroller是否執行完畢。
        if(mScroller.computeScrollOffset()){
            ((View)getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            //通過重繪來不斷呼叫 computeScroll
            invalidate();
        }
}

// 3、開始滑動
case MotionEvent.ACTION_UP:
      View viewGroup = (View) getParent();
      mScroller.startScroll(viewGroup.getScrollX(), viewGroup.getScrollY(),
                        -viewGroup.getScrollX(), -viewGroup.getScrollY());
      invalidate();
       break;

1、Scroller工作原理

  1. Scroller本身不能實現View的滑動,需要配合View的computeScroll方法實現彈性滑動
  2. 不斷讓View重繪,每一次重繪距離滑動的開始時間有一個時間間隔,通過該時間可以得到View當前的滑動距離
  3. View的每次重繪都會導致View的小幅滑動,多次小幅滑動就組成了彈性滑動

動畫

4、通過動畫實現彈性滑動

延時策略

5、通過延時策略實現彈性滑動。

  1. 通過handler、View的postDelayed、或者執行緒的sleep方法。
  2. 實現思路:例如將View滑動100畫素,通過Handler可以每100ms傳送一次訊息讓其滑動10畫素,最終會在1000ms內滑動100畫素。

側滑選單

DraweLayout

1、DrawerLayout是什麼?

  1. Google推出的側滑選單

2、DrawerLayout的使用

  1. 側滑選單的佈局需要用layout_gravity屬性指定。
  2. 主體View的佈局中寬高需要為match_parent不能有layout_gravity屬性
//佈局檔案
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
    android:id="@+id/md_drawerlayout"
    xxx>
    <Button
        android:id="@+id/md_slidemenu_text"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        xxx
        android:layout_gravity="start"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        xxx主體xxx
    </LinearLayout>
</android.support.v4.widget.DrawerLayout>
DrawerLayout drawerLayout = findViewById(R.id.md_drawerlayout);
Button button = findViewById(R.id.md_slidemenu_text);
button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                drawerLayout.closeDrawer(button); //關閉側滑選單
            }
        });

3、DrawerLayout中android:layout_gravity屬性

  1. left/start:選單位於左側
  2. top/bottom:選單位於右側

4、DrawerLayout的方法

1-開啟

drawerLayout.openDrawer(button);

2-關閉

drawerLayout.closeDrawer(button);

3-設定監聽器(DrawerListener)

drawerLayout.setDrawerListener(new DrawerLayout.DrawerListener() {
  //滑動時
    @Override
    public void onDrawerSlide(View drawerView, float slideOffset) {

    }
//開啟時
    @Override
    public void onDrawerOpened(View drawerView) {

    }
//關閉時
    @Override
    public void onDrawerClosed(View drawerView) {

    }
//狀態改變時:{@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
    @Override
    public void onDrawerStateChanged(int newState) {

    }
});

4-設定監聽器(SimpleDrawerListener)

//可以選擇性實現其中的部分回撥介面
drawerLayout.setDrawerListener(new DrawerLayout.SimpleDrawerListener() {
    @Override
    public void onDrawerSlide(View drawerView, float slideOffset) {
        super.onDrawerSlide(drawerView, slideOffset);
    }
});

SlidingPanelLayout

1、SlidingPaneLayout是什麼

  1. 提供一種類似於DrawerLayout的側滑選單效果,“效果並不好”
  2. xml佈局中第一個ChildView就是左側選單的內容,第二個ChildView就是主體內容

2、SlidingPaneLayout的使用

<android.support.v4.widget.SlidingPaneLayout
    android:id="@+id/md_slidingpanelayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/md_slidemenu_text"
        android:layout_width="150dp"
        android:layout_height="match_parent"
        xxx左側內容xxx/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        xxx主體內容xxx
    </LinearLayout>

</android.support.v4.widget.SlidingPaneLayout>
SlidingPaneLayout slidingPaneLayout = findViewById(R.id.md_slidingpanelayout);
Button button = findViewById(R.id.md_slidemenu_text);

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
      //關閉Pane
        slidingPaneLayout.closePane();
    }
});

3、SlidingPaneLayout的方法

// 1. 開啟Pane
slidingPaneLayout.openPane();
// 2. 關閉Pane
slidingPaneLayout.closePane();
// 3. 右側主體頁面縮排去的陰影漸變色
slidingPaneLayout.setSliderFadeColor(Color.BLUE);
// 4. 左側面板縮排去的陰影漸變色
slidingPaneLayout.setCoveredFadeColor(Color.GRAY);

// 5. 監聽器
slidingPaneLayout.setPanelSlideListener(new SlidingPaneLayout.PanelSlideListener() {
    /**
     * 左側面板在滑動
     * @param panel 被移走的主體View
     * @param slideOffset 滑動的百分比(0~1)
     */
    @Override
    public void onPanelSlide(View panel, float slideOffset) {

    }
    //左側Pane已經開啟
    @Override
    public void onPanelOpened(View panel) {

    }
    //左側Pane已經關閉
    @Override
    public void onPanelClosed(View panel) {

    }
});

NavigationView的作用

  1. 配合DrawerLayout使用用於實現其中的左側選單效果
  2. Google在5.0之後推出NavigationView,
  3. 左側選單效果整體上分為兩部分,上面一部分叫做HeaderLayout,下面的那些點選項都是menu

ViewDragHelper

1、ViewDragHelper的作用

  1. 用於編寫自定義ViewGroup工具類
  2. 位於android.support.v4.widget.
  3. 提供一系列操作和狀態追蹤用於幫助使用者進行拖拽和定位子View

2、ViewDragHelper的簡單例項

實現ChildView可以自由拖拽的ViewGroup
1. 建立ViewDragHelper
2. 將ViewGroup的事件處理交給ViewDragHelper
3. 自定義ViewDragHelper.Callback實現一些觸控回撥,用於實現效果。

public class ScrollViewGroup extends LinearLayout{
    private ViewDragHelper mViewDragHelper;

    public ScrollViewGroup(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        /**==============================================
         * 1、建立ViewDragHelper
         *===========================================*/
        mViewDragHelper = ViewDragHelper.create(
                this,  //ViewGroup
                1f,     //設定touchSlop-sensitivity越大,touchslop越小
                new MyViewDragHelperCallback()); //使用者觸控事件的回撥
    }

    /**==============================================
     * 2、ViewGroup的事件處理都交給ViewDragHelper
     *===========================================*/
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {\
        //轉交中斷處理權
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //交由處理事件,且返回true表示處理後續事件。
        mViewDragHelper.processTouchEvent(event);
        return true;
    }

    /**==============================================
     * 3、ViewDragHelper.Callback
     *===========================================*/
    class MyViewDragHelperCallback extends ViewDragHelper.Callback{
        /**
         * 3.1-決定哪些View可以捕獲
         * @return true-捕獲該child; false-不處理
         */
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return true;
        }
        /**
         * 3.2-控制Child在水平方向上的邊界
         * @return 範圍限制後的新left(當前child的left)
         */
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            // left範圍為 leftPadding ~ (getWidth() - getPaddingRight() - child.getWidth())
            final int leftMinBound = getPaddingLeft();
            final int leftMaxBound = getWidth() - getPaddingRight() - child.getWidth();
            final int newLeft = Math.min(Math.max(left, leftMinBound), leftMaxBound);
            return newLeft;
        }
        /**
         * 3.3-控制Child在垂直方向上的邊界
         * @return 範圍限制後的新top
         */
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            final int topMinBound = getPaddingTop();
            final int topMaxBound = getHeight() - getPaddingBottom() - child.getHeight();
            final int newLeft = Math.min(Math.max(top, topMinBound), topMaxBound);
            return newLeft;
        }
    }
}

3、ChilView為Button或者clickable = true時無法拖動的解決辦法

  1. 正常流程: 如果子View不消耗事件,那麼整個手勢(DOWN-MOVE-UP)都是直接進入onTouchEvent,在onTouchEvent的DOWN的時候就確定了captureView。
  2. 子View消耗事件:會先走onInterceptTouchEvent方法,判斷是否可以捕獲,而在判斷的過程中會去判斷另外兩個回撥的方法:getViewHorizontalDragRange和getViewVerticalDragRange,只有這兩個方法返回大於0的值才能正常的捕獲。
/**
 * 返回子View水平滑動範圍。
 * return 0: 則該ChildView不會滑動。
 */
@Override
public int getViewHorizontalDragRange(View child)
{
    return getMeasuredWidth()-child.getMeasuredWidth();
}

/**
 * 返回子View垂直滑動範圍。
 * return 0: 則該ChildView不會滑動。
 */
@Override
public int getViewVerticalDragRange(View child)
{
    return getMeasuredHeight()-child.getMeasuredHeight();
}

ViewDragHelper.Callback

1、ViewDragHelper.Callback的方法和作用

方法 作用
onViewDragStateChanged() 當ViewDragHelper狀態發生變化時回撥(IDLE,DRAGGING,SETTING-自動滾動時
onViewPositionChanged() ChildView位置改變時回撥
onViewCaptured() 捕獲ChildView時回撥
onViewReleased() 鬆開ChildView時回撥
onEdgeTouched() 當觸控到邊界時回撥
onEdgeLock() true的時候會鎖住當前的邊界,false則unLock。
onEdgeDragStarted() 邊緣拖拽開始時回撥
getOrderedChildIndex() 在同一個座標(x,y)下應該去獲取哪一個View。(mViewDragHelper.findTopChildUnder中需要用到)
getViewHorizontalDragRange() 獲取水平方向上的拖拽範圍
getViewVerticalDragRange() 獲取垂直方向上的拖拽範圍
tryCaptureView() 判斷是否捕獲當前View
clampViewPositionHorizontal() 控制Child在水平方向上的邊界
clampViewPositionVertical() 控制Child在垂直方向上的邊界
/**
 * ChildView不在被拖拽的時候呼叫。
 *
 * 1. 想要將ChilView 安置到某個位置, 需要呼叫{@link ViewDragHelper#settleCapturedViewAt(int, int)}
 * 2. 想要將ChilView fling到某個位置, 需要呼叫{@link ViewDragHelper#flingCapturedView(int, int, int, int)}
 *
 * 注意:
 * 1. 如果呼叫這些方法, ViewDragHelper會進入{@link ViewDragHelper#STATE_SETTLING}模式,
 *          此時直到View完全停止, View的捕獲都不會停止。
 * 2. 如果不呼叫這些方法,View會停止且ViewDragHelper會處於{@link ViewDragHelper#STATE_IDLE}模式
 *
 * {@link View#computeScroll()}
 *
 * @param xvel 手指離開螢幕時-X軸速度(畫素/秒)
 * @param yvel 手指離開螢幕時-Y軸速度(畫素/秒)
 */
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
    super.onViewReleased(releasedChild, xvel, yvel);

    if (releasedChild == mAutoBackView){
        mViewDragHelper.settleCapturedViewAt(mAuthoBackOriginPoint.x, mAuthoBackOriginPoint.y);
        invalidate();
    }
}

/**
 * 觸控到邊緣
 */
@Override
public void onEdgeTouched(int edgeFlags, int pointerId) {}

/**
 * true的時候會鎖住當前的邊界,false則unLock。
 */
@Override
public boolean onEdgeLock(int edgeFlags) {
    return false;
}

/**
 * 邊緣拖動的時候回撥。能繞過“tryCaptureView”
 *
 * @param edgeFlags A combination of edge flags describing the edge(s) dragged
 * @param pointerId ID of the pointer touching the described edge(s)
 * @see #EDGE_LEFT
 * @see #EDGE_TOP
 * @see #EDGE_RIGHT
 * @see #EDGE_BOTTOM
 */
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
    super.onEdgeDragStarted(edgeFlags, pointerId);
    //主動通過captureChildView進行捕獲
    mViewDragHelper.captureChildView(mEdgeDragView, pointerId);
}

/**
 * 決定ChildView的Z軸上的順序。(mViewDragHelper.findTopChildUnder(x, y)中需要獲取到座標(x,y)上最上層的子View)
 *
 * @param index 查詢的呼叫位置
 * @return 在呼叫位置上View的index
 */
@Override
public int getOrderedChildIndex(int index) {
    return index;
}

/**
 * 返回子View水平滑動範圍。
 * return 0: 則該ChildView不會滑動。
 */
@Override
public int getViewHorizontalDragRange(View child)
{
    return getMeasuredWidth()-child.getMeasuredWidth();
}

/**
 * 返回子View垂直滑動範圍。
 * return 0: 則該ChildView不會滑動。
 */
@Override
public int getViewVerticalDragRange(View child)
{
    return getMeasuredHeight()-child.getMeasuredHeight();
}

/**
 * 決定哪些View可以捕獲
 * @return true-捕獲該child; false-不處理
 */
@Override
public boolean tryCaptureView(View child, int pointerId) {
    if(child == mEdgeDragView) return false;
    return true;
}
/**
 * 控制Child在水平方向上的邊界
 * @return 範圍限制後的新left(當前child的left)
 */
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
    // left範圍為 leftPadding ~ (getWidth() - getPaddingRight() - child.getWidth())
    final int leftMinBound = getPaddingLeft();
    final int leftMaxBound = getWidth() - getPaddingRight() - child.getWidth();
    final int newLeft = Math.min(Math.max(left, leftMinBound), leftMaxBound);
    return newLeft;
}
/**
 * 控制Child在垂直方向上的邊界
 * @return 範圍限制後的新top
 */
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
    final int topMinBound = getPaddingTop();
    final int topMaxBound = getHeight() - getPaddingBottom() - child.getHeight();
    final int newLeft = Math.min(Math.max(top, topMinBound), topMaxBound);
    return newLeft;
}

@Override
public void onViewDragStateChanged(int state) {}

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

@Override
public void onViewCaptured(View capturedChild, int activePointerId) {}

2、shouldInterceptTouchEvent中方法回撥順序

DOWN:
    getOrderedChildIndex(findTopChildUnder)
    ->onEdgeTouched

MOVE:
    getOrderedChildIndex(findTopChildUnder)
    ->getViewHorizontalDragRange & getViewVerticalDragRange(checkTouchSlop)(MOVE中可能不止一次)
    ->clampViewPositionHorizontal & clampViewPositionVertical
    ->onEdgeDragStarted
    ->tryCaptureView
    ->onViewCaptured
    ->onViewDragStateChanged

3、processTouchEvent中方法回撥順序

DOWN:
    getOrderedChildIndex(findTopChildUnder)
    ->tryCaptureView
    ->onViewCaptured
    ->onViewDragStateChanged
    ->onEdgeTouched
MOVE:
    ->STATE==DRAGGING:dragTo
    ->STATE!=DRAGGING:
        onEdgeDragStarted
        ->getOrderedChildIndex(findTopChildUnder)
        ->getViewHorizontalDragRange & getViewVerticalDragRange(checkTouchSlop)
        ->tryCaptureView
        ->onViewCaptured
        ->onViewDragStateChanged

擴充套件例項

1、ViewDragHelper例項:拖拽返回、邊緣拖拽

2、ViewGroup如何去獲取子控制元件

View mNormalView;
View mAutoBackView;
View mEdgeDragView;

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

    mNormalView = getChildAt(0);
    mAutoBackView = getChildAt(1);
    mEdgeDragView = getChildAt(2);
}

3、ViewGroup如何去獲取某ChildView的初始座標

Point mAuthoBackOriginPoint = new Point();

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

    mAuthoBackOriginPoint.x = mAutoBackView.getLeft();
    mAuthoBackOriginPoint.y = mAutoBackView.getTop();
}

4、ViewGroup如何進行拖拽返回

1-Callback的onViewReleased

@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
    super.onViewReleased(releasedChild, xvel, yvel);
    //1、改變位置
    if (releasedChild == mAutoBackView){
        mViewDragHelper.settleCapturedViewAt(mAuthoBackOriginPoint.x, mAuthoBackOriginPoint.y);
        invalidate();
    }
}

2-內部是mScroller.startScroll因此需要computeScroll配合

@Override
public void computeScroll()
{
    if(mViewDragHelper.continueSettling(true))
    {
        invalidate();
    }
}

5、ViewDragHelper的邊緣拖動

/**
 * 遮蔽"目標控制元件"的滑動效果
 */
@Override
public boolean tryCaptureView(View child, int pointerId) {
    if(child == mEdgeDragView) return false;
    return true;
}
/**
 * 邊緣拖動的時候回撥。能繞過“tryCaptureView”
 */
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
    super.onEdgeDragStarted(edgeFlags, pointerId);
    //主動通過captureChildView進行捕獲
    mViewDragHelper.captureChildView(mEdgeDragView, pointerId);
}
// 設定邊緣追蹤
mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT | ViewDragHelper.EDGE_TOP);

QQ側滑選單

public class DragViewGroup extends FrameLayout {

    //側滑類
    private ViewDragHelper mViewDragHelper;
    private View mMenuView,mMainView;
    private int mWidth;

    public DragViewGroup(Context context) {
        super(context);
        initView();

    }

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

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

    /**-------------------------------------------
     * 1、初始化資料:呼叫ViewDragHelper.create方法
     * ------------------------------------------*/
    private void initView() {
        mViewDragHelper = ViewDragHelper.create(this,callback); //需要監聽的View和回撥callback
    }

    /**-------------------------------
     * 2、事件攔截和觸控事件全部交給ViewDragHelper進行處理
     * ------------------------------*/
    //事件攔截
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }
    //觸控事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //將觸控事件傳遞給ViewDragHelper

        mViewDragHelper.processTouchEvent(event);

        return true;
    }

    /**--------------------------------------------
     * 3、也需要重寫computeScroll()
     *    內部也是通過scroller來進行平移滑動, 這個模板可以照搬
     * -------------------------------------------*/
    @Override
    public void computeScroll() {
        if(mViewDragHelper.continueSettling(true)){
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    /**------------------------------
     * 4、處理的回撥:側滑回調
     * ----------------------------*/
    private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {

        /*-------------------------------
        * 何時開始觸控:
        *  1.指定哪一個子View可以被移動.
        *  2.如果直接返回true,在該佈局之內的所有子View都可以隨意划動
        * ------------------------------*/
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            //如果當前觸控的child是mMainView開始檢測
            return mMainView == child;
        }

        /*-------------------------------
        * 處理水平滑動:
        *  1. 返回值預設為0,如果為0則不處理該方向的滑動。
        *  2. 一般直接返回left,當需要精準計算pading等值時,可以先對left處理再返回
        * ------------------------------*/
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return left;
        }

        /*-------------------------------
        * 處理垂直滑動:
        *  1. 返回值預設為0,如果為0則不處理該方向的滑動。
        *  2. 一般直接返回top,,當需要精準計算pading等值時,可以先對left處理再返回
        * ------------------------------*/
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            return 0;
        }

        /*---------------------------------------------
        *  拖動結束後呼叫,類似ACTION_UP。
        *   這裡是實現側滑選單,一般滑動可以不用這段程式碼
        * ---------------------------------------------*/
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            //手指擡起後緩慢的移動到指定位置
            if(mMainView.getLeft() <500){
                //關閉選單
                mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }else{
                //開啟選單
                mViewDragHelper.smoothSlideViewTo(mMainView,300,0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }
        }
    };

    /**---------------------------------------------------
     * 5、獲取子控制元件用於處理
     *  1. 上面完成了滑動功能,這裡簡單的按照第1、2的順序指定子控制元件View的內容
     *  2. onSizeChanged能夠獲得menu等子控制元件的寬度等資訊,有需求可以後續處理
     * ----------------------------------------------*/
    //XML載入組建後回撥
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mMenuView = getChildAt(0);
        mMainView = getChildAt(1);
    }


    //元件大小改變時回撥
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = mMenuView.getMeasuredWidth();
    }
}

使用(作為父控制元件,裡面依次放menu和main):

    <com.example.xxxx.DragViewGroup
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/colorAccent"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/colorPrimary"/>

    </com.example.xxxx.DragViewGroup>

GestureDetector

1、GestureDetector作用和注意點

  1. 探測手勢事件,需要通過提供的MotionEvent
  2. 該類僅能用於touch觸控提供的MotionEvent,不能用於traceball events(追蹤球事件)
  3. 自定義View中可以重寫onTouchEvent()方法並在裡面用GestureDetector接管。

OnGestureListener

2、OnGestureListener作用

  1. 用於在手勢產生時,去通知監聽者。
  2. 監聽器會監聽所有的手勢,如果只需要監聽一部分可以使用SimpleOnGestureListener

3、OnGestureListener能監聽哪些手勢(5種)?

public interface OnGestureListener {
    /**
     * 1、按下操作。且其他任何事件之前都會觸發該方法。
     *   @param e Down MotionEvent
     */
    boolean onDown(MotionEvent e);

    /**
     * 2、按下之後,Move和Up之前。用於提供視覺反饋告訴使用者已經捕獲了他們的行為。
     *   @param e Down MotionEvent
     */
    void onShowPress(MotionEvent e);

    /**
     * 2、擡起操作。
     *   @param e Up MotionEvent
     */
    boolean onSingleTapUp(MotionEvent e);

    /**
     * 3、滑動操作(由Down MotionEvent e1觸發,當前是Move MotionEvent e2)
     *
     * @param e1 開啟滑動的按下操作。
     * @param e2 觸發onScroll的滑動操作。
     * @param distanceX 最近一次onScroll和當前onScroll之間的X滑動距離。
     * @param distanceY 最近一次onScroll和當前onScroll之間的Y滑動距離。
     */
    boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);

    /**
     * 4、長按操作。
     *
     * @param e 開始長按的Down操作.
     */
    void onLongPress(MotionEvent e);

    /**
     * 5、猛扔操作。
     *
     * @param e1 開始fling操作的Down MotionEvent
     * @param e2 觸發onFling的Move MotionEvent
     * @param velocityX X軸的速度(pixels / second  畫素/每秒)
     * @param velocityY Y軸的速度(pixels / second  畫素/每秒)
     */
    boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
}

上面所有有返回值的方法,return true-消耗該事件;return false-不消耗該事件

4、OnGestureListener的使用方法。

/**====================================================
 * 1、GestureDetector通過context和onGestureListener構造
 *======================================================*/
GestureDetector gestureDetector = new GestureDetector(getApplicationContext(), new GestureDetector.OnGestureListener() {
    //實現5種回撥方法
});
/**======================================
 * 2、在View的touch方法中進行攔截。
 *=======================================*/
imageView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        //交給GestureDetector進行處理
        return gestureDetector.onTouchEvent(event);
    }
});

OnDoubleTapListener

5、OnDoubleTapListener作用

  1. 監聽“雙擊操作”
  2. 監聽“確認的單擊操作”—該單擊操作之後的操作無法構成一次雙擊。

6、OnDoubleTapListener能監聽哪些手勢(3種)?

/**
 * 雙擊或者單擊(後續操作無法導致雙擊)
 */
public interface OnDoubleTapListener {
    /**
     * 1、單擊操作。 不會產生雙擊行為的單擊操作才會觸發。
     *
     * @param e Down MotionEvent
     * @return true-消耗事件; false-不消耗事件。
     */
    boolean onSingleTapConfirmed(MotionEvent e);

    /**
     * 2、雙擊操作.
     *
     * @param e 雙擊操作的第一個按下操作。
     * @return true-消耗事件; false-不消耗事件。
     */
    boolean onDoubleTap(MotionEvent e);

    /**
     * 3、雙擊操作之間發生了down、move或者up事件。
     *
     * @param e 雙擊操作期間產生的MotionEvent
     * @return true-消耗事件; false-不消耗事件。
     */
    boolean onDoubleTapEvent(MotionEvent e);
}

7、OnDoubleTapListener的使用方法

GestureDetector gestureDetector = new GestureDetector(...);
gestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
    // 三種