3.1 View 基础知识
View 是 Android 中所有空间的基类。
3.1 View 的位置参数
在 Android 中,x 轴和 y 轴的正方向分别为右和下。这些坐标相对于父容器来说的:top 是 view 左上角的纵坐标,left 是 view 的横坐标,right 是右下角横坐标,bottom 是右下角纵坐标;x 和 y 分别是 View 左上角的坐标;translationX 和 translationY 是 view 左上角相对于父容器的偏移量。
x = left + translationX
y = top + translationY
注意:View 在平移的过程中,top 和 left 表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是 translationX 和 translationY、x 和 y。
调用 View.requestLayout()
,直接改变了 View 的位置,并非平移,left、top会变,而 translationX、translationY 不会变。
而用属性动画的时候,left、top不会变化,translationX、x会发生变化。
2 MotionEvent 和 TouchSlop
MotionEvent:正常情况下,一次手指触摸屏幕的行为会触发一系列事件,
1:点击屏幕后离开:事件序列:DOWN –>UP
2:点击屏幕滑动一会在离开:DOWN –> MOVE –> … –> MOVE –> UP
通过MotionEvent 对象可以得到:
getX / getY:返回的是相对于当前 view 的左上角的 x 和 y 坐标
getRawX / getRawY:返回的是相对于手机屏幕左上角的 x 和 y 坐标。
TouchSlop:系统所能识别出的被认为是滑动的最小距离。这是一个常量,和设备有关,在不同的设备上这个值可能是不同的。通过代码获得该值:ViewConfiguration.get(getContext()).getScaledTouchSlop()
3 VelocityTracker、GestureDetector 和 Scroller
VelocityTracker: 用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度
注意:1. 获取速度之前必须要先计算速度。2. 这里的速度是指一段时间内手指所滑过的像素数。
GestureDetector :手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。
建议:如果只是监听滑动相关的,建议自己在onTouchEvent
中实现,如果要监听双击这种行为,那么就使用 GestureDetector。
Scroller:实现 View 的弹性滑动。
3.2 View 的滑动
常见的滑动方式有三种:
1:通过 View 本身提供的 scrollTo / scrollBy 方法来实现滑动;
2:通过动画给 View 施加平移效果来实现滑动;
3:通过改变 View 的 LayoutParams 使得 View 重新布局从而实现滑动。
1. 使用scrollTo / scrollBy
在滑动过程中,mScrollX 的值总是等于 View 的左边界和 View 内容的左边界在水平方向上的距离,而mScrollY 的值总是等于 View 的上边界和 View 内容的上边界在竖直方向上的距离。
scrollTo / scrollBy 只能改变 View 内容的位置而不能改变 View 控件在布局中的位置。
mScrollX 和 mScrollY 的单位是像素,当 View 左边缘的位置在 View 内容左边缘的右边的时候,mScrollX 为正值,反之为负;当 View 上边缘的位置在 View 内容上边缘的下边的时候,mScrollX 为正值,反之为负。
2. 使用动画
通过动画能够让一个 View 进行平移,而平移是一种滑动,使用动画来移动 View,主要是操作 View 的 translationX 和 translationY。既可以采用传统的 View 动画,也可以采用属性动画。属性动画为了兼容3.0以下的版本,需要采用开源动画库 nineoldandroids。
View 动画是对 View 的影像做操作,不能真正改变 View 的位置参数,包括宽和高。动画后的动画结果会消失,除非设置 fillAfter 属性为 true。注意:当一个按钮通过 View 动画后,单击新位置无法响应点击事件,点击原来的位置却会响应,为了解决这个问题:1. 使用属性动画; 2. 在新位置预先创建一个相同的 button,和目标 button 的点击事件也要相同,当目标button 完成动画后,就把目标 button 隐藏,同时预先创建的 button 显示出来,这只是一个参考。
3. 改变布局参数
即:改变 LayoutParams。
1. 通过改变(MarginLayoutParams) LayoutParams 的 leftMargin、topMargin等,重新设置 LayoutParams。(view.setLayoutParams(params); 或 view.requestLayout();
)
2. 为了改变一个 Button 的位置,可以在 Button 的左边放一个空的 View,且空 View 的默认宽度为0,需要向右移动 button 的时候,重新设置空 View 的宽度即可。
4. 各种滑动方式的对比
scrollTo / scrollBy:操作简单,适合对 View 内容滑动
动画:操作简单,适合对没有交互的 View 和实现复杂的动画效果
改变布局参数:操作稍微复杂,适用于有交互的 View
3.3 弹性滑动
如何实现弹性滑动:将一次大的滑动分成若干次小的滑动并在一个时间段内完成。
使用 Scroller
下面是典型使用方法:
Scroller mScroller = new Scroller(mContext);
private void smoothScrollTo(int destX, int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
运行过程:当我们构造一个 Scroller 对象并调用它的 startScroll 方法时,Scroller 内部什么都没有做,只是保存了我们传递的几个参数。invalidate();
会导致 View 重绘,在 View 的 draw 方法中又会调用computeScroll()
,该方法在 View 中是一个空实现。因为这个方法才使得弹性滑动,原因:当 View 重绘后会在 draw 方法调用 computeScroll()
,而 computeScroll()
又会去向 Scroller 获取当前的 scrollX 和 scrollerY;然后通过 scrollTo 实现滑动;接着调用 postInvalidate();
进行第二次重绘,重绘过程与上次相同,还是会导致computeScroll()
被调用;然后继续向 Scroller 获取当前的 scrollX 和 scrollY,并通过 scrollTo 滑动到新的位置,如此反复,直至滑动过程结束。
Scroller 的工作原理:Scroller 本身并不能实现 View滑动,通过结合 View 的computeScroll()
才能完成弹性滑动的效果,它不断让 View 进行重绘,每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔 Scroller 就可以得到 View 当前的滑动位置,知道了滑动位置就可以通过 scrollTo 来完成 view 的滑动,就这样,View 的每一次重绘都会导致 View 进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,这就是 Scroller 的工作机制。
使用动画
1. 使用属性动画:ObjectAnimator
2. 使用 ValueAnimator,原理在动画到来的每一帧前获取动画完成的比例,然后再根据这个比例计算出当前 View 所要滑动的距离。思想和Scoller比较类似。
ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(3000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
Log.i(TAG, "onAnimationUpdate: " + fraction);
((View)view.getParent()).scrollTo(startX+(int)(deltaX * fraction), 300);
}
});
animator.start();
使用延时策略
核心思想就是通过发送一系列延时消息从而达到一种渐进式的效果。可以把一段距离分成100次进行滑动,算出每一次应该什么时候滑动和滑动的距离。
3.4 View 的事件分发机制
public boolean dispatchTouchEvent(MotionEvent ev)
进行事件分发,如果事件能够传到当前 View,该方法一定会被调用,返回值受当前 view 的 onInterceptTouchEvent 影响和子 view 的 dispatchTouchEvent 的影响。
public boolean onInterceptTouchEvent(MotionEvent ev)
在上述方法内部调用。如果当前 View 拦截了某个事件,那么在同一个事件序列中,该方法不会被调用,返回结果表示是否拦截当前事件。
public boolean onTouchEvent(MotionEvent event)
在 dispatchTouchEvent 内部调用,用来处理点击事件,返回结果表示是否消耗该事件,如果不消耗,则在同一个事件序列中,当前 View 无法再次接收到事件。
伪代码表示三者关系:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)){
consume = onInterceptTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
当一个点击事件产生后,传递顺序:Activity -> Window ( PhoneWindow ) -> View
,如果一个 View 的 onTouchEvent 返回 false,则会调用父容器的 onTouchEvent,若还是返回false,则会调用Activity 的 onTouchEvent。
1. 某个 View 一旦决定拦截,那么这一事件序列就只能由它来处理,并且它的 onInterceptTouchEvent(ev)
不会再被调用,但是父容器的onInterceptTouchEvent(ev)
还是会调用。
2. 某个 View 一旦开始处理事件,如果它不消耗 ACTion_DOWN 事件,那么同一事件序列中的其它事件都不会交给它处理,并且交给它的父容器处理,即父容器的 onTouchEvent 会被调用。
3. 如果 View 不消耗除了 ACTION_DOWN 以外的事件,那么这个点击事件会消失,父元素的 onTouchEvent 也不会被调用,并且当前 View 会持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理。
4. View 没有onInterceptTouchEvent(ev)
方法,一旦有点击事件传递给它,那么它的 onTouchEvent 就会被调用,除非在 dispatchTouchEvent 中直接返回了。
5. View 的 enable 属性不影响 onTouchEvent 的默认返回值,而clickable 和 longClickable 有一个为 true,那么它的 onTouchEvent 就返回 true。
事件分发源码解析地址:
3.5 View 的滑动冲突
常见的滑动冲突:
场景1:外部滑动方向和内部方向不一致 (ViewPager + ListView):
场景2:外部方向和内部方向一致 (SlideMenu + ViewPager)
场景3:上面两种情况的嵌套 (SlideMenu + ViewPage + ListView)
解决方法:
1. 外部拦截法 点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,不需要就不拦截。
注意:
1>. ACTION_DOWN 事件必须返回false,否则后面的一系列事件不会到达子View。
2>. ACTION_MOVE 事件可以根据条件拦截,父容器需要就拦截,不需要不拦截
3>. ACTION_UP 事件必须返回 false,因为该事件对于当前 ViewGroup 本身也没意义。前提是 Down 事件和 Move 事件都是子 View 消耗,如果此时当前 ViewGroup 的 up 事件返回true的话,子 View 无法接收到该事件,子元素的 onClick 事件无法触发。
private int startX;
private int startY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int lastX = 0;
int lastY = 0;
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
startX = (int) ev.getX();
startY = (int) ev.getY();
return false;
case MotionEvent.ACTION_MOVE:
lastX = (int) ev.getX();
lastY = (int) ev.getY();
if (Math.abs(startX-lastX) > Math.abs(startY-lastY)){
return true;
}else {
return false;
}
case MotionEvent.ACTION_UP:
return false;
default:
break;
}
startX = lastX;
startY = lastY;
return false;
}
2. 内部拦截法: 父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理。
重写子元素的 dispatchTouchEvent:
public boolean dispatchTouchEvent(MotionEvent ev) {
int xNow = 0;
int yNow = 0;
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
/**
* 不让父容器的 onInterceptTouchEvent 方法在接下来的事件被调用
*/
getParent().requestDisallowInterceptTouchEvent(true);
x = (int) ev.getX();
y = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
xNow = (int) ev.getX();
yNow = (int) ev.getY();
if (Math.abs(x - xNow) > Math.abs(y - yNow)){
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
x = xNow;
y = yNow;
return super.dispatchTouchEvent(ev);
}
父 View 的处理:
public boolean onInterceptTouchEvent(MotionEvent ev) {
super.onInterceptTouchEvent(ev);
if (ev.getAction() == MotionEvent.ACTION_DOWN){
return false;
}else {
return true;
}
}
注意:调用getParent().requestDisallowInterceptTouchEvent(false);
方法的时候,仅仅是修改了 FLAG_DISALLOW_INTERCEPT
标记位,让事件来临的时候走onInterceptTouchEvent(MotionEvent ev)
,并不一定真正会被拦截,因此,父元素的ACTION_UP 事件要返回true才能确保事件被父容器拦截。
/**
* 当事件由ViewGroup的子元素处理的时候,mFirstTouchTarget会被赋值指向子元素
* 当ViewGroup拦截事件,mFirstTouchTarget!=null不成立
* 一旦当前ViewGroup拦截事件,actionMasked == ACTION_MOVE和actionMasked == ACTION_UP
* 并且 mFirstTouchTarget == null,不再走当前view的onInterceptTouchEvent(ev)方法,并且其他事件都交给它处理
* 但是父容器的onInterceptTouchEvent(ev)还是会被调用
*/
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
/**
* FLAG_DISALLOW_INTERCEPT一般是在子view中通过requestDisallowInterceptTouchEvent(true)设置的
* FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN之外的事件,因为ACTION_DOWN会重置该标记位
*/
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}