Android事件拦截机制
Android中事件的传递和拦截和View树结构是相关联的,在View树中,分为叶子节点和普通节点,普通节点有子节点只能是ViewGroup,叶子节点可以是View或者ViewGroup。Android和事件分发拦截相关的方法有
dispatchTouchEvent(MotionEvent ev)
事件分发相关的方法,沿着View树将一个用户的触摸事件向下分发。
onInterceptTouchEvent(MotionEvent ev)
在dispatchTouchEvent中被调用,用来判断某一层级是否拦截一个事件,返回true即拦截,事件不会再向下分发,注意View树中叶子节点(View和ViewGroup)直接拦截事件。
onTouchEvent(MotionEvent ev)
一个某一个层级拦截了事件,那么所有事件序列都会交由它处理,后面onInterceptTouchEvent不会再被调用,转而onTouchEvent被调用。OnTouchEvent返回true则消耗掉这个事件序列,如果没有消耗ACTION_DOWN事件则事件序列将沿着View树向上传递,去找能处理这个事件的父View。如果消耗了ACTION_DOWN而没有消耗其它事件,那么这个事件序列将消失。
整体过程描述:事件产生传递到某一个ViewGroup时,首先其onInterceptTouchEvent会被调用,如果当前ViewGroup选择拦截这个事件则返回true,于是它的onTouchEvent会被调用。否则将继续调用子View的dispatchTouchEvent进行方法的拦截判断和相应的处理。
当一个View处理事件时,首先会调用它的OnTouchListener,如果OnTouchListener返回false则会继续调用onTouchEvent,在onTouchEvent中才会检查onClickListener,由此可见三种处理事件方法的优先级是:OnTouchListener > onTouchEvent > onClickListener。
ScrollTo,ScrollBy,Scroller
在实现滑动效果的时候,最常用的三个方法就是ScrollTo,ScrollBy和Scroller
首先介绍ScrollTo和ScrollBy,两个方法一个是滑动到某个位置,一个是滑动多少位置。关键在于,ScrollTo和ScrollBy对于普通的View组件比如TextView、ImageView的效果是移动View的内容,也就是相应的字体、照片,仅对于ViewGroup才是移动所有的子View。也就是说,ScrollTo和ScrollBy通常用在自定义的ViewGroup实现滑动效果时。
其次要理解ViewGroup滑动的坐标系,如下图左边是滑动前的布局,一个ViewGroup下面有两个子View,在ViewGroup中调用ScrollTo(0,300)就是将ViewGroup向下滑动,可以将ViewGroup看做一个透明窗口,向下滑动后第一个子View消失不见,第二个子View相对效果即是向上滑动。所以这里要注意ScrollTo和ScrollBy的正负值,同时记住滑动的是ViewGroup,子View只是间接滑动的。
最后,Scroller很简单,Scroller更类似于动画中的插值器,处理计算和存储坐标值,什么也没有做。当我们调用
mScroller.startScroll(getScrollX(),getScrollY(),0,mHeaderHeight+getPaddingTop(),3000);
后,实际上是在其中根据时间和要移动的像素计算出每一时刻所应该在的像素位置,然后不停的调用scrollBy移动到这个位置并重绘。同时由于View在重绘时绘调用computeScroll方法,所以我们要在其中进行判断并继续scroll,形成有条件递归,形成动画。
下拉刷新组件的简单原理
基本介绍
一个典型的下拉刷新界面如上,对于下拉刷新功能而言,界面主要包含两个部分,一个是展示Refresh界面的部分,一个是展示如ListView之类列表的部分。为了实现下拉刷新功能,我们所需要的就是自定义一个ViewGroup。我们的RefreshLayout中包含两个子View,header和content。header界面如下:
content可以是ListView,同样也是一个ViewGroup。界面初始时由于header和content都可以看到,所以我们在RefreshLayout的onLayout方法结束前,调用scrollTo(0,headerHeight)可以将header滑动出界面。然后,总的思路就是分析RefreshLayout和ListView对于一个触摸事件,谁来拦截谁来处理的问题。
RefreshLayout实现:
RefreshLayout绘制过程:
首先通过 LayoutInflater.from(context).inflate以及addView方法,在RefreshLayout构造函数中向布局添加header和content。对于一个ViewGroup而言,绘制过程中最重要的是onMeasure和onLayout方法。
onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = 0;
for(int i=0;i<getChildCount();i++) {
measureChild(getChildAt(i),widthMeasureSpec,heightMeasureSpec);
height += getChildAt(i).getMeasuredHeight();
}
height = heightMeasureSpec;
setMeasuredDimension(width,height);
}
onMeasure方法中,一定要对全部子View进行measure,在这里调用的是measureChild方法,因为measureChild内部还会根据子View的LayoutParams进一步封装出MeasureSpec进行测量。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int left =getPaddingLeft();
Log.d("TAG", l + " " + t + " " + r + " " + b);
int top = getPaddingTop();
for(int i=0;i<count;i++) {
View child = getChildAt(i);
child.layout(left,top,child.getMeasuredWidth(),child.getMeasuredHeight() + top);
Log.d("TAG", "child: " + child.getMeasuredWidth() + " " + child.getMeasuredHeight());
top += child.getMeasuredHeight();
}
if(!init){
//将ViewGroup向y轴正方向移动,其实相当于将View向y轴负方向移动
scrollTo(0,mHeaderHeight+getPaddingTop());
invalidate();
init = true;
}
}
onLayout方法中进行我们想要的布局,注意由于重新绘制时,onMeasure和onLayout会多次被调用,所以要注意一些初始化方法的执行。
RefreshLayout事件拦截及处理
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
prevY = (int) ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int delY = (int) (ev.getRawY() - prevY);
Log.d("TAG", "delY " + delY);
if(delY>0) {
return true;
}
break;
}
return false;
}
在拦截事件中,只做了一个简单的判断,一旦滑动的纵向距离大于0,表明手指再从上向下滑,同时这里应该判断一下ListView中显示的第一条是不是全部数据中的第一条。然后拦截事件后交由onTouchEvent处理。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
int dy = (int) (event.getRawY() - prevY);
int sy = mHeaderHeight-dy;
scrollTo(0,sy>0?sy:0);
Log.d("TAG", "dy " + dy);
break;
case MotionEvent.ACTION_UP:
refresh();
break;
}
return true;
}
之前将ViewGroup向下滑动了headerHeight的距离,为了让header显示出来,其实应该让ViewGroup向上滑动也即y轴变小,同时为了避免过分滑动还要进行一下判断。当手指抬起时,还要根据移动的y轴增量判断一下是否是有效的滑动,然后处理响应的业务逻辑。注意的是,由于当前是主线程,所以要使用
new Thread(new Runnable() {
@Override
public void run() {
mission();
post(new Runnable() {
@Override
public void run() {
mScroller.startScroll(getScrollX(),getScrollY(),0,mHeaderHeight+getPaddingTop(),3000);
mArrowView.setVisibility(VISIBLE);
mProgress.setVisibility(GONE);
}
});
}
}).start();
新起一个线程完成mission,同时通过当前ViewGroup的消息队列,在任务完成后修改UI。
涉及到的原理大致就是这些,完整的代码可以查看何洪洋老师的博客:
https://github.com/hehonghui/android_my_pull_refresh_view