最近在工作中使用到了scrollTo与scrollBy,因此在这准备对它们的用法以及TouchSlop与VelocityTracker做一下整理与总结,以便加深理解,以下是本篇的主要内容,至于Scroller类的解析以及用法,我会放在下一篇文件记录。
直接开始吧。
1.view相关位置参数
1.1 Android坐标系
在物理学中,描述一个物体的运动通常都需要选定一个参考系,因此所谓的view的相关位置参数也就是这里要说明的Android设备屏幕的平面直角坐标参考系,在android中,将屏幕最左上角的顶点作为android坐标系的原点,从这个点向右是X轴正方向,从这个点向下是Y轴正方向,如下图所示:
int[] location = new int[2]; view.getLocationOnScreen(location); int x = location[0]; int y = location[1];还有就是在触摸事件中我们也可以通过getRawX()和getRawY()来获取当前触摸坐标。以上所介绍的便是android坐标系,在android中还有一种比较特殊的坐标系,这种坐标系叫做视图坐标系,视图坐标系描述的是子视图在父视图中的位置关系。视图坐标与android坐标系一样 将父视图最左上角的顶点作为视图坐标系的原点,从这个点向右是X轴正方向,从这个点向下是Y轴正方向,如下图所示:
由图我们可以发现原点不再是android坐标系中的屏幕最左上角,而是以父视图左上角为坐标系原点。其实我们在触控事件中,通过getX()与getY()所获取到到坐标就是视图坐标系中的坐标。
1.2 View中各类获取间距的参数值
View自身提供的获取坐标的方法:
getTop():获取view自身的顶边到其父布局顶边的距离。
getLeft():获取view自身的左边到其父布局左边的距离。
getRight():获取view自身的右边到其父布局左边的距离。
getBottom():获取view自身的底边到其父布局顶边的距离。
MotionEvent提供的方法
getX() :获取点击事件距离控件左边的距离,即视图坐标。
getY() :获取点击事件距离控件顶边的距离,即视图坐标。
getRawX():获取点击事件距离整个屏幕左边的距离,即绝对坐标。
getRawY():获取点击事件距离整个屏幕顶边的距离,即绝对坐标。
不理解?怎么办?怪我咯?请看下图:
现在够清晰了。还不是不理解?继续怪我咯。翻篇.......
2.touchSlop与VelocityTracker
2.1 touchSlop
那天闲来无事,研究了一下触摸事件,不知在哪个墙角发现touchSlop这货,然后心中无数个草泥马奔腾而过......话说这货是啥?后来才知道案发现场是在ViewConfiguration类中,当时我在另一个类看到这么一个调用函数
ViewConfiguration.get(context).getScaledTouchSlop();而这个函数获取到得值就是 touchSlop,touchSlop到底是啥啊?根据方法注释理解这个touchSlop是一个滑动距离值的常量,也就是说当我们手触摸在屏幕上滑动时,如果滑动距离没有超过touchSlop值的话 ,android系统本身是不会认为我们在屏幕上做了手势滑动,因此只有当我们在屏幕上的滑动距离超过touchSlop值时,android系统本身才会认为我们做了滑动操作并去响应触摸事件,不过要注意的是不同的设备,touchSlop的值可能是不同的,一切以上述的函数获取为准。说到这里,这个touchSlop值到底有什么意义?当我们在处理滑动事件时,其实可以利用这个值来过滤掉一些没必要的动作,比如当两次滑动距离小于这个值时,我们就可以认为滑动没发生,从而更好的优化用户体验。 可是我还有疑问:ViewConfiguration这个货是干啥的?某位大神说过:源码之前,了无秘密!上源码!
/** * Contains methods to standard constants used in the UI for timeouts, sizes, and distances. */ public class ViewConfiguration {
根据文档注释这个类是用来存放UI相关的标准常量,如超时时间,大小,距离.......由此也可知touchSlop只不过是其中的一个常量罢了。我大概扫了几眼这个类,定义的常量还不少,其实我是想说,我不打算分析这个类.....有兴趣的自己再去扫扫.......
2.2 VelocityTracker
/** * Helper for tracking the velocity of touch events, for implementing * flinging and other such gestures. * * Use {@link #obtain} to retrieve a new instance of the class when you are going * to begin tracking. Put the motion events you receive into it with * {@link #addMovement(MotionEvent)}. When you want to determine the velocity call * {@link #computeCurrentVelocity(int)} and then call {@link #getXVelocity(int)} * and {@link #getYVelocity(int)} to retrieve the velocity for each pointer id. */ public final class VelocityTracker {
说啥?
翻译呗:辅助跟踪触摸事件的速率,如快速滑动或者其他手势操作。当我们准备开始跟踪滑动速率时可以使用obtain()方法来获取一个VelocityTracker的实例,然后在onTouchEvent回调函数中,使用addMovement(MotionEvent)函数将当前的移动事件传递给VelocityTracker对象。当我们决定计算当前触摸点的速率时可以调用computeCurrentVelocity(int units)函数来计算当前的速度,使用getXVelocity() 、getYVelocity()函数来获得当前X轴和Y轴的速度。
简单的说就是VelocityTracker是个速度跟踪类,用于跟踪手指滑动的速度,包括x轴方向和y轴方向的速度。如何使用?
如果我们决定跟踪View中onTouchEvent()方法中的手指滑动速度,可以在手指按下时(ACTION_DOWN)使用以下代码:
VelocityTracker velocityTracker=VelocityTracker.obtain(); velocityTracker.addMovement(event);
velocityTracker.addMovement(event)的作用可以理解为收集速率追踪点数据(柯南:跟踪?嘿嘿,你要小心了!)。
接着,当我们想知道当前手指滑动速度时,可以使用以下代码:
velocityTracker.computeCurrentVelocity(1000); float velocityX = velocityTracker.getXVelocity(); float velocityY = velocityTracker.getXVelocity();computeCurrentVelocity (int units),基于当前我们所收集到的点计算当前的速率,当我们确定要获得速率信息的时候,在调用该方法,因为使用它需要消耗很大的性能。
参数:units 我们想要指定的得到的速度单位,如果值为1,代表1毫秒运动了多少像素。如果值为1000,代表1秒内运动了多少像素。如果值为100,代表100毫秒内运动了多少像素。(这个参数设置真有点.......什么鬼嘛!)这个方法还有一个重载函数 computeCurrentVelocity (int units, float maxVelocity), 跟上面一样也就是多了一个参数。
参数:maxVelocity 该方法所能得到的最大速度,这个速度必须和你指定的units使用同样的单位,而且必须是整数.也就是,你指定一个速度的最大值,如果计算超过这个最大值,就使用这个最大值,否则,使用计算的的结果,
这个最大速度可以通过ViewConfiguration.get(context).getScaledMaximumFlingVelocity()方式获取。
getXVelocity()和getYVelocity() ,这两个很简单,获得横向和竖向的速率。前提是一定要先调用computeCurrentVelocity (int units)函数计算当前速度!
最后,东西我们不要了,当然要回收啦!这时当然要调用clear()来重置并调用recycler()方法来回收内存啦,代码如下,请收下!
/** * 使用完VelocityTracker,必须释放资源 */ private void releaseVelocityTracker() { if (mVelocityTracker != null) { mVelocityTracker.clear(); mVelocityTracker.recycle(); mVelocityTracker = null; } }以上就是 VelocityTracker类的简单介绍与使用方法。下面给出代码实例
package com.zejian.scrollerapp; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.ViewConfiguration; import android.widget.TextView; import com.zejian.scrollerapp.utils.LogUtils; /** * Created by zejian * Time 16/1/20 上午11:45 * Email [email protected] * Description: VelocityTracker速度测试类 */ public class VelocityTrackerActicity extends Activity { private static final String TAG = "VelocityTrackerActicity"; private TextView tv; private VelocityTracker mVelocityTracker; private int mPointerId; private int mMaxVelocity; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_velocity_tracker); tv= (TextView) findViewById(R.id.tv); tv.setText("VelocityTrackerActicity"); mMaxVelocity = ViewConfiguration.get(this).getScaledMaximumFlingVelocity(); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { return super.dispatchTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { LogUtils.e("onTouchEvent start!!"); Log.i(TAG, "ACTION_DOWN"); if(null == mVelocityTracker) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); final VelocityTracker verTracker = mVelocityTracker; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //获取第一个触点的id, 此时可能有多个触点,获取其中一个 mPointerId = event.getPointerId(0); break; case MotionEvent.ACTION_MOVE: //计算瞬时速度 verTracker.computeCurrentVelocity(1000, mMaxVelocity); float velocityX = verTracker.getXVelocity(mPointerId); float velocityY = verTracker.getYVelocity(mPointerId); LogUtils.e("velocityX-->" + velocityX); LogUtils.e("velocityY-->"+velocityY); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: releaseVelocityTracker();//释放资源 break; default: break; } return super.onTouchEvent(event); } /** * 使用完VelocityTracker,必须释放资源 */ private void releaseVelocityTracker() { if (mVelocityTracker != null) { mVelocityTracker.clear(); mVelocityTracker.recycle(); mVelocityTracker = null; } } }
3.scrollTo()与scrollBy()
在android中为了实现view的滑动,android系统为此提供了scrollTo()和scrollBy()两个方法。老样子呗,看看源码再说话。
/** * Set the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int, int, int, int)} and the view will be * invalidated. * @param x the x position to scroll to * @param y the y position to scroll to */ public void scrollTo(int x, int y) { if (mScrollX != x || mScrollY != y) { int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; mScrollY = y; invalidateParentCaches(); onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { postInvalidateOnAnimation(); } } } /** * Move the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int, int, int, int)} and the view will be * invalidated. * @param x the amount of pixels to scroll by horizontally * @param y the amount of pixels to scroll by vertically */ public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); }
根据scrollTo(int x, int y)的文档说明,当我们调用scrollTo(int x, int y)方法时,该方法内部将会去调用onScrollChanged(int, int, int, int),这也将直接导致view重绘,也就实现了所谓的view滑动效果。而scrollBy(int x, int y),这哥们可真的够懒了,一点内涵都没有,居然直接跑去调用scrollTo(int x, int y),也罢。不过这么一看两者的区别也很明显,scrollTo(int x, int y)是基于所给参数的绝对滑动,而scrollBy(int x, int y)是基于所给参数的相对滑动,简单一句,scrollTo()是一步到位,而scrollBy()是逐步累加,这点很容易明白,从源码就能看出来了。但是scrollTo或者scrollBy到底是改变了啥啊?(柯南:真相只有一个那就是看源码呗)从scrollTo(int x, int y)源码中我们可以看到mScrollX 和mScrollY 的值将会被改变,这两值又是啥?
/** * The offset, in pixels, by which the content of this view is scrolled * horizontally. * {@hide} */ @ViewDebug.ExportedProperty(category = "scrolling") protected int mScrollX; /** * The offset, in pixels, by which the content of this view is scrolled * vertically. * {@hide} */ @ViewDebug.ExportedProperty(category = "scrolling") protected int mScrollY;等等,我们好像发现了什么?没错, mScrollX 和 mScrollY都是偏移量,而且都是指当前view的内容相对view本身左上角起始坐标的偏移量。不理解?又怪我咯,看下图:
由此可知我们调用scrollTo(int x, int y)和scrollBy(int x, int y)时传递的参数并非是坐标而是偏移量。比如我们view是TextView,那么我们调用scrollTo或者scrollBy方法时,移动的其实就是TextView的内容,但如果我们的view是LinearLayout(ViewGroup),那么移动其实就是该布局内的子view了。到此也算明朗了。android的view内容也提供了获取这两个偏移量大小的方法,如下:
/** * Return the scrolled left position of this view. This is the left edge of * the displayed part of your view. You do not need to draw any pixels * farther left, since those are outside of the frame of your view on * screen. * * @return The left edge of the displayed part of your view, in pixels. */ public final int getScrollX() { return mScrollX; } /** * Return the scrolled top position of this view. This is the top edge of * the displayed part of your view. You do not need to draw any pixels above * it, since those are outside of the frame of your view on screen. * * @return The top edge of the displayed part of your view, in pixels. */ public final int getScrollY() { return mScrollY; }
来个小结:
1.scrollTo()的移动是一步到位,而scrollBy()逐步累加的
2.scrollTo()和scrollBy()传递的参数是偏移量而非坐标
3.scrollTo()和scrollBy()移动的都只是View的内容,View的背景本身是不移动的。
到了这里原本以为差不多了,可是柯南又跑了出来说:真相肯定只有一个,但肯定不是我想的那个,这真是草泥马又飞奔而来了......在实际操作中发现,传入的参数完全跟想执行的操作相反!!!
比如我们对于一个TextView调用scrollTo(0,20),那么该TextView中的content(比如显示的文字:波多),会怎么移动呢?按我们前面掌握的知识,应该是向下移动20个单位。但结果恰恰相反,向上移动了20个单位。如果我们想向下移动20个单位应该这样调用scrollTo(0,-20),这是为啥呢?
要解决这个问题,那么就得看看mScrollX和mScrollY是在哪里被使用的?
根据前面分析,调用scrollTo()方法将会导致view重绘,也就是会去调用public void invalidate(int l, int t, int r, int b)方法,我们先看看这个方法得源码:
/** * Mark the area defined by the rect (l,t,r,b) as needing to be drawn. The * coordinates of the dirty rect are relative to the view. If the view is * visible, {@link #onDraw(android.graphics.Canvas)} will be called at some * point in the future. * <p> * This must be called from a UI thread. To call from a non-UI thread, call * {@link #postInvalidate()}. * * @param l the left position of the dirty region * @param t the top position of the dirty region * @param r the right position of the dirty region * @param b the bottom position of the dirty region */ public void invalidate(int l, int t, int r, int b) { final int scrollX = mScrollX; final int scrollY = mScrollY; invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false); }
invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false)通过这个方法,我们大概也能猜到点猫腻了。如果我们传递的scrollerX值是正数的话,(l - scrollX) 计算后则左边距会变小,所以内容会往左移动(也就是x轴的负方向)。如果我们传递的scrollerX值是负数的话,(l - scrollX) 计算后则左边距会变大,因此内容会往右移动(也就是x轴的正方向),同理,y轴也一样。
所以有如下结论:如果我们想往x轴和y轴正方向移动时,mScrollY和mScrollX必须为负值,相反如果我们想往x轴和y轴负方向移动时,mScrollY和mScrollX就必须为正值啦。噢噢切克闹........
脑海突然冒出lol送塔的画面,然后内心又闪过cf爆敌方头的刺激感,其实我想说来个实战案例吧。
在这里我们自定义一个可以自由滑动的view,通过scrollBy()实现,自定义view代码如下:
package view; import android.content.Context; import android.graphics.Color; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; public class ScrollByDragView extends View{ private int lastX; private int lastY; public ScrollByDragView(Context context) { super(context); ininView(); } public ScrollByDragView(Context context, AttributeSet attrs) { super(context, attrs); ininView(); } public ScrollByDragView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); ininView(); } private void ininView() { setBackgroundColor(Color.BLUE); } @Override public boolean onTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: lastX = (int) event.getX(); lastY = (int) event.getY(); break; case MotionEvent.ACTION_MOVE: int offsetX = x - lastX; int offsetY = y - lastY; ((View) getParent()).scrollBy(-offsetX, -offsetY); break; } return true; } }
代码相对简单,但这里有点要注意的是,((View) getParent()).scrollBy(-offsetX, -offsetY),这个必须调用父类的scrollBy(),因为我们要滑动的我们自己的自定义view。
布局文件drag_view_scrollby.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <view.ScrollByDragView android:layout_width="100dp" android:layout_height="100dp" /> </LinearLayout>activity代码
package com.zejian.androidmotionevent; import android.app.Activity; import android.os.Bundle; public class DragViewScrollBy extends Activity{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.drag_view_scrollby); } }效果图:
好了,到此本篇结束,下篇将分析一下Scroller类。