View 系列一之事件体系

View事件体系

一、View的基础知识点

1、什么是View?

View是界面层控件的抽象,可以是一个具体的View,也可以是ViewGroup,本身包含很多子View。

2、View的参数

  1. top = getTop();
  2. left = getLeft();
  3. right = getRight();
  4. bottom = getBottom();
  5. width = right - left
  6. height = bottom - top
  7. x = left + translationX;
  8. y = top + translationY;
    top,right,bottom,left分别代表View在父容器中的相对父容器的位置,3.0之后的版本新增x,y,translationX,translationY,x和y分别代表View在父容器中的位置,translationX和translationY代表View在父容器中的偏移量。

3、点击事件类型和最小滑动距离

3.1点击事件类型
  1. ActionDown
  2. ActionMove
  3. 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();//提交刷新请求
	}
}

三、滑动

  1. ScrollTo/ScrollBy
  2. 动画
 ObjectAnimator.ofFloat(view,"translationX",0,1000).setDuration(1000).start();//将View在1000ms内,从0移动到1000 px处,书上说这个方法不会移动View本身,点击移动后的View不会有反应,我移动后点击有反应,标记一下。
  1. 改变布局参数
    布局也是一个View,它是ViewGroup。
ViewGroup.MarginLayoutParams Layoutparams = (ViewGroup.MarginLayoutParams)view.getLayoutParams();//获取view的布局参数
Layoutparams.width += 100;
Layoutparams.leftMargin += 100;
tv.requestLayout();//重新绘制布局

四、弹性滑动

  1. Scroller
  2. 动画
    动画的滑动本身就具有弹性,我们可以仿照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();
  1. 延时策略
    通过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方法,如此反复直到事件被最终处理。
外部拦截发父View的Log
如图所示,竖直方向滑动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;
}

关于事件传递的机制,这里给出一些结论:

  1. 一个事件系列以down事件开始,中间包含数量不定的move事件,最终以up事件结束。
  2. 正常情况下,一个事件序列只能由一个View拦截并消耗。
  3. 某个View拦截了事件后,该事件序列只能由它去处理,并且它的onInterceptTouchEvent不会再被调用。
  4. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvnet返回false),那么同一事件序列中的其他事件都不会交给他处理,并且事件将重新交由他的父元素去处理,即父元素的onTouchEvent被调用。
  5. 如果View不消耗ACTION_DOWN以外的其他事件,那么这个事件将会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终消失的点击事件会传递给Activity去处理。
  6. ViewGroup默认不拦截任何事件。
  7. View没有onInterceptTouchEvent方法,一旦事件传递给它,它的onTouchEvent方法会被调用。
  8. View的onTouchEvent默认消耗事件,除非他是不可点击的(clickable和longClickable同时为false)。
  9. View的enable不影响onTouchEvent的默认返回值。
  10. onClick会发生的前提是当前View是可点击的,并且收到了down和up事件。
  11. 事件传递过程总是由外向内的,即事件总是先传递给父元素,然后由父元素分发给子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;
	}
}

内部拦截实现方法Demo已上传。<内部拦截法Demo>
外部拦截实现方法Demo已上传。<外部拦截法Demo>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值