View事件体系
一、View的基础知识点
1、什么是View?
View是界面层控件的抽象,可以是一个具体的View,也可以是ViewGroup,本身包含很多子View。
2、View的参数
- top = getTop();
- left = getLeft();
- right = getRight();
- bottom = getBottom();
- width = right - left
- height = bottom - top
- x = left + translationX;
- y = top + translationY;
top,right,bottom,left分别代表View在父容器中的相对父容器的位置,3.0之后的版本新增x,y,translationX,translationY,x和y分别代表View在父容器中的位置,translationX和translationY代表View在父容器中的偏移量。
3、点击事件类型和最小滑动距离
3.1点击事件类型
- ActionDown
- ActionMove
- ActionUp
基本常见的三个类型,按下,滑动,弹起。
3.2 最小滑动距离
TouchSlop最小滑动距离,小于这个距离则默认没有滑动,这是一个属性值,通过配置文件获取,每个不同的设备值不同。
int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop()
二、速度追踪,手势检测,弹性滚动
1、VelocityTracker
VelocityTracker velocityTracker = VelocityTracker().obtain();//获取VelocityTracker对象
velocityTracker.add(event);//针对event进行速度追踪
velocityTracker.computeCurrentVelocity(1000);//获取1000ms内event的速度
float xVelocity = velocityTracker.getXVelocity();//获取x方向的速度
float yVelocity = velocityTracker.getYVelocity();//获取Y方向的速度,速度值有正负之分,因为速度是矢量
velocityTracker.clear();
velocityTracker.recycle();//计算完速度,不使用的时候释放内存。
2、GestureDector
GestureDector gestureDector = new GestureDector(this);
gestureDector.isLongPressenabled(false);//防止出现长按后无法滑动的情况
boolean resume = gestureDector.onTouchEvent(event);//接管目标View的onTouchEvent,将event放入gestureDector中进行处理,如果gestureDecotr不消耗这个事件,则返回false
return resume;
3、Scroller
1.Scorller不会进行滑动,需要和View的computeScorller()联合使用。
2.Scroll主要调用Scrollto/ScrollBy,滚动的是View的内容,并不是View本身。一个是绝对滚动,一个是相对滚动。
具体代码如下:
Scroller scroller = new Scroller(mContext);//获取弹性滑动对象
//缓慢移动到指定位置
private void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();//View内容距View左边沿的距离,距离可以是负值,同理getScrollY();
int delta = destX - scrollX;
mScroller.startScroll(scrollX,0,delta,0,1000);//1000ms内滑向destX,效果就是慢慢滑动
invalidata();//刷新界面,调用view.ondraw,ondraw调用computeScroll
}
@Override
public void computeScroll(){
if(mScroller.computeScrollOffset()){//计算如果没有到目标位置
scrollTo(mScroller.getCurrX,mScroller.getCurrY());//没有到位置,则继续滚动
postInvalidate();//提交刷新请求
}
}
三、滑动
- ScrollTo/ScrollBy
- 动画
ObjectAnimator.ofFloat(view,"translationX",0,1000).setDuration(1000).start();//将View在1000ms内,从0移动到1000 px处,书上说这个方法不会移动View本身,点击移动后的View不会有反应,我移动后点击有反应,标记一下。
- 改变布局参数
布局也是一个View,它是ViewGroup。
ViewGroup.MarginLayoutParams Layoutparams = (ViewGroup.MarginLayoutParams)view.getLayoutParams();//获取view的布局参数
Layoutparams.width += 100;
Layoutparams.leftMargin += 100;
tv.requestLayout();//重新绘制布局
四、弹性滑动
- Scroller
- 动画
动画的滑动本身就具有弹性,我们可以仿照Scroller进行滑动。
final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new AnimatorUpdateListener(){
@override
public void onAnimationUpdate(ValueAnimator animator){
float fraction = animator.getAnimatedFraction();
view.scrollTo(startX + (int) (deltaX * fraction) , 0);
}
});
animator.start();
- 延时策略
通过Handle和View的postDelayedMessage或者线程Sleep方法延时绘制View
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;//Frame数
private static final int DELATED_TIME = 33;//延时时长
private int mCount = 0;
@suppressLint("HandlerLeak")
private Handler handler = new handler(){
public void handleMessage(Message msg){
switch(msg.what){
case MESSAGE_SCROLL_TO:
mCount ++ ;
if (mCount <= FRAME_COUNT){
float fraction = mCount / (float) FRAME_COUNT;
int scrollX = (int) (fraction * 100);
mButton1.scrollTo(scrollX,0);
mHandelr.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO , DELAYED_TIME);}
break;
default : break;
}
}
}
五、事件分发机制
public boolean dispatchTouchEvent(MotionEvent ev); //用来进行事件的分发
public boolean onInterceptTouchEvent(MotionEvent ev); //用来判断是否拦截事件
public boolean onTouchEvent(MotionEvent ev); //用来处理点击事件
对于根ViewGroup,点击事件产生后,首先会调用他的onDispatchTouchEvent方法,如果Viewgroup的onInterceptTouchEvent返回true表示他要拦截事件,接下来事件就会交给ViewGroup处理,调用onTouchEvent方法.
如果ViewGroup的onInteceptTouchEvent方法返回值为false,表示ViewGroup不拦截该事件,这时事件就传递给他的子View,接下来子View调用dispatchTouchEvent方法,如此反复直到事件被最终处理。
如图所示,竖直方向滑动listview,父View检测到down事件后,返回False,不消耗它,不调用TouchEvent,事件传给子View ListView,ListView消耗它。
如图所示,水平滑动,父View检测到move事件后,根据条件判断返回true,表示消耗它,调用touchevent,在touchevent的move中调用scrollto,检测到up事件后,调用smoothscrollto,联合computeScroll,进行滑动处理。
当一个View需要处理事件时,如果它设置了OnTouchListener,那么onTouch方法会被调用,如果onTouch返回false,则当前View的onTouchEvent方法会被调用,返回true则不会被调用。
同时,在onTouchEvent方法中如果设置了OnClickListener,那么他的onClick方法会被调用。
由此可见处理事件时的优先级关系: onTouchListener > onTouchEvent >onClickListener
这里需要了解下window,activity,view的关系,建议学习下三者之间的关系
//事件分发的伪逻辑代码
public boolean dispatchTouchEvent (MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvnet(ev){//如果自身检测到要处理这个event
consume = onTouchEvent(ev);//则返回处理结果
} else {
consume = child.dispatchTouchEnvet(ev);//否则发送给子View去处理
}
return consume;
}
关于事件传递的机制,这里给出一些结论:
- 一个事件系列以down事件开始,中间包含数量不定的move事件,最终以up事件结束。
- 正常情况下,一个事件序列只能由一个View拦截并消耗。
- 某个View拦截了事件后,该事件序列只能由它去处理,并且它的onInterceptTouchEvent不会再被调用。
- 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvnet返回false),那么同一事件序列中的其他事件都不会交给他处理,并且事件将重新交由他的父元素去处理,即父元素的onTouchEvent被调用。
- 如果View不消耗ACTION_DOWN以外的其他事件,那么这个事件将会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终消失的点击事件会传递给Activity去处理。
- ViewGroup默认不拦截任何事件。
- View没有onInterceptTouchEvent方法,一旦事件传递给它,它的onTouchEvent方法会被调用。
- View的onTouchEvent默认消耗事件,除非他是不可点击的(clickable和longClickable同时为false)。
- View的enable不影响onTouchEvent的默认返回值。
- onClick会发生的前提是当前View是可点击的,并且收到了down和up事件。
- 事件传递过程总是由外向内的,即事件总是先传递给父元素,然后由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的分发过程,但是ACTION_DOWN事件除外。
六、滑动冲突
6.1 内外方向不一样(外部左右,内部上下滑动)
ViewPager + Fragment主流应用基本都是这个样子,Viewpager已经默认为我们处理了滑动冲突事件,所以无需理会。
如果自定义嵌套View出现这种冲突,则可以按判断横竖距离来进行事件分发处理。
6.1.2内外方向相同(外部左右,内部左右)
通过业务逻辑来进行判断,重写父容器或者子容器的InterceptTouchEvent
6.1.3上两种情况嵌套
同上,通过业务逻辑来进行判断。
外部拦截法
所谓的外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截
public boolean onInterceptTouchEvent (MotionEvent event){
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要当前事件) {
intercepted = true;
} else {
intercepted = flase;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default : break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
内部拦截法
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗,否则就交由父容器进行处理,这种方法与Android事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作.
public boolean dispatchTouchEvent ( MotionEvent event ) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction) {
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此类点击事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default :
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
/**除了子元素需要做处理外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件,这样当子元素调parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。
因此,父元素要做以下修改:*/
public boolean onInterceptTouchEvent (MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}