读书笔记|艺术探索第三章-View事件体系
这两天的面试,我发现很多东西我并没有了解其机制,只是停留在表面。
1. View位置参数
参数 | 含义 | 获取方式 |
---|---|---|
(mLeft , mTop) (mRight , mBottom) | 左上角点和右下角点相对于父容器的坐标 | getXXX() |
x,y | 可视View左上角的位置 | getX/getY |
translationX,translationY | 可视View相对于视图本体的偏移量 | getTranslationX/Y |
mScrollX/Y | 内容的相对于原始的偏移量 | getScrollX/Y |
-
Left,Top,Right,Buttom 很好理解。
-
实际上View的类中并没有X,Y变量(我找了好久
(=
)。getX和getY实际上是:
可见源码对x的解释是视图的可视x位置,以像素为单位。。 -
那啥是
translationX
呢?这个属性和动画有关。在使用setTranslationX改变了View的位置但是没有改变LayoutParams里的margin属性。可以理解为translateX/Y和margin是同一级别的。
- 这个用Button举例子,默认scrollX/Y为0,文字是居中。当设置scrollX为正数的时候,文字会在Button中向左边移动。文字就是Button中的内容。
2. View的滑动
2.1 使用scrollTo/scrollBy。
根据源码可见:(1)scrollBy实际上是调用scrollTo的。(2)scrollTo实际上是修改了mScrollX和mScrollY。而这两个参数表示内容的偏移量。不管怎么移动,文字都不会溢出Button。
所以可以得出结论这种移动只能改变View的内容。并且mScrollX>0 内容向左滑动。mScrollY>0 内容向上滑动。
2.2 使用动画
动画实际上是操作translationXY,Scale等。View动画不能改变监听器的位置。Android 3.0 以下无法使用属性动画。属性动画解决了监听器不随视图变化的问题。
具体在第七章读书笔记再学习。
ObjectAnimator.ofFloat(button1, "translationX", 0, 500).setDuration(10000).start();
2.3 改变布局参数
可以通过修改布局参数中的Margin来实现滑动。
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)button2.getLayoutParams();
params.leftMargin += 1;
button2.requestLayout();
3.弹性滑动
3.1 使用Scroll
public void smoothScrollTo(int dx, int dy) {
scroller.startScroll(getScrollX(), 0, dx, dy, 9000); // 记录开始时间,设置标记为滑动
invalidate(); // 重绘之后调用draw,
}
@Override
public void computeScroll() {
// 计算是否滑动到目的地,如果没有,修改mScrollX 和 mScrollY
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY()); // 真正的滑动
postInvalidate(); //重绘
}
}
滑动的入口是方法smoothScrollTo,这是自己再View内部定义的方法。可以看到首先调用了scroller.startScroll(....)。查看源码。
可见startScroll方法主要是初始化一些值,并没有做关于滑动的操作。注意其设置了mMode为滑动,记录了当前时间,计算出了结束时间。然后调用invalidate()。看了一下介绍,这个方法是导致当前View无效,然后会重新绘制,也就是调用Draw。
draw方法比较长,确实调用了computerScroll()方法。
View的computerScroll()是一个空实现,我们重写一下。
@Override
public void computeScroll() {
// 计算是否滑动到目的地,如果没有,修改mScrollX 和 mScrollY
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY()); // 真正的滑动
postInvalidate(); //重绘
}
}
首先看看scroller.computeScrollOffset()
第一步检查标志位,这个我们再startScroll中设置为了false,表示没有滑动结束。
然后计算timePassed,mStartTime我们再startScroll中也初始化了,从开始准备滑动到现在过了多少毫秒,如果time<mDuration,显然滑动还没有结束。那么要滑动到哪儿呢?可见mStartX+Math.round(x*mDeltaX),没错是通过时间过去的百分比计算出路程,然后加上初始值,计算出目的地。最后返回true。
再后来就是调用
scrollTo
了完成正真的滑动。可见修改了mScrollX和mScrollY
,这两个值在一开始就说了,是内容相对于最开始的偏移量。然后调用postInvalidate();
进行重绘,然后又是draw,再次计算时间流逝比,计算路程滑动....总结一下:
(1)mScroll.startScroll,计算开始时间,设置标志位,计算目的地
(2)invalidate,导致视图重绘,从而调用draw
(3)draw重绘的时候,调用了computeScroll方法
(4)computeScroll由调用computerScrollOffset方法,这个方法返回boolean,返回false,表示没有滑动结束,其中计算了 时间流逝比,再通过时间流逝比,计算出等价的路程。
(5) 最后通过
scrollTo
完成滑动。如此循环。
还有一些细节没有展开。
3.2 通过动画
属性动画,移动View本身。
ObjectAnimator.ofFloat(button1, "translationX", 0, 500).setDuration(10000).start();
我们可以通过动画的性质来移动内容。原理很简短,就是利用ValueAnimator得到一个类似于时间流逝比的比值,再用scrollTo来更新视图。从而达到动画的效果。
final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
button1.scrollTo((int) (startX + deltaX * fraction), 0);
}
});
3.3 使用延迟策略
其实也很简单,如果对消息机制比较熟悉的话。
handler.sendEmptyMessageDelayed(1, 100);
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
params.leftMargin += 1;
button2.requestLayout();
count++;
if (count > 200) return;
handler.sendEmptyMessageDelayed(1, 10);
}
};
4. 事件分发
-
public boolean dispatchTouchEvent(MotionEvent ev)
:是否将事件传递给下一级,返回的结果受下一级的dispatchTouchEvent()
和当前的onTouchEvent()
-
public boolean onInterceptTouchEvent(MotionEvent ev)
:在上面函数内部调用,当前View是否拦截事件,返回true表示拦截,返回false表示不拦截。 -
public boolean onTouchEvent(MotionEvent ev)
:用来处理事件,返回true表示消耗,返回false表示不消耗。
用《进阶之光》上的例子就是:
敌人来挑战武当山,武当山上现在有掌门张三丰,武当七侠宋远桥,武当弟子宋青书。张三丰肯定不会直接出动(onInterceptTouchEvent == false)
,而是让宋远桥去(child.dispathcTouchEvent(ev))
,宋远桥(ViewGroup)
也威名远扬,也不会轻易应战(onInterceptTouchEvent == false)
,派遣宋青书(child.dispathchTouchEvent(ev)
,宋青书(底层的View)
没有徒弟了(没有child了)
,只好自己去迎战... 这就是事件的从上向下传递。
结果挑战的是成昆,宋青书处理不掉他(onTouchEvent = false)
,于是叫宋远桥来,结果宋远桥也不是对手(onTouchEvent = false)
,于是张三丰只好亲自出马(调用 onTouchEvent())
。
5. 滑动冲突
- 左右嵌套上下滑动冲突,根据滑动的左右分量和上下分量的大小来解决。
- 左右嵌套左右滑动冲突,根据具体的业务来解决。在实际开发中可以判断内存View是否滑动到了尽头,如果滑动到了尽头再滑动外层View,否则外层View不动。