Android 的滑动分析以及各类实现


1、滑动效果的产生

滑动一个View,本质区别就是移动一个View。改变当前View所在的坐标,原理和动画类似不断改变坐标位置实现。实现View的滑动就必须监听滑动的事件,而且根据事件传入的坐标,动态且不断改变View的坐标,从而实现View跟随用户触摸的滑动而滑动。android

(1)、Android的坐标系

Android中将屏幕最左上角的顶点做为Android坐标系的原点,从这个点向右是X轴正方向,从这个点向下是Y轴正方向,以下图:bash


系统提供了getLocationOnScreen(int location[])这样的方法来获取Android坐标系中点的位置,即该视图左上角在Android坐标系中的坐标。在触控事件中使用getRawX()、getRawY()方法所得到的坐标一样是Android坐标系中的坐标。ide

(2)、视图坐标系

Android中除了上面所说的这种坐标系以外,还有一个视图坐标系,它描述了子视图在父视图中的位置关系。这两种坐标系并不矛盾也不复杂,他们的做用是相互相成的。与Android坐标系相似,视图坐标系一样是以原点向右为X轴正方向,以原点向下为Y轴正方向,只不过在视图坐标系中,原点再也不是Android坐标系中的屏幕最左上角,而是以父视图左上角为坐标原点,以下图:布局


在触控事件中,经过getX()、getY()所得到的坐标系就是视图坐标系中的坐标。post

(3)、触控事件——MotionEvent动画

触控事件MotionEvent在用户交互中,占着举足轻重的地位。首先看看MotionEvent封装的一些经常使用事件常量,定义了触控事件的不一样类型。ui

//单点触摸按下动做
public static final int ACTION_DOWN             = 0;

//单点触摸离开动做
public static final int ACTION_UP               = 1;

//触摸点移动动做
public static final int ACTION_MOVE             = 2;

//触摸动做取消
public static final int ACTION_CANCEL           = 3;

//触摸动做超出边界
public static final int ACTION_OUTSIDE          = 4;

//多点触摸按下动做
public static final int ACTION_POINTER_DOWN     = 5;

//多点离开动做
public static final int ACTION_POINTER_UP       = 6;复制代码

一般状况会在onTouchEvent(MotionEvent event)方法中经过event.getAction()方法来获取触控事件的类型,并使用switch-case方法来进行筛选,这个代码的模式基本固定:this

@Override
public boolean onTouchEvent(MotionEvent event) {
    //获取当前输入点的X、Y坐标(视图坐标)
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //处理按下事件
            break;
        case MotionEvent.ACTION_MOVE:
            //处理移动事件
            break;
        case MotionEvent.ACTION_UP:
            //处理离开事件
            break;
    }
    return true;
}复制代码

在不涉及多点操做的状况下,一般可使用以上代码来完成触控事件的监听。spa

在Android中系统提供了很是多的方法来获取坐标值、相对距离等。方法丰富当然好,下面对坐标系的API进行总结,以下图:.net


这些方法能够分为以下两个类别:

  • View提供的获取坐标方法
    • getTop():获取到的是View自身的顶边到其父布局顶边的距离。
    • getLeft():获取到的是View自身的左边到其父布局最左边的距离。
    • getRight():获取到的是View自身的右边到其父布局左边的距离。
    • getBottom():获取到的是View自身的底边到其父布局顶边的距离。
  • MotionEvent提供的方法
    • getX():获取点击事件距离空间左边的距离,即视图坐标。
    • getY():获取点击事件距离控件顶边的距离,即视图坐标。
    • getRawX():获取点击事件距离整个屏幕左边的距离,即绝对坐标。
    • getRawY():获取点击事件距离整个屏幕顶边的距离,即绝对坐标。

2、实现滑动的七种方式

当了解Android坐标系和触控事件后,咱们再来看看如何使用系统提供的API来实现动态地修改一个View坐标,即实时滑动效果。而无论采用哪种方式,其实现的思想基本是一致的,当触摸View时,系统记下当前触摸点坐标,当手指移动时,系统记下移动后的触摸点坐标,从而获取到相对于前一次坐标点的偏移量,并经过偏移量来修改View的坐标,这样不断重复,实现滑动过程。

