Android群英传学习笔记,介绍了android坐标系、视图坐标系、onTouch事件分发、layout方法、scrollBy方法、Scroller、ViewDragHelper 等等内容。

Android坐标系

在介绍如何实现View滑动之前先了解一下Android的坐标系,我们在初中数学就学过坐标系,有原点和X轴Y轴,不过屏幕上的坐标系稍微有点区别,移动设备一般将屏幕的左上角定义为原点,向右为X轴正方向,向下为Y轴正方向,如下图:

屏幕坐标系

View坐标系

与屏幕坐标系相同,View也有自己的坐标系,我们可以称之为视图坐标系,描述了本身和父布局的位置关系,原点在View的左上角:

视图坐标系

View及MotionEvent坐标获取

View自身坐标获取方法

  • getTop():获取到的,是view自身的顶边到其父布局顶边的距离

  • getLeft():获取到的,是view自身的左边到其父布局左边的距离

  • getRight():获取到的,是view自身的右边到其父布局左边的距离

  • getBottom():获取到的,是view自身的底边到其父布局顶边的距离

MotionEvent坐标获取

  • getX():获取点击事件相对控件左边的x轴坐标,即点击事件距离控件左边的距离

  • getY():获取点击事件相对控件顶边的y轴坐标,即点击事件距离控件顶边的距离

  • getRawX():获取点击事件相对整个屏幕左边的x轴坐标,即点击事件距离整个屏幕左边的距离

  • getRawY():获取点击事件相对整个屏幕顶边的y轴坐标,即点击事件距离整个屏幕顶边的距离

说了这么多方法都不如一张图最直接:原图链接

坐标获取

触控事件onTouch

学好触控事件是掌握后续内容的重要基础,触控事件回调的MotionEvent封装了一些常用的事件常量,定义了一些常见类型动作。

/**
 * A pressed gesture has started, the motion contains the initial starting location.
 */
public static final int ACTION_DOWN             = 0;

/**
 * A pressed gesture has finished, the motion contains the final release location as well as any intermediate
 * points since the last down or move event.
 */
public static final int ACTION_UP               = 1;

/**
 * A change has happened during a
 * press gesture (between {@link #ACTION_DOWN} and {@link #ACTION_UP}).
 */
public static final int ACTION_MOVE             = 2;

/**
 * The current gesture has been aborted.
 */
public static final int ACTION_CANCEL           = 3;

/**
 * A movement has happened outside of the normal bounds of the UI element. 
 */
public static final int ACTION_OUTSIDE          = 4;

/**
 * A non-primary pointer has gone down.
 */
public static final int ACTION_POINTER_DOWN     = 5;

/**
 * A non-primary pointer has gone up.
 */
public static final int ACTION_POINTER_UP       = 6;

我们让View滑动的大概思路是重写View的onTouchEvent(MotionEvent event)方法,来控制View的移动,这个代码模板基本固定的,show me the code :

@Override
public boolean onTouch(View v, MotionEvent event) {
    // 记录当前point所在的位置
    x = (int) event.getX();
    y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
        //处理按下事件
        break;
    case MotionEvent.ACTION_MOVE:
        //处理移动事件
        break;
    case MotionEvent.ACTION_UP:
        //处理松开事件
        break;
    }
    // 事件处理完毕
    return true;
}

该方法return true 代表触控事件到这里就处理完毕了,不必要再继续传递,不懂的可以去再回顾一下Android的触摸事件分发机制。下面我们就可以进入主题,来看一下有哪些方法可以移动View。

实现滑动

我们了解了Android坐标系和触控事件,接着我们可以模拟实现View的滑动了,思路是:当发生onTouch事件时,记录下位置,当手指移动时,记录移动的坐标,获得一个相对偏移量,然后修改View的位置,不断重复下去就实现了View的模拟滑动。那么,怎么改动View的位置呢,下面有介绍几种方法可以设置View的位置。

layout方法

  • 在ACTION_DOWN里面,记录下按下的坐标
case MotionEvent.ACTION_DOWN:
    lastX = x;
    lastY = y;
    break;
  • 每次onTouch回调记录下该点的坐标
int x = (int) event.getX();
int y = (int) event.getY();
  • 在ACTION_MOVE里面计算偏移量,然后调用layout方法
case MotionEvent.ACTION_MOVE:
    int offsetX = x - lastX;
    int offsetY = y - lastY;
    layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
    break;

效果如图:

layout方法.gif

offsetLeftAndRight()和offsetTopAndBottom()

看命名就知道这个方法的作用,这是系统提供的对View上下、左右同时进行移动的API,效果与上相同。就不赘述了。

case MotionEvent.ACTION_MOVE:
    int offsetX = x - lastX;
    int offsetY = y - lastY;
    offsetLeftAndRight(offsetX);
    offsetTopAndBottom(offsetY);
    break;

LayoutParams

通过改变View的LayoutParams布局参数,就可以移动View的位置,这里通常修改View的Margin属性,代码如下:

case MotionEvent.ACTION_MOVE:
    int offsetX = x - lastX;
    int offsetY = y - lastY;
    ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
    params.leftMargin = getLeft() + offsetX;
    params.topMargin = getTop() + offsetY;
    break;

其实根据父布局的类型,可以设置LinearLayout.LayoutParams或者RelativeLayout.LayoutParams,不过这样就必须先知道父布局的类型,不如ViewGroup.MarginLayoutParams来的方便。

scrollTo和scrollBy

