Android View深入解析基础知识VelocityTracker,GestureDetector,Scroller
Posted Ruffian-痞子
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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();
刚才说了,速率是矢量有方向 xVel
,yVel
也是有正负值的,
速度 = (末位置 - 起位置) / 时间
其中 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个接口,一个内部类
接口:OnGestureListener
,OnDoubleTapListener
内部类: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
有什么区别呢?区别:如果是单击事件回调 onSingleTapUp
和 onSingleTapConfirmed
;如果是双击事件不会执行 onSingleTapConfirmed
2. onDoubleTap(MotionEvent e)
:双击事件
3. onDoubleTapEvent(MotionEvent e)
:双击间隔中发生的动作
SimpleOnGestureListener
对OnGestureListener
,OnDoubleTapListener
两个接口的所有方法进行了空实现,开发者可以对所需要实现的方法进行重写
说了很多理论的东西,但是很有用,认认真真琢磨一下,下面简单看一下使用
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内容可以通过 scrollBy
, scrollTo
进行滑动,但是这种滑动太过于生硬,用户体验很差,于是我们需要使用 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 获取当前 scrollX
,scrollY
,然后通过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的基础知识,可能会有点枯燥,但绝对是进阶的必经之路,希望看客朋友们用心琢磨,细细品味,相信一定会有收获。
以上是关于Android View深入解析基础知识VelocityTracker,GestureDetector,Scroller的主要内容,如果未能解决你的问题,请参考以下文章
Carson带你学Android:深入解析自定义View工作流程