参考文章:要点提炼|开发艺术之View
- view基础
- view的滑动(三种)
- view弹性滑动(三种)
- view事件分发机制
- view滑动冲突
1 view基础
1.1什么是view
View是所有控件的基类,其中也包括ViewGroup在内。ViewGroup是代表着控件的集合,ViewGroup可以包括多个ViewGroup或多个View。从某种角度上来讲Android中的控件可以分为两大类:View与ViewGroup。通过ViewGroup,整个界面的控件形成了一个树形结构,上层的控件要负责测量与绘制下层的控件,并传递交互事件。在每棵控件树的顶部都存在着一个根ViewGroup对象,它是整棵控件树的核心所在,所有的交互管理事件都由它来统一调度和分配,从而对整个视图进行整体控制。
1.2view的位置参数
1.2.1Android坐标系:以屏幕的左上角为坐标原点,向右为x轴增大方向,向下为y轴增大方向。
1.2.2View的位置由它的四个顶点决定,分别对应View的四个属性:top、left、right、bottom。其中left是左上角的横坐标,right是右下角的横坐标,top是左上角的纵坐标,bottom是右下角的纵坐标。
- 注意这些坐标是相对于View父容器而言,是一种相对的坐标。
- 所以View的宽高和坐标关系:width = right - left,height = bottom - top,这六个属性也可以利用View的get方法获取,如:left = getLeft();right = getRight();等等
1.2.3android3.0加入额外几个参数:x,y,translationX、translationY。其中x和y是View左上角的坐标,translationX和translationY是View左上角相对于父容器的偏移量,它们默认值是0。这些参数也是相对于View父容器
- x = left + translationX,y = top + translationY,x和left不同体现在:left是View的初始坐标,在绘制完毕后就不会再改变;而x是View偏移后的实时坐标,是实际坐标。y和top的区别同理。
- 安卓也提供了相应的get/set方法,在onCreate()方法里无法获取到View的坐标参数,这是因为此时View还未开始绘制,全部坐标参数将都是0
1.3MotionEvent和TouchSlop()(触控事件、最小距离)
1.3.1MotionEvent:是手指触摸屏幕锁产生的一系列事件。典型事件有:
- ACTION_DOWN:手指刚接触屏幕
- ACTION_MOVE:手指在屏幕上滑动
- ACTION_UP:手指在屏幕上松开的一瞬间
- (事件列:从手指接触屏幕至手指离开屏幕,这个过程产生的一系列事件,任何事件列都是以DOWN事件开始,UP事件结束,中间有无数的MOVE事件。)
通过MotionEvent 对象可以得到触摸事件的x、y坐标。其中通过getX()、getY()可获取相对于当前view左上角的x、y坐标;通过getRawX()、getRawY()可获取相对于手机屏幕左上角的x,y坐标。
1.3.2TouchSlop:系统所能识别的被认为是滑动的最小距离。即当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。(该常量和设备有关,可用它来判断用户的滑动是否达到阈值,获取方法:ViewConfiguration.get(getContext()).getScaledTouchSlop()。)
1.4VelocityTracker和GestureDetector和scoller(速度追踪、手势检测、弹性滑动对象)
1.4.1VelocityTracker:速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。
- 首先,必须在view的onTouchEvent方法中追踪当前单击事件的速度
- 接着在ACTION_UP事件中获取当前的速度。(这里计算的是1000ms时间间隔移动的像素值,假设像素是100,即速度是每秒100像素。(如何计算的?速度=(终点位置-初始位置)/时间,(所以水平方向由右向左速度就是负的,由左向右速度就是正的))
- 最后,当不需要使用它的时候,需要调用clear方法来重置并回收内存)
VelocityTracker velocityTracker = VelocityTracker.obtain();//实例化一个VelocityTracker 对象
velocityTracker.addMovement(event);//添加追踪事件
//第一步,获取速度,设置时间间隔为1s;第二步,计算速度
velocityTracker .computeCurrentVelocity(1000);//获取速度前先计算速度,这里计算的是在1000ms内
float xVelocity = velocityTracker .getXVelocity();//得到的是1000ms内手指在水平方向从左向右滑过的像素数,即水平速度
float yVelocity = velocityTracker .getYVelocity();//得到的是1000ms内手指在水平方向从上向下滑过的像素数,垂直速度
//第三步,调用clear方法来重置并回收内存
velocityTracker.clear();
velocityTracker.recycle();
1.4.2GestureDetector:手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。
- 创建一个GestureDetecor对象并实现OnGestureListener接口,根据需要实现单击等方法
- 接着,接管目标view的onTouchEvent方法,在待监听view的onTouchEvent方法中添加
- 然后,就可以有选择的实现OnGestureListener和OnDoubleTapListener中的方法了
建议:如果只是监听滑动操作,建议在onTouchEvent中实现;如果要监听双击这种行为,则使用GestureDetector 。
GestureDetector mGestureDetector = new GestureDetector(this);//实例化一个GestureDetector对象
mGestureDetector.setIsLongpressEnabled(false);// 解决长按屏幕后无法拖动的现象
boolean consume = mGestureDetector.onTouchEvent(event);//接管目标view的onTouchEvent方法
return consume;
1.4.3Scroller实现过度滑动的效果(需要view的computeScroll方法配合使用才能完成这个功能,接下来在详解)
2 view的滑动(三种)
比如下拉刷新和slideMenu,他们的基础都是滑动,滑动在Android开发中有很重要的作用。
- 通过View本身提供的scrollTo/scrollBy方法【推荐阅读:scrollTo/scrollBy 使用详解】
- 两者区别:scrollBy是内部调用了scrollTo的,它是基于当前位置的相对滑动;而scrollTo是绝对滑动,因此如果利用相同输入参数多次调用scrollTo()方法,由于View初始位置是不变只会出现一次View滚动的效果而不是多次。
- 两者都只能对view内容进行滑动,而不能使view本身滑动。
- 通过动画给View施加平移效果
- 主要通过改变View的translationX和translationY参数来实现
- 可用view动画
- 也可以采用属性动画,如果使用属性动画的话,为了能够兼容3.0以下版本,需要采用开源动画库nineoldandroids。
- 注意View动画的View移动只是位置移动,并不能真正的改变view的位置,而属性动画可以。
- 通过改变View的LayoutParams使得View重新布局
- 比如将一个View向右移动100像素,向右,只需要把它的marginLeft参数增大即可
MarginLayoutParams params = (MarginLayoutParams) btn.getLayoutParams();
params.leftMargin += 100;
btn.requestLayout();// 请求重新对View进行measure、layout
三种方式对比:
- scrollTo/scrollBy:操作简单,适合对view内容滑动。非平滑
- 动画:操作简单,主要适用于没有交互的view和实现复杂的动画效果
- 改变LayoutParams:操作稍微复杂,适用于有交互的view。非平滑
3 view弹性滑动(三种)
【推荐阅读:View滑动与实现滑动的几种方法】
(对弹性滑动完成总时间有精确要求的使用场景下,使用延时策略是一个不太合适的选择。这几种滑动方式侧重的是滑动的思想,在实际使用中可以灵活的扩展实现想要的效果)
3.1通过动画:动画本身就是一种渐近的过程,故可通过动画来实现弹性滑动。方法是:(使用动画滑动的话,滑动完后若有单击事件,那么只会在view原始的位置才会有点击事件。因为在系统里)
//在100ms内使得View从原始位置向右平移100像素
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
3.2使用延时策略:通过发送一系列延时信息从而达到一种渐近式的效果,具体可以通过Handler和View的postDelayed方法,也可使用线程的sleep方法。
3.3使用Scroller:
Scroller的惯用代码:
Scroller scroller = new Scroller(mContext); //实例化一个Scroller对象
private void smoothScrollTo(int dstX, int dstY) {
int scrollX = getScrollX();//View的左边缘到其内容左边缘的距离
int scrollY = getScrollY();//View的上边缘到其内容上边缘的距离
int deltaX = dstX - scrollX;//x方向滑动的位移量
int deltaY = dstY - scrollY;//y方向滑动的位移量
scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000); //开始滑动
invalidate(); //刷新界面
}
@Override//计算一段时间间隔内偏移的距离,并返回是否滚动结束的标记
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurY());
postInvalidate();//通过不断的重绘不断的调用computeScroll方法
}
}
startScroll源码如下,可见它并没有进行实际的滑动操作,而是通过后续invalidate()方法去做滑动动作。
(具体过程:在MotionEvent.ACTION_UP事件触发时调用startScroll方法->马上调用invalidate/postInvalidate方法->会请求View重绘,导致View.draw方法被执行->会调用View.computeScroll方法,此方法是空实现,需要自己处理逻辑。具体逻辑是:先判断computeScrollOffset,若为true(表示滚动未结束),则执行scrollTo方法,它会再次调用postInvalidate,如此反复执行,直到返回值为false。原理:Scroll的computeScrollOffset()根据时间的流逝动态计算一小段时间里View滑动的距离,并得到当前View位置,再通过scrollTo继续滑动。即把一次滑动拆分成无数次小距离滑动从而实现弹性滑动。)
public void startScroll(int startX,int startY,int dx,int dy,int duration){
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;//滑动时间
mStartTime = AnimationUtils.currentAminationTimeMills();//开始时间
mStartX = startX;//滑动起点
mStartY = startY;//滑动起点
mFinalX = startX + dx;//滑动终点
mFinalY = startY + dy;//滑动终点
mDeltaX = dx;//滑动距离
mDeltaY = dy;//滑动距离
mDurationReciprocal = 1.0f / (float)mDuration;
}
4 view事件分发机制
事件分发本质:就是对MotionEvent事件分发的过程。即当一个MotionEvent产生了以后,系统需要将这个点击事件传递到一个具体的View上。点击事件的传递顺序:Activity(Window) -> ViewGroup -> 根View(->子view)【补充阅读:对Activity、View、Window的理解】
主要用三个方法来共同完成的:【推荐阅读:Android事件分发机制详解(源码)】
-
dispatchTouchEvent:进行事件的分发(传递)。返回值是 boolean 类型,受当前onTouchEvent和下级view的dispatchTouchEvent影响
-
onInterceptTouchEvent:对事件进行拦截。该方法只在ViewGroup中有,View(不包含 ViewGroup)是没有的。一旦拦截,则执行ViewGroup的onTouchEvent,在ViewGroup中处理事件,而不接着分发给View。且只调用一次,所以后面的事件都会交给ViewGroup处理。
-
onTouchEvent:进行事件处理。
伪代码
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume=onTouchEvent(ev);
} else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
自己的理解:组长view接到了任务事件,组长若拦截了就进行处理并返回true不进行分发了,若不拦截就分发给组员view,组员view重复划线操作,直到最后一个组员view
5 view滑动冲突
一般情况下,在一个界面里存在内外两层可同时滑动的情况时,会出现滑动冲突现象。
常见滑动冲突场景:
- 外部滑动和内部滑动方向不一致:如ViewPager嵌套ListView或scrollView(实际这么用没问题,因为ViewPager内部已处理过)。
- 外部滑动方向和内部滑动方向一致:如ScrollView嵌套ListView(实际上也已被解决)。
- 上面两种情况的嵌套
滑动冲突的处理规则:
- 对场景一:(根据他们的特征来解决滑动冲突)当用户左右/上下滑动时让外部View拦截点击事件,当用户上下/左右滑动时让内部View拦截点击事件。即根据滑动的方向判断谁来拦截事件。关于判断是上下滑动还是左右滑动,可根据滑动的距离或者滑动的角度去判断。
- 对场景二:一般从业务上找突破点。即根据业务需求,规定何时让外部View拦截事件何时由内部View拦截事件。
- 对场景三:相对复杂,可同样根据需求在业务上找到突破点。
滑动冲突的解决方式:【推荐阅读:一文解决Android View滑动冲突】
- 外部拦截法:指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。(需要重写父容器的onInterceptTouchEvent方法,在内部做出相应的拦截。)
//伪代码
//重写父容器的拦截方法
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://对于ACTION_DOWN事件必须返回false,一旦拦截后续事件将不能传递给子View
intercepted = false;
break;
case MotionEvent.ACTION_MOVE://对于ACTION_MOVE事件根据需要决定是否拦截
if (父容器需要当前事件) {
intercepted = true;
} else {
intercepted = flase;
}
break;
}
case MotionEvent.ACTION_UP://对于ACTION_UP事件必须返回false,一旦拦截子View的onClick事件将不会触发
intercepted = false;
break;
default : break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
- 内部拦截法:指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。(需要配合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);//为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)方法时,父元素才能继续拦截所需的事件。因此,父View需要重写onInterceptTouchEvent方法
public boolean onInterceptTouchEvent (MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
内部拦截法要求父容器不能拦截ACTION_DOWN的原因:由于该事件并不受FLAG_DISALLOW_INTERCEPT(由requestDisallowInterceptTouchEvent方法设置)标记位控制,一旦ACTION_DOWN事件到来,该标记位会被重置。所以一旦父容器拦截了该事件,那么所有的事件都不会传递给子View,内部拦截法也就失效了。