Android View深入解析(一)基础知识VelocityTracker,GestureDetector,Scroller

来源:互联网 发布:python win32api 截屏 编辑:程序博客网 时间:2024/05/22 04:53

系列文章
Android View深入解析(一)基础知识VelocityTracker,GestureDetector,Scroller
Android View深入解析(二)事件分发机制
Android View深入解析(三)滑动冲突与解决

本系列文章建立在有一定View基础的前提上,适合开发者进阶提升。
相信不少开发者都尝试过自定义View,能够轻易的画出一些简单的控件,这时候你是不是觉得自己好像已经很厉害了?觉得自定义View也不过如此。很正常,这也许就是在入门阶段的瓶颈,是时候突破一下成为进阶选手了,强烈建议:这个系列的文章,别只是看看,深入理解,然后动手码一下,真能受益不浅。

View 的位置参数

View是Android中所有控件的基础类,TextView,ImageView等基础控件都是继承自View。View的位置是通过4个属性决定的:left,top,right,bottom
这4个属性都是相对于父容器而言的。top是指View上边缘到父容器的纵坐标值,left是View左边缘到父容器的横坐标值。right,bottom类推。其中需要注意的是,在Android中 x 轴和 y轴的正方向分别是 右 和 下 ,也就是我们常说的,原点在左上角。

(图片源自任玉刚老师)
这里写图片描述

根据上图我们可以得出

width = right - leftheight = bottom - top 

通过查看源码我们发现在View类中分别存在mLeft,mRight,mTop,mBottom 这4个成员变量,它们的获取方式

  • left = getLeft();
  • right = getRight();
  • top = getTop();
  • bottom = getBottom();

从android 3.0 开始,view 增加了几个额外的参数:x,y,translationX,translationY,其中x,y是View左上角的坐标,而 translationX,translationY 是 View 左上角相对于父容器的偏移量,与View基本参数一样,这几个参数都是相对于父容器,并且提供相应的 get/set 方法。这几个参数的换算关系如下:

x = left + translationX ;y = top + translationY ;

需要注意的是,View在平移过程中,left,top 是指原始左上角的位置信息,其值并不会改变,此时改变的是:x,translationX,y,translationY 这四个参数。

MotionEvent和TouchSlop

1.MotionEvent
是手指触摸屏幕产生的一系列事件,其中常用的有:

  • MotionEvent.ACTION_DOWN : 按下屏幕一瞬间触发
  • MotionEvent.ACTION_MOVE :按下后在屏幕上稍微移动就会产生的事件
  • MotionEvent.ACTION_UP :抬起时触发

当手指点击屏幕然后松开,触发事件:DOWN > UP
当手指点击屏幕,滑动一会再松开:DOWM > MOVE…MOVE > UP
手指在移动过程中会产生多次 MOVE 事件,它很敏感,稍微移动一下都会触发大量的MOVE事件

以上3种是常见的触屏事件,通过MotionEvent对象可以获取到触发事件时 x , y 的坐标值。系统提供了两组方法 getX/getY 和getRawX/getRawY

getX/getY : 返回相对于 当前View 左上角的 x y 坐标值getRawX/getRawY : 返回相对于 手机屏幕 左上角的 x y 坐标值

2.TouchSlop

TouchSlop 是系统所能识别的被认为是滑动的最小距离,也就是说,滑动两点之间的距离小于这个常量,系统则 不 认为这是滑动操作。这个常量跟设备相关,不同的手机获取的值可能不同,获取方法:

int touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();

可以利用这个常量来做滑动过滤,刚刚说了 MotionEvent.ACTION_MOVE 是一个非常敏感的事件,轻微动一下手指触发一大堆 MOVE 事件,而且移动距离非常小,如果此时控件逻辑跟随手指移动,则会出现一直抖动的情况,通过TouchSlop 判断,只有大于TouchSlop 的滑动才认为是滑动事件,小于这个常量的移动则不认为是滑动事件,这样做将会有更好的用户体验