经过一个实例看看Android中该如何实现滑动效果,定义一个View,处于LinearLayout中,实现一个简单布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    
    <com.xjf.drawview.DragView1
        android:layout_width="100dp"
        android:layout_height="100dp" />

</LinearLayout>复制代码

咱们的目的就是让这个自定义的View随着手指在屏幕上的滑动而滑动。初始化时显示效果:


(1)、layout方法

在View绘制时,会调用onLayout()方法来设置显示的位置。一样,能够经过修改View的left,top,right,bottom四个属性来控制View的坐标。与前面提供的模板代码同样,在每次回调onTouchEvent的时候,咱们都来获取一下触摸点的坐标,代码以下:

//获取当前输入点的X、Y坐标(视图坐标)
int x = (int) event.getX();
int y = (int) event.getY();复制代码

接着,在Action_DOWN事件中记录触摸点的坐标,以下:

case MotionEvent.ACTION_DOWN:
    // 记录触摸点坐标
    lastX = x;
    lastY = y;
    break;复制代码

最后,能够在Action_MOVE事件中计算偏移量,并将偏移量做用到Layout方法中,在目前Layout的left,top,right,bottom基础上,增长计算出来的偏移量,代码以下所示:

case MotionEvent.ACTION_MOVE:
    // 计算偏移量
    int offsetX = x - lastX;
    int offsetY = y - lastY;
    // 在当前left、top、right、bottom的基础上加上偏移量
    layout(getLeft() + offsetX,
            getTop() + offsetY,
            getRight() + offsetX,
            getBottom() + offsetY);
    break;复制代码

这样没错移动后,View都会调用Layout方法来对本身从新布局,从而达到移动View的效果。

上面的代码中,使用的是getX()、getY()方法来获取坐标值,即经过视图坐标来获取偏移量。固然,一样可使用getRawX()、getRawY()来获取坐标,并使用绝对坐标来计算偏移量,代码以下:

// 视图坐标方式
@Override
public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getRawX();
    int y = (int) event.getRawY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // 记录触摸点坐标
            lastX = x;
            lastY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            // 计算偏移量
            int offsetX = x - lastX;
            int offsetY = y - lastY;
            // 在当前left、top、right、bottom的基础上加上偏移量
            layout(getLeft() + offsetX,
                    getTop() + offsetY,
                    getRight() + offsetX,
                    getBottom() + offsetY);
            //从新设置初始化坐标
            lastX = x;
            lastY = y;
            break;

    }
    return true;
}复制代码

使用绝对坐标系,有一点很是须要注意的地方,就是在每次执行完ACTION_MOVE的逻辑后,必定要从新设置初始化坐标,这样才能准确地获取偏移量。

(2)、offsetLeftAndRight()与offsetTopAndBottom()

这个方法至关于系统提供的一个对左右、上下移动的API的封装。当计算出偏移量后,只须要使用以下代码就能够完成View的从新布局,效果与使用Layout方法同样,代码以下所示:

//同时对left和right进行偏移
offsetLeftAndRight(offsetX);
//同时对top和bottom进行偏移
offsetTopAndBottom(offsetY);复制代码

这里的offsetX、offsetY与在layout方法中计算offset方法同样。

(3)、LayoutParams

LayoutParams保存了一个View的布局参数。所以能够在程序中,经过改变LayoutParams来动态地修改一个布局的位置参数,从而达到改变View位置的效果。咱们能够很方便在程序中使用getLayoutParams()来获取一个View的LayoutParams。固然,计算偏移量的方法与在Layout方法中计算offset也是同样。当获取到偏移量以后,就能够经过setLayoutParams来改变其LayoutParams,代码以下:

LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);复制代码

这里getLayoutParams()获取LayoutParams时,须要根据View所在View父布局的类型来设置不一样的类型,好比这里将View放在LinearLayout中,那么就可使用LinearLayout.LayoutParams。若是在RelativeLayout中,就要使用RelativeLayout.LayoutParams。这一切的前提是你必需要有一个父布局,否则系统没法获取LayoutParams。

在经过改变LayoutParams来改变一个View的位置时,一般改变的是这个View的Margin属性,因此除了使用布局的LayoutParams以外,还可使用ViewGroup.MarginLayoutParams来实现这一一个功能,代码:

ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);复制代码

