本文内容参考于《android开发艺术探索》,作为分析总结。
1、一些基础
1.1、理解View的位置参数
看下面这张图:
那么:
width = right(getRight) - left(getLeft)
height = bottom(getBottom) - top(getTop)
比较注意的是:activity的生命周期中view是在绘制状态,所以getRight、getWidth...这些方法获取的是0,具体解决办法可自行研究。而且这几个参数都是相对于父容器的。
从Android3.0开始,View增加了x、y、translationX、translationY。其中x和y是View左上角的坐标,translationX和translationY是View左上角相对于父容器的偏移量,默认值为0。这几个参数也是相对于父容器。那么:
x=left+translationX;
y=top+translationY;
需要注意的是,View平移过程中,top、left表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是x、y、translationX、translationY。
1.2、MotionEvent和TouchSlop
1. MotionEvent
典型的几种事件类型:
- ACTION_DOWN--刚接触屏幕
- ACTION_MOVE--屏幕上移动
- ACTION_UP--屏幕上松开的一瞬间
典型的一系列事件:
- 点击屏幕后离开松开,事件序列:DOWN->UP
- 点击屏幕滑动一会再松开,事件序列:DOWN->MOVE->...->MOVE->UP
在序列事件中可以通过MotionEvent对象得到点击事件发生的x和y坐标。例如:
event.getX();
event.getRawX();
这和View的getX是不同的,再者就是event.getX()获取的是相当于当前View左上角的x坐标,event.getRawX()返回的是相对于手机屏幕左上角的x坐标。
2. TouchSlop
TouchSlop是系统所能够识别出的被认为是滑动的最小距离,换句话说,当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。获取这个常量方式:
ViewConfiguration.get(this).getScaledDoubleTapSlop();
1.3、VelocityTracker、GestureDetector和Scroller
1. VelocityTracker
速度追踪,用于追踪手指在滑动工程中的速度,包括水平和竖直方向的速度(有正负之分,即速度是有方向的):
private VelocityTracker mVelocityTracker = null;
@Override
public boolean onTouchEvent(MotionEvent event) {
int index = event.getActionIndex();
int pointerId = event.getPointerId(index);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
if (mVelocityTracker == null) {
//获取VelocityTracker对象
mVelocityTracker = VelocityTracker.obtain();
} else {
mVelocityTracker.clear();//清除加入的event
}
//加入event
mVelocityTracker.addMovement(event);
break;
case MotionEvent.ACTION_MOVE:
mVelocityTracker.addMovement(event);
//1000代表是1s,即单位时间
mVelocityTracker.computeCurrentVelocity(1000);
//获取x、y速度
Loger.e("ACTION_MOVE:" + VelocityTrackerCompat.getXVelocity(mVelocityTracker,
pointerId) + "-" + VelocityTrackerCompat.getYVelocity(mVelocityTracker,
pointerId));
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//回收
mVelocityTracker.recycle();
break;
}
return true;
}
使用较为简单,不复杂。
2. GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。使用较为简单,可参考官方文档:https://developer.android.com/training/gestures/detector.html
这里说几个比较常用的:
- onSingleTapUp,单击
- onFiling,快速滑动
- onScroll,拖动
- onLongPress,长按
- onDoubleTap,双击
但是实际开发中,GestureDetector并非必须用,可以自己在View的OnTouchEvent方法中实现所需的监听。比如监听滑动相关的,建议在onTouchEvent中实现,像双击这种可以使用GestureDetector。
3. Scroller
弹性滑动对象,用于实现View的弹性滑动。View的scrollTo/scrollBy滑动时瞬间完成,效果不好。所就要用Scroller来实现有过度效果的滑动。Scroller本身无法让View弹性滑动,它需要和View的computeScroll方法配合使用才能共同完成这个功能。下面是一个通用模板:
private Scroller mScroller = new Scroller(mContext);
private void scroll(int x, int y) {
int scrollX = getScrollX();
int delta = x - scrollX;
//1000ms的移动
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
2、View的滑动
一般View的滑动有下面三种方式:通过View本身提供的scrollTo/scrollBy方法,通过动画给View施加平移效果来实现滑动,通过改变View的LayoutParams使得View重新布局从而实现滑动。
2.1、使用scrollTo/scrollBy
首先需要名曲的是,scrollTo/scrollBy滑动是对view的内容进行移动,而非view本身。比如对于ViewGroup来讲,里面的组件都归宿于他的内容。
下面是scrollTo/scrollBy源码:
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();
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
源码中可以看到scrollBy内部调用的也是scrollTo方法。里面有mScrollX 和mScrollY 参数,这两个参数的意思是view的边和view的内容的距离。mScrollX 值总是等于view左边缘和view内容左边缘在水平方向的距离,mScrollY的值总是等于View上边缘和View内容边缘在竖直的距离。这个值是有正负之分的,比如view左边缘在view内容左边缘右边时,那么mScrollX 为正,反之为负。
2.2、使用动画
使用动画主要是操作View的translationX和translationY属性。可以使用传统View动画,也可以采用属性动画(支持3.0以上,3.0以下采用开源动画库nineoldandroids:
http://nineoldandroids.com/)
比如采用属性动画完成1s内从原始位置向右平移100像素:
ObjectAnimator.ofFloat(mBtn, "translationX", 0, 100)
.setDuration(1000)
.start();
建议使用属性动画,动画有些bug,比如移动后不能点击,属性动画没有这个问题。
2.3、改变布局参数
这个就很简单了。比如,一个button要右移,改变LayoutParams里的marginLeft即可,或者左边放一个空的view也可以。
2.4、各种滑动方式对比
- scrollTo/scrollBy,内容滑动,简单
- 动画,复杂的动画效果,简单
- 改变布局参数,复杂
3、弹性滑动
弹性滑动原理是吧一次大的滑动分成若干次小的滑动并在一个时间内完成。实现方式比如:通过Scroller、Handler和postDelayed、Thread和sleep等。
3.1、使用Scroller
首先要注意的是,Scroller的滑动时内容的滑动。在1.3的Scroller介绍中有一个典型使用:
private Scroller mScroller = new Scroller(mContext);
private void scroll(int x, int y) {
int scrollX = getScrollX();
int delta = x - scrollX;
//1000ms的移动
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
这里解释下为什么能够实现弹性滑动。Scroller调用startSroll开始移动,但是实际上startScroll方法并没有执行移动,只是对数据进行了存储:
传入的这几个参数,startX和startY表示滑动的起点,dx和dy表示的是要滑动的距离,而duration表示是滑动时间。这里的滑动是指View内容的滑动而非View本身位置的改变。再者就是能够滑动的原因是因为:invalidate这个方法,这个方法调用会导致View重绘,重绘调用view的draw方法,draw方法又会调用computeScroll方法。这时候因为重新了computeScroll方法,方法中又会去向scroller获取当前的scrollX和scrollY,然后通过scrollTo方法实现滑动;接着又调用postInvalidate方法进行下次重绘;然后又走了上面的流程。invalidate和postInvalidate放的区别在于前者是UI线程中调用,后者是非线程中调用。
再来说下scroller的computeScrollOffset方法。主要看源码的这一部分:
这个方法的意思主要是采用时间流逝计算当前的scrollX和scrollY。这样就可以做到弹性滑动。
3.2、通过动画
动画本身就是渐进过程,所以天然弹性效果。比如下面这个代码:
ObjectAnimator.ofFloat(view, "translationX", 0, 100)
.setDuration(1000)
.start();
这个可以让一个View在100s内向右移动100像素。
那么如果想模仿Scroller来实现View的弹性滑动呢?这里也可以利用动画的特性来实现:
final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(100);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
mBtn.scrollTo(startX + (int) (deltaX * fraction), 0);
}
});
其实你会发现,这个代码本质上并没有作用于任何对象。这里是根据动画完成的渐变获取View所要滑动的距离。实际上,这里的滑动针对的还是View的内容而非View本身。
3.3、使用延时策略
延时策略的核心是发送一系列延时消息,比如Handler、View的postDelayed、线程的sleep方法。Handler主要是通过调用sendEmptyMessageDelayed方法实现,一直这样调用本身即可。对于postDelayed方法,可以通过其来发送一个延时消息,然后再消息中进行View的滑动,如果连续不断地发送这种延时消息,那么就可以实现弹性滑动的效果。对于sleep方法来说,通过在while循环中不断地滑动View和sleep。
代码这里就不在贴出,原理和上面两种方式相同。
其实这三种方法的核心思想都是把一个完整的移动分割成一个一个小的移动,这样的话就需要借助有时间流的工具,比如computeScroll、ValueAnimator、Handler等等。