View的滑动 scrollBy / scrollTo

1.scrollBy / scrollTo

为了实现View的滑动,View提供了scrollBy/scrollTo方法实现View的滑动,这两个方法有什么区别呢?通过查看源码其实 scrollBy 也是调用 scrollTo 方法。
scrollBy 基于当前位置的相对滑动,例如:从0开始向右滑动10px,不断调用,不断移动 ,0 -> 10,10 -> 20,20 -> 30 …
scrollTo 基于所传参数的绝对滑动,例如:从0开始向右滑动10px,无论调用几次都是从 0 -> 10。

来认识View内部的两个属性:mScrollX 和 mScrollY,这两个属性可以通过 getScrollX、getScrollY 获得。这里记住一个原则,在View的滑动过程中

mScrollX 的值总是等于 View 左边缘到 View内容左边缘的水平距离
mScrollY 的值总是等于 View 上边缘到 View内容上边缘的竖直距离

这里写图片描述

来,看图说明:
红色方框表示View,蓝色方框表示View内容,
红色箭头的距离就是mScrollX的值,蓝色箭头的距离就是mScrollY的值。

当View内容左边缘 在View左边缘的右边时 mScrollX为负值
当View内容上边缘 在View上边缘的下边时 mScrollY为负值

很拗口吧?还是一张图来的实际。(方框表示View,实体阴影表示View内容)
(图来自任玉刚老师)

这里写图片描述

① 原始状态;② 水平向左移动100px;③ 水平向右移动100px
④ 水平向右移动100px,竖直向上移动100px ;⑤ 竖直向上移动100px ; ⑥ 竖直向下移动100px

这里特别说明一下: scrollBy/scrollTo实现的滑动指View内容的滑动,并不是View本身位置的滑动。

VelocityTracker 速度追踪

VelocityTracker 主要用于跟踪触摸事件的速率,例如: 手指在水平方向或竖直方向滑动的速率。
什么是速率?其实速率也就是我们常说的速度,从物理学上说,速度表示物体运动快慢程度,速度是矢量,有大小和方向。公式:v = s / t ;
当然,我们这里说的VelocityTracker追踪器获取的速率也是有大小和方向(正负值)。
使用方法很简单,首先创建实体

VelocityTracker  mVelocityTracker = VelocityTracker.obtain();

1.跟踪触摸事件,那么我们得跟MotionEvent关联起来:mVelocityTracker.addMovement(event);
2.计算速率:mVelocityTracker.computeCurrentVelocity(1000);
方法名起的很好,一目了然,计算当前速率(所以说写代码的时候命名是很重要的),参数是时间 t ,单位 毫秒
3.获取水平或者竖直方向的速率:

int xVel = (int) mVelocityTracker.getXVelocity();int yVel = (int) mVelocityTracker.getYVelocity();

刚才说了,速率是矢量有方向 xVelyVel 也是有正负值的,

速度 = (末位置 - 起位置) / 时间

其中 xVel 水平方向从左向右滑动是 正值,从右向左是负值;yVel 竖直方向从上往下滑动是 正值,从下往上是负值。这个不难理解,原点在左上角,向右和向下是正方向。

记得用完之后需要mVelocityTracker.clear(); clear()将速度跟踪器复位到初始状态,以便再次使用,当你不再需要使用VelocityTracker的时候,需要将对象释放掉,避免内存溢出,mVelocityTracker.recycle();
下面给出一个完整是示例代码