咱们能够发现,使用ViewGroup.MarginLayoutParams更加的方便,不须要考虑父布局的类型,固然它们的本质都是同样。

(4)、scrollTo与scrollBy

在一个View中,系统提供了scrollTo、scrollBy两种方式来改变一个View的位置。这两个方法的区别很是好理解,与英文中To与By的区别相似,scrollTo(x,y)表示移动到一个具体的坐标点(x,y),而scrollBy(dx,dy)表示移动的增量为dx,dy。

与前面几种方式相同,在获取偏移量后使用scrollBy来移动View,代码以下:

int offsetX = x - lastX;
int offsetY = y - lastY;
scrollBy(offsetX, offsetY);复制代码

可是,当咱们拖动View的时候,你会发现View并无移动,其实方法没错,View确实移动了,只是移动的并非咱们想要的东西。scrollTo、scrollBy方法移动的是View的content,即让View的内容移动,若是在ViewGroup中使用scrollTo、scrollBy方法,那么移动的将是全部子View,若是在View中使用,那么移动的将是View的内容,例如TextView,content就是它的文本,ImageView,content就是它的drawable对象。

经过以上的分析,如今知道为何不能再View中使用这两个方法来拖动这个View了。那么咱们就该View所在的ViewGroup中来使用scrollBy方法,移动它的子View,代码以下:

((View) getParent()).scrollBy(offsetX, offsetY);复制代码

可是再次拖动View的时候,你会发现View虽然移动了,但却在乱动,并非咱们想要的跟随触摸点的移动而移动。这里先看一下视图移动,不妨这样想象一下手机屏幕是一个中空的盖板,盖板下面是一个巨大的画布,也就是咱们想要显示的视图。当把这个盖板盖在画布上的某一处时,透过中间空的矩形,咱们看见了手机屏幕上显示的视图,而画布上其余地方的视图,则被盖板盖住了没法看见。咱们的视图与这个例子很是相似,咱们没有看见视图,并不表明它就不存在,有可能只是在屏幕外面而已。当调用scrollBy方法时,能够想象为外面的盖板在移动,这么说比较抽象。

下图一中间的矩形至关于屏幕,及可视区域。后面的content就至关于画布,表明视图。能够看到,只有视图的中间部分目前是可视的,其余部分都不可见。在可见区域中,咱们设置了一个Button,它的坐标为(20,10)。

下面使用scrollBy方法,将盖板(屏幕、可视区域),在水平方向上向X轴正方向(向右)平移20,在竖直方向上向Y轴正方向(下方)平移10,那么平移以后的可视区域如图二。


图一



图2、移动以后的可视区域

咱们发现,虽然设置scrollBy(20,10),偏移量均为X轴、Y轴正方向上的正数,可是在屏幕的可视区域内,Button却向X轴、Y轴负方向上移动了。这就是由于参考系选择的不一样,而产生的不一样效果。

经过上面的分析能够发现,若是讲scrollBy中的参数dx和dy设置为正数,那么content讲向坐标轴负方向移动,若是将scrollBy中的参数dx和dy设置为负数,那么content将向坐标轴正方向移动,所以回到前面的例子,要实现跟随着手指移动而滑动的效果,就必须将偏移量改成负值,代码以下:

int offsetX = x - lastX;
int offsetY = y - lastY;
((View) getParent()).scrollBy(-offsetX, -offsetY);复制代码

如今在运行一次发现和前面几种方式效果相同了,相似地使用绝对坐标时,也能够经过使用scrollTo发方法来实现这一效果。

(5)、Scroller

前面提到了scrollBy、scrollTo方法,就不得再也不来讲一说Scroller类。Scroller类与scrollBy、scrollTo方法十分类似。什么区别?先看例子,若是要完成这样一个效果;经过点击按钮,让一个ViewGroup的子View向右移动100个像素。问题看起来很简单,只要在按钮的点击事件中使用前面的scrollBy方法设置下偏移量就能够了吗?确实这样可让一个子ViewGroup中的子View平移,可是无论使用scrollBy仍是scrollTo方法,子view的平移都是瞬间发生的,在事件执行的时候平移就已经完成了,这样的效果会让人感受很是忽然,Google建议使用天然的过分动画来实现移动效果。所以Scroller类就这样诞生了,经过Scroller类能够实现平滑移动的效果,而不是瞬间就完成移动。

