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

系列文章
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 - left

height = 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
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值