public class VelocityTrackerDemo extends View {    private VelocityTracker mVelocityTracker;    public VelocityTrackerDemo(Context context) {        super(context);        //创建实例        mVelocityTracker = VelocityTracker.obtain();    }    @Override    public boolean onTouchEvent(MotionEvent event) {        //关联(添加)事件        mVelocityTracker.addMovement(event);        switch (event.getAction()) {            case MotionEvent.ACTION_DOWN:                break;            case MotionEvent.ACTION_MOVE:                break;            case MotionEvent.ACTION_UP:                //计算速率                mVelocityTracker.computeCurrentVelocity(1000);                //获取水平、竖直方向速率                int xVel = (int) mVelocityTracker.getXVelocity();                int yVel = (int) mVelocityTracker.getYVelocity();                Log.e("VelocityTracker", "xVel:" + xVel);                if (xVel > 0) {//向左滑动                } else {//向右滑动                }                //复位                mVelocityTracker.clear();                break;        }        return super.onTouchEvent(event);    }    @Override    protected void onDetachedFromWindow() {        super.onDetachedFromWindow();        //释放        mVelocityTracker.recycle();    }}

GestureDetector 手势检测

用户触摸屏幕的时候会产生多种事件,事件能够组合成许多手势,例如:点击,滑动,双击等等,一般情况下,我们可以重写 onTouchEvent 方法,根据触发事件编写逻辑实现手势操作,但是这个方法太过于简单,要实现复杂的手势就显得力不从心了,于是便有了 GestureDetector(Gesture:手势Detector:识别)类,通过这个类我们可以识别很多的手势。通过查看源码发现GestureDetector给提供了2个接口,一个内部类
接口:OnGestureListenerOnDoubleTapListener
内部类:SimpleOnGestureListener

OnGestureListener 接口

 public interface OnGestureListener {        boolean onDown(MotionEvent e);        void onShowPress(MotionEvent e);        boolean onSingleTapUp(MotionEvent e);        boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);        void onLongPress(MotionEvent e);        boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);    }

这里提供了6个函数的定义需要我们实现,看看这些函数在什么情况会触发
1.onDown(MotionEvent e) :手指按下触发
2.onShowPress(MotionEvent e) :当手指按下屏幕一段时间,并且在没有执行滑动或者抬起时调用。大概就是按钮按下时背景改变的那个状态
3.onLongPress(MotionEvent e) :长按屏幕事件

触发顺序:onDown > onShowPress > onLongPress

4.onSingleTapUp(MotionEvent e) :一次单独的轻击

非常快速的点击一下:

这里写图片描述

按下之后稍微迟疑一下再抬起(这个迟疑的时间就是触发onShowPress的时间,具体是多长应该有个获取的方式)

这里写图片描述

如果按下时间过长再抬起,或者按下后滑动再抬起,都不会触发onSingleTapUp

这里写图片描述

5.onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) :滑屏,用户按下屏幕,快速滑动,松开
参数解释:
e1 :第一个 ACTION_DOWN MotionEvent
e2 :最后一个 ACTION_MOVE MotionEvent
velocityX:X轴上的运动速率 像素/秒
velocityY:Y轴上的运动速率 像素/秒

6.onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) :按住View拖动触发
参数解释
e1 :第一个 ACTION_DOWN MotionEvent
e2 :最后一个 ACTION_MOVE MotionEvent
distanceX:X轴上的移动距离
distanceY:Y轴上的移动距离

OnDoubleTapListener 接口

public interface OnDoubleTapListener {        boolean onSingleTapConfirmed(MotionEvent e);        boolean onDoubleTap(MotionEvent e);        boolean onDoubleTapEvent(MotionEvent e);    }

1.onSingleTapConfirmed(MotionEvent e) : (确认)单击事件。

理解:相当于最后确认该次事件是 onSingleTap 而不是 onDoubleTap,跟 onSingleTapUp 有什么区别呢?区别:如果是单击事件回调 onSingleTapUponSingleTapConfirmed;如果是双击事件不会执行 onSingleTapConfirmed

这里写图片描述

2. onDoubleTap(MotionEvent e) :双击事件
3. onDoubleTapEvent(MotionEvent e):双击间隔中发生的动作

SimpleOnGestureListener
OnGestureListenerOnDoubleTapListener 两个接口的所有方法进行了空实现,开发者可以对所需要实现的方法进行重写