Scroller类的实现原理,其实它与前面使用的scrollTo和scrollBy方法来实现子View跟随手指移动的原理基本相似,虽然scrollBy芳芳法是让子View瞬间从某点移动到另外一个点,可是因为在ACTION_MOVE事件中不断获取手指移动的微小的偏移量,这样就将一段距离划分红了N个很是小的偏移量。虽然每一个偏移量里面,经过scrollBy方法进行了瞬间移动,可是在总体上却能够得到一个平滑移动的效果。这个原理与动画的实现原理也是基本相似的,它们都是利用了人眼的视觉暂留特性。

下面咱们使用Scroller类实现平滑移动,在这个实例中,一样让子View跟随手指的滑动而滑动,可是在手指离开屏蔽时,让子View平滑的移动到初始化位置,即屏幕左上角。使用Scroller类须要以下三个步骤:

  • 初始化Scroller

首先经过它的构造方法来建立一个Scroller对象,代码以下所示:

// 初始化Scroller
mScroller = new Scroller(context);复制代码
  • 重写computerScroller方法,实现模拟滑动

下面咱们须要重写computerScroller()芳芳法,它是使用Scroller类的核心,系统在绘制View的时候会在draw()方法中调用该方法。这个方法实际就是使用的scrollTo方法。再结合Scroller对象,帮助获取到当前滚动值。咱们能够经过不断地瞬间移动一个小的距离来实现总体上的平滑移动效果。代码以下:

@Override
public void computeScroll() {
    super.computeScroll();
    // 判断Scroller是否执行完毕
    if (mScroller.computeScrollOffset()) {
        ((View) getParent()).scrollTo(
                mScroller.getCurrX(),
                mScroller.getCurrY());
        // 经过重绘来不断调用computeScroll
        invalidate();
    }
}复制代码

Scroller类提供了computeScrollOffset()方法来判断是否完成了整个滑动,同时也提供了getCurrX()、getCurrY()方法来得到当前的滑动坐标。在上面的代码中,惟一须要注意的是invalidate()方法,由于只能在computeScroller()方法中获取模拟过程当中的scrollX和scrollY坐标。但computeScroll()方法是不会自动调用的,只能经过invalidate()->draw()->computeScroll()来间接调用compuetScroll()方法,因此须要在compuetScroll()方法中调用invaliDate()方法,实现循环获取scrollX和scrollY的目的。而当模拟过程结束后,scroller.compuetScrollOffset()方法会返回false,而中断循环,完成平滑移动过程。

  • startScroll开启模拟过程

咱们在须要使用平滑移动的事件中,使用Scroller类的startScroll()方法来开启平滑移动过程。startScroll()方法具备两个重载方法。

public void startScroll(int startX, int startY, int dx, int dy)复制代码
public void startScroll(int startX, int startY, int dx, int dy, int duration)复制代码

能够看到它们的区别就是一个具备指定的支持时长,而另外一个没有。很好理解,与在动画中设置duration和使用默认的显示时长是一个道理。其余四个坐标,则与他们的命名含义相同,就是起始坐标与偏移量。在获取坐标时,一般可使用getScrollX()和getScrollY()方法来获取父视图中content所滑动到的点的坐标,须要注意的是这个值的正负,它与在scrollBy、scrollTo中讲解的状况是同样的。

根据以上三步,就可使用Scroller类实现平滑移动,在构造方法中初始化Scroller对象,重写View的computerScroll()方法,最后监听手指离开屏蔽的事件,并在该事件中调用startScroll()方法完成平滑移动。监听手指离开屏幕的事件,只须要在onTouchEvent中增长一个ACTION_UP监听选项便可,代码以下所示:

case MotionEvent.ACTION_UP:
    // 手指离开时,执行滑动过程
    View viewGroup = ((View) getParent());
    mScroller.startScroll(
            viewGroup.getScrollX(),
            viewGroup.getScrollY(),
            -viewGroup.getScrollX(),
            -viewGroup.getScrollY());
    invalidate();
    break;复制代码