前者表示移动到具体的坐标点位置,后者表示在原有的位置基础上再移动一个偏移量。但是与之前三个方法不同的是,前三个方法都是移动View自己本身,而这两个方法移动的都是View里面的内容,如果放在ViewGroup中使用,则移动的是ViewGroup里面所有的子View

那我们的思路就要换一下了,我们为了移动View,那我们就来移动View所在的ViewGroup,但是要注意的是,移动的偏移量要取反,为什么呢?这是因为本来是该View移动dx、dy,现在View保持不动,让ViewGroup移动,则根据相对运动原理,就相当于ViewGroup移动了-dx、-dy。

下面我们来举个简单的例子解释scrollBy。如下图:ViewGroup里面是可视区域,第二个小人的坐标是(200,100),现在我们把ViewGroup移到第二个小人的位置,scrollBy(200,100),效果如第二张图:在可视区域内,相当于第二个小人的偏移量为(-200,-100)

移动前的可视区域

移动后的可视区域

这么解释一定明白多了,我们看一下实现代码:

case MotionEvent.ACTION_MOVE:
    int offsetX = x - lastX;
    int offsetY = y - lastY;
    ((View)getParent()).scrollBy(-offsetX,-offsetY);
    break;

效果如图:

scrollBy方法.gif

Scroller

通过Scroller类来实现一些平滑的动画效果,可以设置动画时间等等,简直就是滑动利器!现在我们来实现一个效果:View跟着手指滑动,当松开手指时就让View回到原始位置

  • 在View构造函数里初始化Scroller
scroller = new Scroller(context);
  • 重写computeScroll方法
@Override
public void computeScroll() {
    super.computeScroll();
    if (scroller.computeScrollOffset()) {
        offsetLeftAndRight(scroller.getCurrX()-getLeft());
        offsetTopAndBottom(scroller.getCurrY()-getTop());
        invalidate();
    }
}
  • 在ACTION_UP里启动动画
scroller.startScroll(getLeft(),getTop(),-getLeft()+initX,-getTop()+initY,2000);

startScroll前两个参数是起始位置,后两个参数为终点位置,第五个参数是动画持续时间,可以省略。

效果如图:

Scroller方法.gif

属性动画

这个后续再单独写一篇介绍属性动画的,跳过。

ViewDragHelper

在开发自定义ViewGroup的时候,经常要根据业务需求实现onInterceptTouchEvent和onTouch(很繁琐啊!有木有!),不过Google在support库中为我们提供了一个超级强大的类ViewDragHelper,可以实现诸多滑动布局,侧滑菜单就是之一。这里奉上官方介绍,可能需要翻墙?

下面我们举个实现侧滑菜单的例子:自定义ViewGroup布局,然后里面有MenuView和MainView,滑动MainView超过一定距离就显示MenuView。

  • 初始化
private View mMenuView,mMainView;
private int mWidth;

//布局完成后调用
@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();//侧滑菜单的宽度
}

//构造函数
public ViewDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mViewDragHelper = ViewDragHelper.create(this,callback);
}

其中,构造函数里的callback是我们要自己实现的业务逻辑。也是该类的重要核心内容

  • 拦截事件交给ViewDragHelper处理
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    return mViewDragHelper.shouldInterceptTouchEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    mViewDragHelper.processTouchEvent(event);
    return true;
}
  • 重写computeScroll方法
@Override
public void computeScroll() {
    if (mViewDragHelper.continueSettling(true)) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
}
  • 实现callback
private final ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        //触摸的布局是否为MainView
        return mMainView==child;
    }

    @Override
    public int clampViewPositionVertical(View child,int top,int dy) {
        //不需要检测垂直滑动,直接返回0
        return 0;
    }

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

    @Override
    public void onViewReleased(View child,float xVel,float yVel) {
        super.onViewReleased(child,xVel,yVel);
        //核心逻辑:滑动MainView超过一定距离就显示MenuView
        if (mMainView.getLeft() < mWidth) {
            mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
            ViewCompat.postInvalidateOnAnimation(ViewDragLayout.this);
        } else {
            mViewDragHelper.smoothSlideViewTo(mMainView,mWidth,0);
            ViewCompat.postInvalidateOnAnimation(ViewDragLayout.this);
        }
    }
};

来看一下效果:

ViewDragHelper.gif

ViewDragHelper.Callback中定义有大量的回调方法,就不一一介绍了。

最后

到这里,我们介绍的View滑动方法就学习完了,最后我们来实现一个滑动ViewGroup,来模拟微信下拉的粘性动画,直接上代码:

@Override
public boolean onTouchEvent(MotionEvent event) {
    int y = (int) event.getRawY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            if (scroller.computeScrollOffset())
                scroller.forceFinished(true);
            lastY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            int scrollY = y - lastY;
            offsetTopAndBottom(scrollY/3-getTop());
            break;
        case MotionEvent.ACTION_UP:
            scroller.startScroll(getLeft(),getTop(),0,-getTop()+initY,duration);
            invalidate();
            break;
    }
    return true;
}

在ACTION_DOWN里判断动画有没有结束,可以强制结束,这样就可以连续向下拖动。在ACTION_MOVE里设置偏移量,除以3可以调节偏移量滑动的比例。最后在松手时回到原位置,一起来看一下效果吧。

UIScrollView.gif

项目地址

参考:

  1. 《Android群英传》徐宜生 第五章:Android Scroll 分析 ps:医生的书力荐,还有第二版神兵利器大大提高效率,带你飞。
  2. 安卓中的坐标系 ps:良心之作,推荐
  3. Android ViewDragHelper完全解析 ps:鸿洋公众号hongyangAndroid,关注可以学到不少东西。