说了很多理论的东西,但是很有用,认认真真琢磨一下,下面简单看一下使用

mGestureDetector = new GestureDetector(mContext, new SimpleOnGesture());

构造实例:传入context,和一个实现OnGestureListener接口的实例

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

调用onTouchEvent方法检测手势触发的事件。

接着我们编写一个ImageView实现双击放大的效果(粗略实现)

package com.r.view;import android.animation.ObjectAnimator;import android.content.Context;import android.util.AttributeSet;import android.view.GestureDetector;import android.view.MotionEvent;import android.widget.ImageView;import android.widget.Toast;/** * GestureDetector使用demo * * @author ZhongDaFeng * @date 2017/10/14 */public class RImageView extends ImageView {    private boolean mIsEnlarge = false;    private Context mContext;    private RImageView mImageView;    private GestureDetector mGestureDetector;    public RImageView(Context context) {        this(context, null);    }    public RImageView(Context context, AttributeSet attrs) {        super(context, attrs);        mContext = context;        mImageView = this;        mGestureDetector = new GestureDetector(mContext, new SimpleOnGesture());        /**         *开启可点击         */        setEnabled(true);        setFocusable(true);        setLongClickable(true);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        mGestureDetector.onTouchEvent(event);        return super.onTouchEvent(event);    }    class SimpleOnGesture extends GestureDetector.SimpleOnGestureListener {        @Override        public boolean onDown(MotionEvent e) {            LogUtils.e("onDown");            return false;        }        @Override        public void onShowPress(MotionEvent e) {            LogUtils.e("onShowPress");        }        @Override        public boolean onSingleTapUp(MotionEvent e) {            LogUtils.e("onSingleTapUp");            return false;        }        @Override        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {            LogUtils.e("onScroll");            return false;        }        @Override        public void onLongPress(MotionEvent e) {            LogUtils.e("onLongPress");        }        @Override        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {            LogUtils.e("onFling");            return false;        }        @Override        public boolean onSingleTapConfirmed(MotionEvent e) {            LogUtils.e("onSingleTapConfirmed");            Toast.makeText(mContext, "单击", Toast.LENGTH_SHORT).show();            return super.onSingleTapConfirmed(e);        }        @Override        public boolean onDoubleTap(MotionEvent e) {            LogUtils.e("onDoubleTap");            Toast.makeText(mContext, "双击", Toast.LENGTH_SHORT).show();            float start = 1.0f;            float end = 2.0f;            if (mIsEnlarge) {                start = 2.0f;                end = 1.0f;            }            ObjectAnimator.ofFloat(mImageView, "scaleX", start, end).setDuration(150).start();            mIsEnlarge = !mIsEnlarge;            return super.onDoubleTap(e);        }    }}

很简单,一幕了然,双击执行动画放大,再双击缩小。在xml布局文件中直接使用控件运行就可以查看效果

Scroller 弹性滑动

前面介绍了View内容可以通过 scrollByscrollTo 进行滑动,但是这种滑动太过于生硬,用户体验很差,于是我们需要使用 Scroller 实现弹性滑动,缓慢的,优雅的滑动。

1.startScroll(int startX, int startY, int dx, int dy, int duration) :开始滚动,通过拖拽等进行View内容的滚动

参数解释
startX :起始X偏移量
startY :起始Y偏移量
dx : X轴将要移动的偏移量
dy : Y轴将要移动的偏移量
duration :执行时间

2.fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) : 滑动,手指按下屏幕快速移动后抬起,View内容继续滑动

参数解释
startX :起始X偏移量
startY :起始Y偏移量
velocityX : X轴滑动速度
velocityY : Y轴滑动速度
minX :X轴最小滑动距离
maxX :X轴最大滑动距离
minY :Y轴最小滑动距离
maxY :Y轴最大滑动距离

3. computeScrollOffset() :计算滚动偏移量,返回值boolean。如果返回 true 表示滑动还未结束,返回false表示滑动已经结束