在startScroll()方法中咱们获取子View移动的距离-getScrollX()、getScrollY(),并将偏移量设置为其相反数,从而将子View滑动到原位置。这里的invalidate()方法是用来通知View进行重绘,调用computeScroll()的模拟过程。固然,也能够给startScroll()方法增长一个duration的参数来设置滑动的持续时长。

(6)、属性动画

属性动画请参见个人另外一篇:Android全套动画使用技巧

(7)、ViewDragHelper

Google在其support库中为咱们提供了DrawerLayout和SlidingPaneLayout两个布局来帮助开发者实现侧边栏滑动的效果。这两个新的布局方便咱们建立本身的滑动布局界面,在这两个强大布局背后有一个功能强大的类——ViewDragHelper。经过ViewDragHelper,基本能够实现各类不一样的滑动、拖放需求,所以这个方法也是各类滑动解决方案中的终结绝招。

下面演示一个使用ViewDragHelper建立一个QQ侧边栏滑动的布局,如图:


图三




图四

  • 初始化ViewDragHelper

首先须要初始化ViewDragHelper,ViewDragHelper一般定义在一个ViewGroup的内部,经过静态工厂方法进行初始化,代码以下:

mViewDragHelper = ViewDragHelper.create(this, callback);复制代码

第一个参数监听的View,一般须要一个ViewGroup,即parentView;第二个参数是一个Callback回调,这个回调就是整个ViewDragHelper的逻辑核心。

  • 拦截事件

重写拦截事件,将事件传递给ViewDragHelper进行处理;

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return mViewDragHelper.shouldInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    //将触摸事件传递给ViewDragHelper,此操做必不可少
    mViewDragHelper.processTouchEvent(event);
    return true;
}复制代码
  • 处理computeScroll()

使用ViewDragHelper一样须要重写computeScroll()方法,由于ViewDragHelper内部也是经过Scroller来实现平滑移动的。

@Override
public void computeScroll() {
    if (mViewDragHelper.continueSettling(true)) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
}复制代码
  • 处理回调Callback

建立一个ViewDragHelper.Callback

private ViewDragHelper.Callback getCallback = new ViewDragHelper.Callback() {
    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        return false;
    }
};复制代码

as自动重写tryCaptureView()方法,经过这个方法能够指定在建立ViewDragHelper时,参数parentView中的哪个子Vieww能够被移动,例如咱们在这个实例中自定义一个ViewGroup,里面定义了两个子View——Menu View和MainView,以下代码:

// 什么时候开始检测触摸事件
@Override
public boolean tryCaptureView(View child, int pointerId) {
    //若是当前触摸的child是mMainView时开始检测
    return mMainView == child;
}复制代码

具体垂直滑动方法clampViewPositionVertical()和水平滑动方法clampViewPositionHorizontal()。实现滑动这个两个方法必须写,默认返回值是0,即不发生滑动,固然若是只重写clampViewPositionVertical()或clampViewPositionHorizontal()中的一个,那么就只会实现该方向上的滑动效果。

// 处理垂直滑动
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
    return top;
}

// 处理水平滑动
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
    return left;
}复制代码

clampViewPositionVertical(View child, int top, int dy)中的参数top,表明在垂直方向上child移动的距离,dy则表示比较前一次的增量。clampViewPositionHorizontal(View child, int left, int dx)也是相似的含义,一般状况下只须要返回top和left便可,但须要更加精确地计算padding等属性的时候,就须要对left进行一些处理,并返回合适大小的值。

经过重写上面的三个方法,就能够实现基本的滑动效果。当用手拖动MainView的时候,它就可有跟随手指的滑动而滑动了,代码:

private ViewDragHelper.Callback callback =  new ViewDragHelper.Callback() {

            // 什么时候开始检测触摸事件
            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                //若是当前触摸的child是mMainView时开始检测
                return mMainView == child;
            }
 

            // 处理垂直滑动
            @Override
            public int clampViewPositionVertical(View child, int top, int dy) {
                return 0;
            }

            // 处理水平滑动
            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx) {
                return left;
            }

            
        };复制代码

在前面的Scroller中讲解时实现一个效果——手指离开屏幕后,View滑动回到初始位置。如今使用ViewDragHelper实现,在ViewDragHelper.Callback中,系统提供了这样的方法——onViewReleased(),经过重写这个方法,能够很是简单地实现当手指离开屏幕后实现的操做。这个方法内部是使用Scroller类实现的,这也是前面重写computeScroll()方法的缘由。

