您的位置:首页 > 移动开发 > Android开发

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

2017-10-13 20:50 489 查看
系列文章

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的基础知识,可能会有点枯燥,但绝对是进阶的必经之路,希望看客朋友们用心琢磨,细细品味,相信一定会有收获。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