View的事件体系
View的基础知识
View的位置参数
一个View
的位置主要由四个顶点构成, 或者可以就是两个点就可以确定. 分别为左上点,右下角每个点都对应x,y两个属性. 因为默认都是矩形, 所以两个点就可以确定.
一个View
的大小可以利用四个属性可知. 分别对应getLeft()
,getRight()
,getTop()
,getBottom
系统提供的函数.
- 一个控件的宽: getRight() - getLeft()
- 一个控件的高: getTop() - getBottom()
在Android3.0中, View增加了几个属性:x
, y
, translationX
, translationY
-
x
,y
: 表示View的左上角坐标点(最终坐标点). -
translationX
,translationY
: 表示View的左上点相对于父容器的偏移量(默认是0).
而这些参数的换算关系为:
x = left + translationX;
y = top + translationY;
MotionEvent和TouchSlop
MotionEvent是指手指在接触屏幕之后产生的一系列事件
最常见事件类型是ACTION_DOWN
,ACTION_MOVE
,ACTION_UP
一次事件可以有不同的持续时间, 和不同的事件类型. 例如
- 按下抬起 : DOWN –> UP
- 按下移动抬起 : DOWN -> MOVE -> MOVE -> … ->UP
- ….
而在移动时可以根据MotionEvent
提供的参数获对应的xy取值.
*getX/getY
: 返回相对于当前View左上角的x,y坐标.
getRawX/getRawY
: 返回的是针对整个屏幕的左上角的x,y坐标.
TouchSlop是系统可以识别的最小滑动距离单位
只有手指两次滑动大于这个TouchSlop
,系统才认为是滑动.
ViewConfiguration.get(getContent).getSealedTouchSlop()
可以获得这个系统值默认8dp.
用途: 在自定义的时候, 可以参考系统的默认值, 来作为实际的滑动定义.
VelocityTracker GestureDetector
VelocityTracker 速度追踪
用于追踪手指在滑动过程中的速度,包括水平和数值方向的速度
使用方式: 在View的OnTouchEvent方法中:
//获得速度追踪对象
VelocityTracker velocity = VelocityTracker.obtain();
velocity.addMovement(event);
//计算速度 并获取计算值
velocity.computeCurrentVelocity(1000); //设定一个时间间隔值
float xVelocity = velocity.getXVelocity();
float yVelocity = velocity.getYVelocity();
必须要先计算并设定计算速度的时间单元值,才可以获得速率.
公式: 速度 = (终点位置 - 起点位置) / 时间间隔值
可以看到, 计算的速度是根据我们自己添加的时间间隔值计算的. 并且速度可以为负值,如果向左滑动.
当不需要的时候, 调用clear()
重置并回收内存.
velocity.clear();
velocity.recycle();
GestureDetector 手势检测
用于辅助检测用户的单击, 滑动, 长按, 双击等行为.
使用如下
创建GestureDetector
对象并实现OnGestureDetector
接口.
GestureDetector mGestureDetector = new GestureDetector(this);
// 解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);
然后接管目标View的onTouchEvent()
方法. 在onTouchEvent()
方法中
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
然后根据需求可以选择性的实现OnGestureListener
和OnDoubleTapListener
接口
接口的方法说明:
方法名 | 描述 | 所属接口 |
---|---|---|
onDown |
按下 | OnGestureListener |
onShowPress |
按下 但是未松开或者拖动.强调状态 | OnGestureListener |
onSingleTapUp |
抬起 表示单击行为, 双击中也会触发 | OnGestureListener |
onScroll |
按下并拖动 拖动行为 | OnGestureListener |
onLongPress |
长按 | OnGestureListener |
onFling |
按下屏幕病快速滑动后松开 | OnGestureListener |
onDoubleTap |
双击,两次连续单击组成, 与onSingleTapConfirmed无法共存 | OnDoubleTapListener |
onSingleTapConfirmed |
严格意义上的单击 双击中的单击无法触发 | OnDoubleTapListener |
onDoubleTapEvent |
表示发生了行为 | OnDoubleTapListener |
实际开发中:根据喜好来使用. 即使不使用GestureDetector
辅助手势检测类,一样可以实现.
建议: 如果要监听双击这种行为就是用此类.
Scroller 弹性滑动对象
用于实现View的弹性滑动.
在开发中, 当需要把View从一个点移动到另一个点的时候. 如果使用scrollTo/scrollBy
进行滑动时, 都是瞬间完成. 没有过度动画, 给用户感觉很生硬. 使用Scroller
可以实现有过渡的滑动.Scroller本身无法让View弹性滑动, 需要和View的computerScroll
进行配合使用.
下面会说到
View的滑动
实现滑动的方式有三种:
- 通过View本身的
scrollTo/scrollBy
方法实现滑动 - 通过动画给View施加平移效果来实现动画
- 通过改变View的
LayoutParams
使View重新布局实现滑动
scrollTo/scrollBy
首先要明确一点: 这两个方法只能改变View的内容位置,而不能改变View本身在布局中的位置
而且方法中都是以像素值来进行移动的.
- scrollTo: 针对当前View的绝对位置进行移动.
- scrollBy: 根据当前View的内容值进行相对位置移动.
看一下scrollBy
的源码调用
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
其实本质上scrollBy
调用了scrollTo
方法
而mScrollX/mScrollY
是什么? 这个就是当前View的内容 与这个View实际布局位置(原始位置)的差值.
而当前View内容这个东西就是让用户看到的效果发生改变. 但是如果这个View可以被点击. 那么能触发点击的位置是View的实际所在布局位置. 而不是View的内容显示的位置.
使用动画
使用动画来对View进行移动,主要就是操作View的translationX/translationY属性
可以使用普通动画和属性动画.
普通动画是对View进行影像的移动. 可以通过设置fillAfter=true
,来让影像在动画结束时候保留最终结果.而不是还原到起始位置.
而属性动画会对真实位置也进行改变.
ObjectAnimator.ofFloat(tagerView,"translationX",0,100).setDuration(100).start()
改变布局参数"
这个比较简单, 获得View的LayoutParams
参数.进行修改,改好之后再赋值回去.
MarginLayoutParams params = (MarginLayoutParams)mTextView.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
mTextView.requestLayout();
//或者mTextview.setLayoutParams(params);
关于这三种方式的简单总结
- scrollTo/scrollBy: 操作简单, 适合对View内容的滑动
- 动画: 操作简单,主要适用于没有交互的View和实现复杂的动画效果.
- 修改布局参数: 操作稍微复杂,适用于有交互的View.
弹性滑动
使用Scroller
一个简单的使用方法如下:
Scroller mScroller = new Scroller(mContent);
// 封装一个方法, 接收要移动到的目标点 x和y
private void smoothScrollTo(int destX, int destY){
int scrollX = getScrollX();
int deltaX = destX - scrollX;
// 1000ms内逐渐滑向destX
mScroller.startScroll(scrollX, 0, deltaX, 0, 1000);
invalidate();
}
//复写View的computeScroll方法
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate()
}
}
源码中Scroller类中startScroll()
方法,其实没有实际操作什么,只是保存了调用方法时,传递的几个参数. 如: 开始结束点,时间等. 那动画究竟是怎么实现的? 复写的computeScroll()
又有什么用?
流程顺序这样的: 当调用了startScroll()
系统只是保存了一些信息, 但是下面调用invalidate()
. 这个方法都知道是会导致View的重绘, 在View的draw()
方法中又会去调用computeScroll()
方法,本身computeScroll()
是一个空实现,但是这里进行了复写. 而这个方法我们复写的时候调用了scrollTo()
方法! ok这样View就会真正的移动了! 但是还有一点这次滚动只是整个滚动事件的一个小部分,后续的怎么触发的? 就是下面又调用了postInvalidate()
, 又会重新绘制重新调用computeScroll()这个复写过的空实现方法.
而Scroller类中的computeScrollOffset()
可以直接返回这个滚动的动作是否全部完成. 源码实现思路就是根据时间的流逝的百分比来计算出当前ScrollX和ScrollY的值.
// 核心代码 x就是时间流逝的百分比
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
小结Scroller的工作原理:
Scroller
本身不可以实现滑动, 需要和View的ComputeScroll()
配合使用来完成弹性滑动. 通过不断的在computeScroll()
调用View的重绘方法. 每次绘制时候的当前时间与开始时间的时间差与设定的执行动画时间的百分比,算出每一次需要scroll到的坐标点, 然后通过调用scrollTo()来实现每一次的小滚动效果. 通过一连串的滚动达到了平滑的效果. 这就是Scroller
工作机制. 完全实现了解耦操作. 这个过程没有任何一处对View进行引用,甚至连内部计时器都没有.
补充:
1、top、left、right、bottom的值,是在view的onLayout的时候确定。
2、scrollView只在绘制的时候onLayout,在滚动的时候不会再次出发onLayout,所以对于子View的top、left、right、bottom是没有影响的、
3、listView自身有回收机制,所以在滚动的时候需要时刻去检测item是否已经滚动出了屏幕,这样就需要重新测量子view的位置,所以就直接影响了item的top、left、right、bottom。
通过动画
可以直接使用ObjectAnimator.ofFloat(tagerView,"translationX",0,100).setDuration(100).start()
也可以利用动画的特性, 实现与Scroller原理近似的方法.
final int startX = 100;
final int endX = 200;
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 1).setDuration(1000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int offset = (int) animation.getAnimatedFraction();
mTextview.scrollTo(startX+offset, 0);
}
});
让系统算出每个时间片我们需要移动的距离, 并回调给我们.让我们自己实现. 如果是一组动画在相同的时间执行的绝对值相同我们就可以在onAnimationUpdate()
一起进行调用.
使用延时策略
核心思想就是通过发送一些列延时消息从而达到一种渐进的效果.
可以使用: Handler
, View的postDelayed()
方法, 或者线程的sleep()
参看文章
《Android 开发艺术探索》书集
《Android 开发艺术探索》 03-View的事件体系