@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
    super.onViewReleased(releasedChild, xvel, yvel);
    //手指抬起后缓慢移动到指定位置
    if (mMainView.getLeft() < 500) {
        //关闭菜单
        //等同于Scroll的startScroll方法
        mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
        ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
    } else {
        //打开菜单
        mViewDragHelper.smoothSlideViewTo(mMainView,300,0);
        ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
    }
}复制代码

设置让MainView移动后左边距小于500像素的时候,就使用smoothSlideViewTo()方法来说MainView还原到初始状态,即坐标(0,0),左边距大于500则将MainView移动到(300,0)坐标,即显示MainView。

//ViewDragHelper

mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);复制代码

//Scroller

mScroller.startScroll(x,y,dx,dy);
invalidate();复制代码

滑动的时候,在自定义ViewGroup的onFinishInflate()方法中,按照顺序将子View分别定义成MenuView和MainView,并在onSizeChanged方法中得到View的宽度。若是须要根据View的宽度来处理滑动后的效果,就可使用这个值判断。

/***
 * 加载完布局文件后调用
 */
@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();
}复制代码

最后,整个经过ViewDragHelper实现QQ侧滑功能代码:

package com.xjf.drawview;

import android.content.Context;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;

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();
    }

    /***
     * 加载完布局文件后调用
     */
    @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();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将触摸事件传递给ViewDragHelper,此操做必不可少
        mViewDragHelper.processTouchEvent(event);
        return true;
    }

    private void initView() {
        mViewDragHelper = ViewDragHelper.create(this, callback);
    }

    private ViewDragHelper.Callback callback =
            new ViewDragHelper.Callback() {

                // 什么时候开始检测触摸事件
                @Override
                public boolean tryCaptureView(View child, int pointerId) {
                    //若是当前触摸的child是mMainView时开始检测
                    return mMainView == child;
                }

                // 触摸到View后回调
                @Override
                public void onViewCaptured(View capturedChild,
                                           int activePointerId) {
                    super.onViewCaptured(capturedChild, activePointerId);
                }

                // 当拖拽状态改变,好比idle,dragging
                @Override
                public void onViewDragStateChanged(int state) {
                    super.onViewDragStateChanged(state);
                }

                // 当位置改变的时候调用,经常使用与滑动时更改scale等
                @Override
                public void onViewPositionChanged(View changedView,
                                                  int left, int top, int dx, int dy) {
                    super.onViewPositionChanged(changedView, left, top, dx, dy);
                }

                // 处理垂直滑动
                @Override
                public int clampViewPositionVertical(View child, int top, int dy) {
                    return 0;
                }

                // 处理水平滑动
                @Override
                public int clampViewPositionHorizontal(View child, int left, int dx) {
                    return left;
                }

                // 拖动结束后调用
                @Override
                public void onViewReleased(View releasedChild, float xvel, float yvel) {
                    super.onViewReleased(releasedChild, xvel, yvel);
                    //手指抬起后缓慢移动到指定位置
                    if (mMainView.getLeft() < 500) {
                        //关闭菜单
                        //至关于Scroller的startScroll方法
                        mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
                        ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
                    } else {
                        //打开菜单
                        mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);
                        ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
                    }
                }
            };

    @Override
    public void computeScroll() {
        if (mViewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }
}复制代码

除此以外,ViewDragHelper不少强大的功能还没获得展现,在ViewDragHelper.Callback中,系统定义了大量的监听事件来帮助咱们处理各类事件,以下:

  • onViewCaptured()这个事件在用户触摸到View后回调
  • onViewDragStateChanged()这个事件在拖拽状态改变时回调,好比idle,dragging等状态

STATE_IDLE:View当前没有被拖拽也没执行动画,只是安静地待在原地

STATE_DRAGGING:View当前正在被拖动,因为用户输入或模拟用户输入致使View位置正在改变

STATE_SETTLING:View当前正被安顿到指定位置,由fling手势或预约义的非交互动做触发

  • onViewPositionChanged()//view在拖动过程坐标发生变化时会调用此方法,包括两个时间段:手动拖动和自动滚动。
相关文章
相关标签/搜索