一、View基础知识
(1)什么是View
View是所有控件的基类,ViewGroup的基类也是View,所以View有可能是单一控件也可能是一组控件。所以一个布局的子控件可以是一个View也可以是一个ViewGroup
(2)View的位置参数
View的位置由控件的四个顶点决定,分别是top、left、right、bottom,top是左上角的纵坐标,left是左上角的横坐标,right是右下角的横坐标,bottom是右下角的纵坐标。(这些坐标都是相对父容器的)
这些坐标的获得方法:
top=getTop();
left=getLeft();
right=getRight();
bottom=getBottom();
扩展(相对屏幕的坐标获取):
getRawX();
getRawY();
宽度和高度与位置参数的关系为:
width=right-left;
hight=bottom-top;
从android3.0以后又引入了x、y、translationX、translationY等几个参数,x,y为View的左上角的坐标,translationX和translationY是相对自身的偏移量
x=left+translationX
y=top+translationY
left和top是View第一次绘画时的左上角的坐标值为固定值
推荐阅读:View 的 translationX、 translationY , X、Y 和 Left、Top,Right、Bottom
(3)MotionEvent和TouchSlop
1.MotionEvent
手指接触屏幕的一系列的事件中经典的有:
ACTION_DOWN:手指刚接触屏幕
ACTION_MOVE:手指在屏幕上滑动
ACYION_UP:手指离开屏幕
一次完整的手指接触屏幕动作由一个ACTION_DOWN和零个或者多个ACTION_MOVE和一个ACYTON_UP构成,当没有ACTION_MOVE的时候为点击,当有ACTION_MOVE时为滑动。
通过motionEvent还可以获取点击事件发生的坐标,分别为getX/getY和getRawX/getRawY,它们的区别和View的方法一样。
2.TouchSolp
TouchSolp为系统所能识别的被认为是滑动的最小距离。获取方式为ViewConfiguration.get(getContext()).getScaledTouchSlop();
(4)VelocityTracker、GestureDetector、Scroller
1.VelocityTracker
速度追踪,用于追踪手指滑动过程中的速度,包括水平速度和竖直速度。
使用方法:
VelocityTracker velocityTracker=VelocityTracker.obtain();
velocityTracker.addMovement(event);
velocityTracker.computeCurrentVelocity(1000);
int xVelocity=(int)velocityTracker.getXVelocity();
int YVelocity=(int)velocityTracker.getYVelocity();
2.GestureDetector
手势检测,用于辅助检测点击、双击、滑动、长按等行为
使用方法:
GestureDetector gestureDetector=new GestureDetector(this);
gestureDetector.setIsLongpressEnabled(false);
boolean consume=gestureDetector.onTouchEvent(event);
return consume;
然后有选择的实现OnGestureListener和OnDoubleTapListener接口
接口中常用的方法:
onSingleTapUp 单击
onFling 快速滑动
onScroll 拖动
onLongpress 长按
onDoubleTap 双击
建议:如果监听滑动相关的,用onTouchEvent中实现,监听单击双击事件用GestureDeteator
3.scroller
弹性滑动对象,当使用View的scrollTo/scrollBy时候滑动比较生硬瞬间完成的,使用Scroller对象可以缓慢的滑动过去
使用方法:
Scroller scroller=new Scroller(mContent);
private void smoothScrollTo(int destX,int destY){
int scrollX=getScrollX();
int delta=destX-scrollX;
scroller.startScroll(scrollX,0,delta,0,1000);
invalidate();
}
@Override
public void computeScroll(){
if(scrolller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),scroller.getCurrY());
postInvalidate();
}
}
(5)View的滑动
1.使用scrollTo/scrollBy
源码:
scrollTo/scrollBy源码
其中mScrollX表示View左边缘和View内容左边缘的水平距离,mScrollY表示View上边缘和View内容上边缘的竖直距离。scrollTo/scrollBy只能改变内容的位置不能改变View的位置。View的左边缘在内容左边缘的右边时mScrollX为正值,反之为负值,View的上边缘在内容上边缘的下边时mScrollY为正值,反之为负值。
2.使用动画
可以使用一个动画让一个View平移,主要操纵View的translationX和translationY属性。
使用方法(属性动画):
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100),start();
属性动画平移可以改变View的位置
3.改变布局参数
改变LayoutParams
使用方法:
MarginLayoutParams params=(MarginLayoutParams)mButton.getLayoutParams();
params.width+=100;
params.leftMargin+=100;
mButton.requestLayout();
//或者mButton.setLayoutParams(params);
各种滑动的对比
scrollTo/scrollBy:适合对内容的滑动
动画:适合完成复杂的滑动(属性动画也适用于有交互的View)
改变Layout参数:操作稍微复杂,适用于有交互的View
(6)弹性滑动
1.使用Scroller
Scroller内部其实什么都没有做,只是保存了我们传入的参数。invalidate方法可以使view重绘,在View的draw方法又会调用computeScroll方法,view里的computeScroll是个空实现,我们在computeScroll中调用Scroller的computeScrollOffset方法判断是否滑动完成,computeScrollOffset方法通过时间流逝来计算出scrollX和scrollY的值即mCurrX和mCurrY的值(单位移动的距离,这个方法把要滑动的距离分成一小块一小块的),然后调用scrollTo方法滑动单位距离,如果未完成滑动递归调用invalidate(postInvalidate)
2.通过动画
动画本身就是一种渐近的过程,通过动画实现本身就是天然的弹性滑动。
3.使用延时策略
使用handler、View的postDelayed方法、或者使用线程sleep方法
(7)事件分发机制
1.点击事件的传递规则
这里点击事件指的是MotionEvent,点击事件的分发过程就是对MotionEvent的分发过程。这传递过程由三个重要的方法共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
dispatchTouchEvent:用来进行事件的分发,如果有时间能够传到该View这个方法一定会调用,返回结果受到onTouchEvent和下级的dispatchTouchEvent方法的影响,表示是否消耗此事件。
onInterceptTouchEvent:在dispatchTouchEvent中调用,表示是否拦截此事件,如果当前View拦截某个事件,那么在同一事件序列中,此方法不会在被调用,返回结果为是否拦截此事件。
onTouchEvent:在dispatchTouchEvent中调用,用来处理点击事件,返回结果表示是否消耗事件,如果不消耗事件,在同一事件序列中,此View不再接收到事件
它们之间的关系为(伪代码):
public void dispatchTouchEvent(Motion Event event){
boolean consume=false;
if(onIntercepTouchEvent(event)){
cusume=onTouchEvent(event);
} else{
consume=child.dispatchTouchEvent(event);
}
return consume;
}
对于一个根VIewGroup来说,首先调用它的dispatchTouchEvent方法,如果dispatchTouchEvent内的onInterceptTouchEvent如果返回true表示要拦截这个事件,就会交给这个ViewGroup来处理,即调用onTouchEvent,如果onInterceptTouchEvent返回false则将事件传递给子控件,一直反复直到事件被处理。
如果一个View设置了OnTouchListener,那么OnTouchListener中的onTouch就会被调用,如果onTouch方法返回false则onTouchEvent就会调用,如果返回true则onTouchEvent不会被调用,由此可见OnTouchListener的优先级比onTouchEvent高。如果设置有OnClickListener,只要当onTouchEvent被调用的时候才有用。如果onTouch返回true则onClick就会失效。
事件传递的顺序:Activity-》window-》View
关于事件传递机制的结论:
- 同一个事件序列指的是从手指接触屏幕那一刻起直到手指离开屏幕的那一刻所产生的一系列事件,以down开始,过程中move,up结束
- 正常情况下,一个事件的序列只能被一个View拦截并消耗
- 某个View一旦决定拦截某个事件,那么这个事件序列都只能由它来处理,并且它的onInterceptTouchEvent都不会再被调用。
- 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件,那么同一事件序列中的其他事件都不会再交由它处理,并且事件将重新交由它的父元素处理。
- 如果View不消耗除ACTION_DOWN以外的其它事件,那么点击事件会消失,父元素的onTouchEvent也不会调用,View还会接收到后续的事件,消失的事件最终会交由Activity处理。
- ViewGroup默认不拦截任何事件,Android源码中的onInterceptTouchEvent默认返回false
- View没有onInterceptTouchEvent方法,一但有事件传递给它,他的onTouchEvent就会被调用
- View的onTouchEvent默认都是消耗事件的,除非它是不可点击的(clickable和longClickable都为false),longClickable默认都为false,clickable分情况,例如:
button的clickable为true,textview的clickable为false - View的enable不影响onTouchEvent的默认返回值,只要clickable和longClickable可以影响
- onClick会被调用的前提是View是可以点击的,且收到down和up的事件
- 事件的分发总是由外向内的,即事件总是先传递给父元素,然后分发给子元素。
(8)事件分发源码分析
1.Activity对事件的分发过程
先传递给Activity,Activity交给Window做具体处理,window分发给decor View,decor View指的是当前界面的底层容器(即setContent View所设置的View的父容器)
Android的dispatchTouchEvent方法源码:
Activity首先交给window处理,如果返回ture,事件循环结束,返回false则会调用Activity的onTouchEvent。
2.window处理事件
源码:
直接传递给decor View处理
3.decor View
decor View继承于frameLayout且是父容器,所以直接会传到顶级View
4.顶级View的分发机制
分段看源码:
有两种情况下会判断是否拦截事件,事件类型为ACITION_DOWN或者mFirstTouchTarget不为空的时候,mFirstTouchTarget不为空的意思是当子控件处理事件成功时候,mFirstTouchTarget指向子控件,那么后面来ACTION_MOVE和ACTION_UP时,这两个条件都不满足,导致onInterceptTouchEvent不再被调用。
当ViewGroup不拦截事件时,事件传递给View的的源码:
image.png
首先遍历所有子元素,判断子元素是否可以收到事件,有两点可以判断能否接收事件:子元素是否在播动画或者点击事件的坐标是否在子元素的区域内。如果某个元素满足这两个条件,那么事件就交由它处理。
view处理事件源码:
由于View不包含子元素,所以它只能自己处理事件,首先它会判断有没有设置OnTouchLIstener,如果没有则调用onTouchEvent
(9)滑动冲突
1.常见的滑动冲突
- 外部滑动和内部滑动方向不一致
- 外部滑动和内部滑动方向一致
- 上面两种的嵌套
2.滑动冲突处理规则
对于第一种冲突,我们根据是水平滑动还是竖直滑动来决定谁来拦截事件
对于第二种冲突,我们根据 业务上的需求,决定让谁拦截事件
3.解决方式
外部拦截法
外部拦截法的意思是所有事件都要经过父元素的拦截处理,如果父容器需要则拦截,不需要则不拦截。外部拦截法重写父元素的onInterceptTouchEvent方法,伪代码如下:
内部拦截法
内部拦截法是父元素不拦截任何事件,全部传递给子元素,如果子元素需要就消耗掉,不需要就不消耗。需要配合requestDisallowInterceptTouchEvent(requestDisallowInterceptTouchEvent相当于一个开关,可以设置父元素是否可以拦截事件)使用。
伪代码如下:
除了子元素要做处理以外,父元素也要默认拦截除了ACTION_DOWN以外的事件。
父元素的修改:
网友评论