前言:
1.view不属于四大组件,但是它的作用堪比四大组件,甚至比Receiver和Provider还要重要
2.Activity提供可视化的功能,Android系统提供了很多基础控件,比例button,textview.checkbox,
但是很多时候控件不能满足我们的需求,这个时候就需要自定义控件,而控件的自定义就需要对于android整个view的事件体系有深入的理解。
3.如何解决滑动冲突问题?需要对view的事件分发机制有一定的了解
一.view的位置参数
view的位置主要有他的四个顶点决定,分别对应view的四个属性:mLeft,mTop,mRight,mBottom这几个只代表View的原始位置,这个值是不变的,
如何获取view的四个属性呢?
mleft = getLeft();
mRight = getRight();
mTop = getTop();
mBottom = getBottom();
从3.0开始,view增加了额外的几个属性:x,y,translateX,translateY;
x,y: 是view的左上角的坐标;
translateX,translateY:是view左上角相对之前左上角的偏移量
如果view发生了偏移,那么x,y,translateX,translateY就会变化,x,y,translateX,translateY之间的对应关系如下:
x = mLeft + translateX;
y = mTop + translateY;
注意:这些坐标是相对于view的父容器来的,因此它是一种相对坐标,由此我们可以得出view的宽高和坐标关系:
int with = right - left;
int height = bottom - top;
二.MotionEvent和TouchSlop对象
1.获取点击事件发生的坐标,系统提供了两组方法:getX/getRawX,getY/getRawY ;
getX/getY:返回的是相对于当前view左上角的x,y坐标;
getRawX/getRawY:返回时相对于屏幕左上角的x,y坐标
举个例子:假如现在有一个LinearLayout,它相对屏幕的绝对坐标是222,333,linearlayout中有一个button,
这个button相对LinearLayout是39,34,所以如果getX,getY就是39,34,
如果是getRawX,getRawY就是 222+39,333+34
2、TouchSlop是系统所能识别出的被认为是滑动的最小距离,这是一个常量,这个值在framework/base/core/res/res/values/config.xml中,这个值和设备有关,不同的设备这个值也不尽相同
TouchSlop获取方式: ViewConfiguration.get(getContex()).getScaledTouchSlop();
三.VelocityTracker:速度追踪,用于追踪手指在滑动过程中的速度,包括水平和垂直方向的速度,在view的ontouchEvent()方法中追踪当前滑动的速度,
代码如下:
VelocityTracker vVelocityTracker = VelocityTracker.obtain();
vVelocityTracker.addMovement(event);//此方法在onTouchEvent(Event event)中调用
// 如果想知道当前滑动的速度,可以采用以下方式:
vVelocityTracker.computeCurrentVelocity(1000);//表示 1000ms
int xVelocity = vVelocityTracker.getXVelocity();
int yVelocity = vVelocityTracker.getYVelocity();
注意两点:
1.获取速度之前必须先计算速度,vVelocityTracker.computeCurrentVelocity(1000)就是计算速度,
2.速度单位是像素,比如将时间设置1000ms,就是1s,手指在水平方向划过200,那么水平速度就是200,
速度可以是负数:手指从右向左滑就是负数,速度 = (终点 - 起点)/ 时间段
3.最后不需要的时候调用clear方法重置并回收:
VelocityTracker.clear();
vVelocityTracker.recycle();
四.GestureDetector:用户辅助检测用户的单击,滑动,长按,双击等行为,使用GestureDector也并不复杂,参考如下:
GestureDetector gestureDector = new GestureDetector(this);
解决长按屏幕后无法拖动的现象gestureDector.setIsLongPressEnabled(false);
接着接管View的ontouchEvent()方法,在view的onTouchevent()方法中添加如下代码:
int consume = gestureDector.onTouchEvent(event);
return consume;
做完上面两步就可以有选择的实现OnGestureListener和OnDoubleTapListener中的方法
OnlGestureListener:
onDown():一个action_down触发
onShowPress():一个action_down触发,他和onDown()的区别就是,没有松开,没有拖动
onSingleTapup();这是一个单击行为
onScroll();手指按下屏幕并拖动:有一个Action_down和多个Action)_move这是拖动行为
onLongPress:用户长时间按着屏幕不放,长按 onFling:用户按下屏幕,快速滑动后松开,一个down,多个move,一个up
OnDoubleTapListener:
nDoubleTap:双击,两次连续的单击组成,他不可能和onSingleTapComfirmed()并存
onSingleTapComfirmed:严格的单击行为,注意它和onSingleTapUp的区别,如果触发了onSingleTapComfirmed后面不能跟着单击行为,这只能是单击,而不可能是双击
onDoubleTapEvent:发生了双击行为,down,move.up都会触发
五.Scroller弹性滑动对象:
Scroller是一个专门用于处理滚动效果的工具类,可能在大多数情况下,我们直接使用Scroller的场景并不多,
但是很多大家所熟知的控件在内部都是使用Scroller来实现的,如ViewPager、ListView等。
而如果能够把Scroller的用法熟练掌握的话,我们自己也可以轻松实现出类似于ViewPager这样的功能。 用于实现view的弹性滑动,
我们知道,当使用view的scrollTo/scrollBy方法来滑动时,其过程是瞬间的,这种用户体验很不好,
这个时候需要使用scroller来实现过度效果的滑动,Scroller本身无法让view弹性滑动,他需要和view的computeScroll()方法配合使用才能完成这个功能,
如何实现Scroller呢?
它的典型代码是固定的:Scroller Scroller = new Scroller(this);
缓慢滚动到指定位置
private void smoothScrollTo(int destX,int destY){
int scrollX = getScrolllX();
int delta= destX - scrollX;//100ms滑动到destX、效果就是慢慢滑动
scroller.startScroll(scrollX,0,delta,0,100);
invalidate();//调用onDraw()---->调用computeScroll()
}
@override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();//--->调用onDraw()--->computeScroll(),如此反复
}
}
解析:scroller的startScroll(int startx,int starty,int dx,int dy,int duration)其实很简单,
startx,starty表示滑动的起点,dx,dy表示的是要滑动的距离,duration是滑动的时间
Scroller的startScroll是无法滑动的,真正滑动的方法是invalidate();此方法会回调ondraw(),onDraw()方法又会回调computeScroll(),
在computeScroll()中通过mScroller.computeScrollOffset()判断当前时刻应该所在的位置,如果当前位置不是startScroll设置的终点位置,
就会调用scrollTo滑动到这个时刻应该位于的位置
六、View的滑动
其实任何一个控件都是可以滚动的,因为在View类当中有scrollTo()和scrollBy()这两个方法。
通过三种方法可是实现view的滑动:
1.view本身提供的scrollTo/scrollBy方法
scrollBy实际内部也是调用了scrollTo(),他是基于当前位置的相对移动,而scrollTo则是基于所传参数的绝对移动
view的内部有两个属性mScrollX,mScrollY,这个两个属性通过getScrollX,getScrollY获取,
在滑动过程中mScrollX总是等于view的左边缘和view内容的左边缘的距离,mscrollY等于view上边缘与view内容上边缘的距离
scrollTo和ScrollBy只改变view内容的位置,并不改变view的位置
view的左边缘在view内容左边缘的右边时,mScrollX为正,反之为负;
view的上边缘在view内容上边缘的下边时,mScrollY为正,反之为负;
换句话说,上滑为正,左滑为正,否则反之
2.通过动画给view添加平移效果来实现滑动
其实平移就是一种滑动,他可以采用传统的view动画,也可以使用属性动画,
如果属性动画为了兼容3.0以下的版本,需要采用开源动画库nineoldandroids
3、改变布局参数
LinearLayout.LayoutParams marginLayoutParams = (LinearLayout.LayoutParams) scrollByBtn.getLayoutParams();
marginLayoutParams.leftMargin += 100; scrollByBtn.setLayoutParams(marginLayoutParams);
各种滑动方式的对比:
scrollTo/scrollBy是专门用于view的滑动,但是只能针对内容,不能滑动view本身
动画:操作简单,主要用于没交互的view和实现复杂的动画效果
改变布局参数:操作稍微复杂,适用于有交互的view
qi、View的弹性滑动
1.使用scroller
2.通过动画(使用valueAnimator,通过监听某一个值的变化,来计算view应该在什么位置,并使用scrollto滑动,这种方式和scrooller非常相似
3.使用延时:核心思想就是发送一系列延迟消息从而达到一个渐进的效果,具体可以使用Handler和view的postDelayed方法,
使用线程的sleep也可以实现,在while循环中不断的滑动view合理sleep
八、View的事件分发机制
1.点击事件的传递规则
点击事件的分发其实就是MotionEvent事件分发的过程,当一个MotionEvent产生以后,系统需要把这个事件传递给具体的view,这个传递的过程就是分发过程,
点击事件的分发过程由三个重要的方法共同完成:dispathTouchEvent(),onInterceptTouchEvent(),onTouchEvent();
dispathTouchEvent:
如果事件能够传递到当前view,那么此方法一定被调用,返回值受View的onInterceptTouchEvent和下级view的dispathTouchEvent的影响
onInterceptTouchEvent:
在dispathTouchEvent方法的内部调用,用来判断是否拦截某个事件,如果当前view拦截了某个事件,那么同一事件序列当中此方法不会被再次调用,
返回值表示是否拦截当前事件
onTouchEvent:
在dispathTouchEvent方法中调用,用来处理点击事件,返回值表示是否消耗此事件,如果不消耗,同一个事件序列中,当前view不会再次接受事件
以下代码来表示三者之间的区别:
@Override protected boolean dispatchHoverEvent(MotionEvent event) {
boolean consume = false;
if(onInterceptTouchEvent(event)){
consume = onTouchEvent(event);
}else {
consume = child.dispatchTouchEvent(event);
}
return consume;
}
流程梳理:
1.对于一个viewGroup,当点击事件产生以后,他的dispatchTouchEvent就会调用,如果这个ViewGroup的onInterceptTouchEvent返回true就表示要拦截当前事件,
接着事件就会交给viewgroup处理,即它的onTouchEvent就会被调用; 如果这个Viewgroup的onInterceptTouchEvent返回false就表示不拦截当前事件,
这时当前事件就会传递给它的子元素, 接着子元素的dispatchTouchEvent方法就会被调用,如此反复,直到事件被最终处理
2.当一个view需要处理事件时,如果它设置了OnTouchListener,那么OnTouchEvent的onTouch()方法就会回调,
如果ontouch返回true,onTouchEvent不会回调 如果ontouch返回false,onTouchEvent就会回调,
由此可见,view设置的onTouchListener的优先级比onTouchEvent高,
在onTouchEvent方法中,如果设置了onClickListener,那么它的onClick()方法就会被调用可以看出我们平时用的OnClickListener,
其优先级最低,处于事件的末尾3.当一个点击事件产生后,事件传递顺序:actiivty---》windows---》view,view收到事件以后就会按照分发机制进行分发,
考虑一种情况:如果view的onTouchEvent返回false,那么它的父容器的onTouchEvent()就会调用,
一次类推,如果所有的元素都不处理这个事件,那么这个事件将会最终传给activity,即Actiivty的ontouchEvent调用
结论:
1.同一个事件是指从手指触屏到手指离开,这个过程所产生的一系列事件
2.正常情况下,一个事件只能被一个view拦截消耗(但也可以通过特殊手段,强行传递给其他的view处理),
因为一旦一个元素拦截了某个事件,那么同一系列的所有事件都会直接交由他处理 因此就不会再调用onInterceptTouchEvent来询问了,
3,viewGroup默认不拦截任何事件
4.view没有onInterceptTouchEvent方法,一旦点击事件传递给他,它的ontouchEvent就会调用
5.view的ontouchEvent默认是消耗事件的,除非他是不可点击(clickable,longClickable同时为false)
6、view的enable属性不影响onTouchEvent的默认返回值,哪怕一个view是disable状态,只要它的clickable和longclickable返回true,
那么ontouchEvent就返回true
7.onclick发生的前提是当前view是可点击的,并且它收到download和up事件
8.事件总是由外到内,从父元素传递给子元素,通过requestDisallowInterceptTouchEvent方法可以在子元素干预父元素的事件分发过程,但是action_down事件除外
九.事件分发的源码解析;
Activity.dispatchTouchEvent-->getWindow().superdispatchTouchEvent--->mDecor.superDispatchtouchEvent()
getWindow.getDecorView().findViewByID(android.R.id.content).getChildAt(0)这种方式可以获取通过setContentView()设置的内容
viewGroup在如下两种情况会判断是否要拦截当前事件:
1.事件类型是Action_down 或者 mFirstTouchTarget != null,当事件有viewGroup的子元素成功处理,mFirstTouchTarget会被赋值并指向子元素,
反过来一旦viewGroup拦截此事件,mFirstTouchTarget != null不成立,导致viewGroup的onInterceptTouchEvent不会再调用
当然有一种情况:FLAG_DISALLOW_INTERCEPT标志位,这个标志位是通过requestDisallowInterceptTouchEvent方法设置的,
FLAG_DISALLOW_INTERCEPT一旦设立,viewGroup无法拦截除ACTION_DOWN以外的事件,为什么说是ACTION_DOWN以外的事件呢?
这是因为ACTION_DOWN会重置此标志
View的CLICKABLE和LONGCLICKABLEL有一个为true,那么他就会消耗这个事件,即onTouchEvent返回true,不管它是不是DISABLE状态,
当ACTION_UP事件发生时,会触发performClick方法 如果设置了onClickListener,那么performClick方法内部会调用它的onclick方法
view的LONGCLICKABLE默认是false,view的CLICKABLE是否为false,跟具体的view有关,button的CLICKABLE默认是true,textview默认是false
setonClickListener自动将view的clickable设为true,setlongClicklistener()默认将longClickable设为true;
十.滑动冲突解决方案:
这里给出了两种解决滑动冲突的方式:外部拦截法和内部拦截法
外部拦截法:点击事件经过父容器的拦截处理,如果父容器需要此事件就拦截,不需要就不拦截,外部拦截法需要重写父容器 的onInterceptTouchEvent方法,在内部做相应的拦截即可,代码如下:
父类中:
int mLastXIntercept;
int mLastYIntercept;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
intercept = false;//必须返回false,如果返回true,move,up就没法传给子view
break;
case MotionEvent.ACTION_MOVE:
if(父容器需要当前点击事件){
intercept = true;
}else {
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;//必须返回false
break;
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercept;
}
内部拦截法:需要配合requestDisallowInterceptTouchEvent方法才能正常工作,
子类中:
int mLastXIntercept;
int mLastYIntercept;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);//请求父控件不要拦截
break;
case MotionEvent.ACTION_MOVE:
int deltX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if(父容器需要当前点击事件){
getParent().requestDisallowInterceptTouchEvent(false);//请求要拦截
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return super.dispatchTouchEvent(ev);
}
父类:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if(action == MotionEvent.ACTION_DOWN){
return false;
}else {
return true;
}
}