Scroller 实现滑动需要重写 computeScroll()

 @Override    public void computeScroll() {        super.computeScroll();        if (mScroll.computeScrollOffset()) {            scrollTo(mScroll.getCurrX(), mScroll.getCurrY());            postInvalidate();        }    }

computeScroll() 中向 Scroll 获取当前 scrollXscrollY,然后通过scrollTo进行滑动,再调用 postInvalidate()postInvalidate() 会导致 View 重绘,View 的 draw 又会调用 computeScroll() 方法,
只要滑动还未结束就会一直执行,慢慢移动到目标位置。

那么第一次调用重绘的地方在哪里呢?当然是在开始执行滑动之后

 private void smoothScrollBy(int dx, int dy) {        mScroller.startScroll(0, 0, dx, dy, 500);        invalidate();    }

我们实现一下滚动的效果,手指向上滑动,View内容跟着向上滚动至半屏;手指向下滑动View内容向下滚动至半屏

 @Override    public boolean onTouchEvent(MotionEvent event) {        mVelocityTracker.addMovement(event);        int y = (int) event.getY();        switch (event.getAction()) {            case MotionEvent.ACTION_UP:                mVelocityTracker.computeCurrentVelocity(1000);                int yVelocity = (int) mVelocityTracker.getYVelocity();                if (yVelocity > 0) {                    scroll(-mHeightPixels / 2 - getScrollY());                } else {                    scroll(mHeightPixels / 2 - getScrollY());                }                mVelocityTracker.clear();                break;        }        return true;    }

上述代码分析,当手指抬起时获取滑动速度,再根据速度的方向做上下滑动的判断,当竖直速度大于0表示向上滑动,小于0表示向下滑动。
附上完整示例代码

public class ScrollLayout extends LinearLayout {    private int mHeightPixels = 0;    private Scroller mScroll;    private VelocityTracker mVelocityTracker;    public ScrollLayout(Context context) {        this(context, null);    }    public ScrollLayout(Context context, AttributeSet attrs) {        super(context, attrs);        init(context);    }    private void init(Context context) {        mScroll = new Scroller(context);        mVelocityTracker = VelocityTracker.obtain();        mHeightPixels = context.getResources().getDisplayMetrics().heightPixels;    }    @Override    public boolean onTouchEvent(MotionEvent event) {        mVelocityTracker.addMovement(event);        switch (event.getAction()) {            case MotionEvent.ACTION_UP:                mVelocityTracker.computeCurrentVelocity(1000);                int yVelocity = (int) mVelocityTracker.getYVelocity();                if (yVelocity > 0) {                    scroll(-mHeightPixels / 2 - getScrollY());                } else {                    scroll(mHeightPixels / 2 - getScrollY());                }                mVelocityTracker.clear();                break;        }        return true;    }    /**     * 滚动     *     * @param dy     */    private void scroll(int dy) {        mScroll.startScroll(0, getScrollY(), 0, dy, 500);        invalidate();    }    @Override    public void computeScroll() {        super.computeScroll();        if (mScroll.computeScrollOffset()) {            scrollTo(0, mScroll.getCurrY());            postInvalidate();        }    }    @Override    protected void onDetachedFromWindow() {        super.onDetachedFromWindow();        mVelocityTracker.recycle();    }}

xml布局代码

<?xml version="1.0" encoding="utf-8"?><com.r.view.ScrollLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:id="@+id/container"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical">    <TextView        android:layout_width="match_parent"        android:layout_height="match_parent"        android:background="@color/colorAccent"        android:gravity="center"        android:text="@string/app_name"        android:textSize="20sp" /></com.r.view.ScrollLayout>

直接在activity中引入布局文件运行即可。

以上是View的基础知识,可能会有点枯燥,但绝对是进阶的必经之路,希望看客朋友们用心琢磨,细细品味,相信一定会有收获。

阅读全文
0 0
原创粉丝点击