主要介绍内容:
View的位置主要由它的四个顶点来决定,分别对应与View的四个属性:top、left、right、bottom,其中top是左上角纵坐标,left是左上角横坐标,right是右下角横坐标,而bottom是右下角纵坐标。需要注意的是,这些坐标都是相对于View的父容器来说的,因此它是一种相对坐标,View的坐标和父容器的关系如下图所示。在Android中,X轴和Y轴的正方向分别为右和下,这点不难理解,不仅仅是Android,大部分显示系统都是按照这个标准来定义坐标系的。
根据上面给出的图,我们很容易得出一个结论:
width = right - left
height = bottom - top
那么如何得到View的这四个参数呢?也非常简单,在View的源码中它们对应于mLeft、mRight、mTop 和 mBottom 这四个成员变量,具体获取方式如下所示:
left = getLeft()
right = getRight()
top = getTop()
bottom = getBottom
从Android 3.0开始,View增加了额外的几个参数:x、y、translationX 和 translationY,其中x和y是View左上角的坐标,而translationX和translationY是View左上角相对于父容器的偏移量。这几个参数也是相对于父容器的坐标,并且translationX和translationY的默认值是0,和View的四个基本的位置参数一样,View也为它们提供了get/set方法,这几个参数的换算关系如下所示。
x = left + translationX
y = top + translationY
需要注意的是,View在平移过程中,top和left表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是x、y、translationX 和 translationY这四个参数。
在手指接触屏幕后所产生的一系列事件中,典型的事件类型有如下几种:
正常情况下,一次手指接触屏幕的行为会触发一系列点击事件,考虑如下几种情况:
上述两种情况是典型的事件序列,同时通过MotionEvent对象我们可以得到点击事件发生的 X 和 Y坐标。为此,系统提供了两组方法:getX/getY 和 getRawX/getRawY。它们的区别其实很简单,getX/getY 返回的是相当于当前View左上角的 x 和 y 坐标,而 getRawX/getRawY 返回的是相当于手机屏幕左上角的 x 和 y 坐标 如图:
TouchSlop是系统所能识别出的被认为是滑动的最小距离,换句话说,当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。原因很简单:滑动的距离太短,系统不认为它是滑动。这是一个常量,和设备有关,在不同设备上这个值可能是不同的,通过如下方式即可获取这个常量:ViewConfiguration.get(getContext()).getScaledTouchSlop(),这个常量有什么意义呢?当我们在处理滑动时,可以利用这个常量来做一些过滤,比如当两次滑动事件的滑动距离小于这个值,我们就可以认为为达到滑动距离的临界值,因此就可以认为它们不是滑动,这样做可以有更好的用户体验。其实如果细心的话,可以子啊源码中找到这个常量的定义,在 frameworks/base/core/res/res/values/config.xml 文件中,如下所示:这个 ” config_viewConfigurationTouchSlop”对应的就是这个常量的定义。
<dimen name = "config_viewConfigurationTouchSlop">8dpdimen>
速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度,它的使用过程很简单,首先,在View的onTouchEvent方法中追踪当前事件的速度;
@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getAction();
VelocityTracker obtain = VelocityTracker.obtain();
obtain.addMovement(event);
switch (action) {
case MotionEvent.ACTION_MOVE:
obtain.computeCurrentVelocity(10000);
float xVelocity = obtain.getXVelocity();
float yVelocity = obtain.getYVelocity();
Log.e("当前追踪手指滑动过程中的速度为", "X = " + xVelocity + "====== Y = " + yVelocity);
break;
case MotionEvent.ACTION_UP:
obtain.clear();
obtain.recycle();
obtain = null;
}
return super.onTouchEvent(event);
}
在这一步中有两点需要注意:第一点,获取速度之前必须先计算速度,即 getXVelocity 和 getYVelocity 这两个方法前面必须要调用computeCurrentVelocity方法;第二点,这里的速度指一段事件内手指所滑动过的像素数,比如将事件间隔设为1000ms时,在 1s 内,手指在水平方向从左向右滑过 100 像素,那么水平速度就是 100。注意速度可以为负数,当手指从右往左滑动时或手指从下往上滑动时,返回的即为负值,这个需要理解一下。速度的计算可以用如下公式来表示:
速度 = (终点位置 - 起点位置) / 时间段
根据上面的公式在加上 Android 系统的坐标系,可以知道,手指逆着坐标系的正方向滑动,所产生的速度就为负值。另外,computeCurrentVelocity这个方法的参数表示的是一个时间单元或者说时间间隔,它的单位是毫秒(ms),计算速度时得到的速度就是在这个时间间隔内手指在水平或竖直方向上所滑动的像素数。*针对上面的例子,我们如果通过obtain.computeCurrentVelocity(100)来获取速度,那么得到的速度就是手指在 100ms 内所滑动的像素数。
最后,当不需要使用它的时候,需要调用clear方法来重置并回收内存;
obtain.clear();
obtain.recycle();
obtain = null;
更多关于VelocityTracker的使用,请参照这篇博客或自行百度 博客地址:手势事件:滑动动速度跟踪类VelocityTracker介绍
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为,要使用GestureDetector也不复杂,有兴趣的可以参考下面几篇博客来学习GestureDetector的使用:
弹性滑动对象,用于实现View的弹性滑动。我们知道,当使用View的scrollTo/scrollBy 方法来进行滑动时,其过程是瞬间完成的,这个没有过渡效果的滑动用户体验不好。这个时候就可以使用Scroller来实现有过渡效果的滑动,其过程不是瞬间完成的,而是在一定的时间间隔内完成的。Scroller本身无法让View弹性滑动,它需要和View的computeScroll方法配合使用才能共同完成这个功能。那么如何使用Scroller呢?它的典型代码是固定的,如下所示:至于它为什么能实现弹性滑动,我们在后面的View的滑动中会进行详细阐述。
/**
* 缓慢滚动到指定位置
* @param destX 指定滚动到的X轴位置
* @param destY 指定滚动到的Y轴位置
*/
private void smoothScrollTo(int destX, int destY) {
//获取当前滚动的距离
int scrollX = getScrollX();
//获取需要滚动的偏移量
int delta = destX - scrollX;
//设置1000ms内滚动到delta位置,而效果就是慢慢滑动
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
/**
* 持续滚动,实现慢慢滑动
*/
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
能够实现View滑动的方法有很多,下面我们一一介绍:
为了实现View的滑动,View提供了专门的方法来实现这个功能,那就是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);
}
从上面的源码可以看出,scrollBy实际上也是调用了scrollTo方法,它实现了基于当前位置的相对滑动,而 scrollTo 则实现了基于所传递参数的绝对滑动,这个不难理解。利用 scrollTo 和 scrollBy 来实现View的滑动,这不是一件困难的事,但是我们要明白滑动过程中View内部的两个属性 mScrollX 和 mScrollY 的改变规则,这两个属性可以通过 getScrollX 和 getScrollY 方法分别得到。这里要简要概况一下:在滑动过程中,mScrollX 的值总是等于 View的左边缘 和 View内容左边缘在水平方向的距离,而 mScrollY 的值总是等于View的上边缘和View 的内容的上边缘在竖直方向的距离。View 边缘是指View的位置,由四个顶点组成,而View内容边缘是指View中的内容的边缘,scrollTo 和 scrollBy 只能改变View内容的位置而不能改变View在布局中的位置。 mScrollX 和 mScrollY 的单位为像素,并且当View左边缘在View内容左边缘的右边时,mScrollX 为正值,反之为负值;当View的上边缘在View内容上边缘的下边时,mScrollY 为正值,反之为负值。话句话说,如果从左向右滑动,那么mScrollX 为负值,反之为正值;如果从上往下滑动,那么 mScrollY 为负值,反之为正值;
为了更好地理解这个问题,下面举个例子,如下面几幅图所示:在图中假设水平和竖直方向的滑动距离都为 100像素,针对图中各种滑动情况,都给出了对应的 mScrollX 和 mScrollY 的值,根据上面的分析,可以知道,使用 scrollTo 和scrollBy 来实现View的滑动,只能将View的内容进行移动,并不能将View本身进行移动,也就是说,不管怎么滑动,也不可能将当前View滑动到附近View所在的区域。
更多关于 scrollTo/scrollBy方法的文章请参考一下链接:
上面我们已经介绍了采用 scrollTo/scrollBy 来实现View的滑动,下面我们来说下另外一种实现滑动的方式——使用动画。通过动画我们能够让一个View进行平移,而平移就是一种滑动。使用动画来移动View,主要是操作View的 translationX 和 translationY 属性,既可以采用传统的View动画,也可以采用属性动画,如果采用属性动画的话,为了能够兼容 3.0以下的版本,需要采用开源动画库 nineoldandroids (http://nineoldandroids.com/)
采用View动画的代码,如下所示。此动画可以在100ms内将一个View从原始位置向右下角移动100个像素。
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:zAdjustment="normal">
<translate
android:duration="100"
android:fromXDelta="0"
android:fromYDelta="0"
android:interpolator="@android:anim/linear_interpolator"
android:toXDelta="100"
android:toYDelta="100">translate>
set>
如果采用属性动画的话,那就更简单了,以下代码可以将一个View在100ms内从原始位置上向右平移100像素。
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
上面给出的两个例子就是使用动画使View滑动的方法,具体关于动画相关的知识我们在后面会另开一篇来讲。
在这里我们将介绍第三种实现View滑动的方法,那就是改变布局参数,即改变 LayoutParams。这个就比较好理解了,比如我们想把一个 Button 向右平移100px,我们只需要将这个 Button 的 LayoutParams 里的 marginLeft 参数的值增加 100px 即可,是不是很简单呢?还有一种情形,为了达到移动 Button 的目的,我们还可以在 Button 的左边放置一个空的 View,这个空 View 的默认宽度为 0,当我们需要向右滑动 Button 时,只需要重新设置空 View 的宽度即可,当空 View 的宽度增大时(假设 Button 的父容器是水平方向的 LinearLayout),Button 就会被自动挤向右边,这样也间接的实现了 View 向右平移的效果。如何重新设置一个 View 的 LayoutParams 呢? 很简单, 如下所示:
scrollTo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) scrollTo.getLayoutParams();
layoutParams.leftMargin +=100;
// scrollTo.requestLayout();
scrollTo.setLayoutParams(layoutParams);
}
});
通过改变LayoutParams的方式去实现View的滑动同样是一种很灵活的方法,需要根据不同情况去做不同的处理。
我们知道,在 View 进行绘制时,会调用 onLayout() 方法来设置显示的位置。同样我们可以通过修改 View 的 left、top、right、bottom 四个属性来控制 View 的坐标。这里我们就不演示了,这种方式基本不常用,知道就行。
先看 scrollTo/scrollBy 这种方式,它是 View 提供的原生方法,其作用是专门用于 View 的滑动,他可以比较方便地实现滑动效果并且不影响内部元素的单击事件,但是它的缺点也是很显然的:它只能滑动View的内容,并不能滑动View本身。
再看动画,通过动画的方式来实现 View 的滑动,这要分为两种情况,如果是 Android 3.0 以上并且采用的是属性动画,那么采用这种方式没有明显缺点;如果是使用 View 动画或者在 Android 3.0之前使用属性动画,均不能改变View本身的属性。在实际使用中,如果动画元素不需要响应用户的交互,那么使用动画来做滑动是比较合适的,否则就不太合适。但是动画有一个很明显的优点,那就是一些复杂的效果必须通过动画才能实现。
最后再看一下改变布局这种方式,它除了使用起来比较麻烦点以外,也没有明显的缺点,它的主要使用对象是一些具有交互性的 View,因为这些 View 需要和用户交互,直接通过动画去实现会有问题。
针对上面的分析下面来做一下总结,如下所示:
知道了 View 的滑动,我们还要知道如何实现 View 的弹性滑动,比较生硬地滑动过去,这种方式的用户体验是在太差了,因此我们要实现渐进式滑动,那么如何实现弹性滑动呢?其实实现方法有很多,但是它们都有一个共同思想:将一次大的滑动分成若干个小的滑动并在一个时间段内完成,弹性滑动的具体实现方式有很多,比如通过 Scroller、Handler#postDelayed 以及 Thread#sleep 等,下面一一进行介绍。
前面我们已经对 Scroller的使用方法进行了简单的介绍,下面我们来分析一下它的源码,从而探究为什么它可以实现 View 的弹性滑动。
/**
* 缓慢滚动到指定位置
* @param destX 指定滚动到的X轴位置
* @param destY 指定滚动到的Y轴位置
*/
private void smoothScrollTo(int destX, int destY) {
//获取当前滚动的距离
int scrollX = getScrollX();
//获取需要滚动的偏移量
int delta = destX - scrollX;
//设置1000ms内滚动到delta位置,而效果就是慢慢滑动
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
/**
* 持续滚动,实现慢慢滑动
*/
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
上面是 Scroller 的典型用法,这里先描述它的工作的原理:当我们构造一个 Scroller 对象并且调用它的 startScroll 方法时, Scroller内部其实什么也没做,它只是保存了我们传递的几个参数,这几个参数从 startScroll 的原型上就可以看出来:如下所示:
/**
* Start scrolling by providing a starting point and the distance to travel.
* The scroll will use the default value of 250 milliseconds for the
* duration.
*
* @param startX Starting horizontal scroll offset in pixels. Positive
* numbers will scroll the content to the left.
* @param startY Starting vertical scroll offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel. Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel. Positive numbers will scroll the
* content up.
*/
public void startScroll(int startX, int startY, int dx, int dy) {
startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
}
/**
* Start scrolling by providing a starting point, the distance to travel,
* and the duration of the scroll.
*
* @param startX Starting horizontal scroll offset in pixels. Positive
* numbers will scroll the content to the left.
* @param startY Starting vertical scroll offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel. Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel. Positive numbers will scroll the
* content up.
* @param duration Duration of the scroll in milliseconds.
*/
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
这个方法的含义很清楚,startX 和 startY 表示的是滑动的起点, dx 和 dy 表示的是要滑动的距离,而 duration 表示的是滑动时间,即这个滑动过程完成所需要的时间,注意这里的滑动是指 View 内容的滑动而非 View 本身位置的改变。 可以看到,仅仅调用 startScroll方式是无法让 View 滑动的,因为它内部并没有做滑动相关的事,那么 Scroller到底是如何让 View 弹性滑动的呢? 答案就是 startScroll 方法下面的 invalidate 方法,虽然有点不可思议,但是的确是这样的。 invalidate 方法会导致 View 重绘,在 View 的 draw 方法中又会去调用 computeScroll 方法, computeScroll 方法在 View 中是一个空实现,因此需要我们自己去实现,上面的代码已经实现了 computeScroll 方法。正是因为这个 computeScroll方法, View 才能实现弹性滑动。这看起来还是很抽象,其实是这样的:当 View 重绘后会在 draw 方法中调用 computeScroll 方法,而 computeScroll 方法又会去向 Scroller获取当前的 scrollX 和 scrollY(mScroller.getCurrX() 和 mScroller.getCurrY());然后通过调用 scrollTo 方法实现滑动;接着又调用 postInvalidate 方法来进行第二次重绘,这一次重绘的过程和第一次重绘一样,还是会导致 computeScroll 方法被调用;然后继续向 Scroller获取当前的 scrollX 和 scrollY(mScroller.getCurrX() 和 mScroller.getCurrY()),并通过 scrollTo 方法滑动到新的位置,如此反复,直到整个滑动过程结束。
下面我们就来看一下 Scroller 的 computeScrollOffset 方法的实现,如下所示:
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
...
return true;
}
看到这里是不是突然明白了什么?这个方法会根据时间的流逝来计算出当前的 scrollX 和 scrollY 的值。计算方法也很简单,大意就是根据时间流逝的百分比来算出 scrollX 和 scrollY 改变的百分比并计算出当前的值,这个过程类似于动画中的插值器的概念,这里我们先不去深究这个具体过程。这个方法的返回值也很重要,它返回 true 表示滑动还未结束, false 则表示滑动已经结束,因此当这个方法返回 true 时,我们要继续进行 View 的滑动。
通过上面的分析,我们应该明白了 Scroller 的工作原理了,这里做一下概括: Scroller本身并不能实现 View 的滑动,它需要配合 View 的computeScroll 方法才能完成弹性滑动的效果,它不断地让 View 重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔 Scroller 就可以得出 View 当前的滑动位置,知道了滑动位置就可以通过 scrollTo 方法来完成 View 的滑动。就这样,View 的每一次重绘都会导致 View 进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,这就是 Scroller 的工作机制。由此可见, Scroller 的设计思想是多么的值的让人称赞,这个过程中它对 View 没有丝毫的引用,甚至在它内部来计时器都没有。
动画本身就是一个渐进的过程,因此通过它来实现的滑动天然就具备弹性效果,比如一下代码可以让一个 View 在 100ms 内向右移动 100像素。
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
不过这里想说的并不是这个问题,我们可以利用动画的特性来实现一些动画不能实现的效果。还拿 scrollTo 来说,我们也想模仿 Scroller 来实现 View 的弹性滑动,那么利用动画的特性,我们可以采用如下方式来实现:
...
scrollTo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//TODO 弹性滑动——测试弹性滑动——通过动画的方式实现弹性滑动
final int deltaX = 150;
ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
mArea.scrollTo((int) (deltaX * fraction), 0);
}
});
animator.start();
}
});
在上述代码中,我们的动画本质上没有作用于任何对象上,它只是在 1000ms 内完成了整个动画过程。利用这个特性,我们就可以在动画的每一帧到来时获取动画完成的比例,然后在根据这个比例计算出当前 View 所要滑动的距离。注意,这里的滑动针对的是 View 的内容而非 View 本身。 可以发现,这个方法的思想其实和 Scroller 比较类似,都是通过改变一个百分比配合 scrollTo 方法来完成 View 的滑动。需要说明一点,采用这种方法除了能够完成弹性滑动以外,还可以实现其他动画效果,我们完全可以在 onAnimationUpdate 方法中加上我们想要的其他操作。
延时策略的核心思想是通过发送一系列延时消息从而达到一种渐进式的效果,具体来说可以使用 Handler 或 View 的 postDelayed 方法,也可以使用线程池的 sleep 方法。对于 postDelayed 方法来说,我们可以通过它来延时发送一个消息,然后在消息中来进行 View 的滑动,如果接连不断地发送这种延时消息,那么就可以实现弹性滑动的效果。对于 sleep 方法来说,通过在 while 循环中不断地滑动 View 和 sleep,就可以实现弹性滑动效果。
下面演示采用 Handler 来完成弹性滑动,其他方法请自行尝试,大致思想是一样的。下面的代码大约在 1000ms(这个时间不能确定,只是一个大概值),代码比较简单,就不再详细介绍了,之所以说大约 1000ms,是因为采用这种方式无法精确地定时,原因是系统的消息调用也是需要时间的,并且所需时间不定。
//TODO 弹性动画——使用延时策略——所需要参数
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELAYED_TIME = 33;
private int mCount = 0;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_SCROLL_TO:
mCount++;
if (mCount <= FRAME_COUNT) {
float fraction = mCount / (float) FRAME_COUNT;
int x = (int) (fraction * 100);
mArea.scrollTo(x, 0);
mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
}
break;
}
}
};
scrollTo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//TODO 弹性滑动——使用延时策略
mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
}
});