本节主要围绕View滑动冲突和View相关的工作原理进行
本系列文章都是先列知识点、流程图、部分简要介绍然后对该内容中会出现在面试中的点进行梳理
有关知识点并没有深入讲解,对于原理流程图部分,强烈推荐配合源码查看
引入:今天群里讨论一个问题,不知道大家是否清楚
问题:在ActivityThread的main方法中loop一直是死循环,我们的生命周期都是通过系统的Handler的回调方法中进行的,包括更新界面,那在代码中执行setText方法或者view的重绘也是在handler的handleMessage方法中执行的吗?是不是所有的关于UI的更新都是走的Android的消息机制通过系统handler更新,下面选取了五个对该问题的讨论
-
首先UI刷新走的是ViewRootImpl方法,最终通过WMS刷新,同时ViewRootImpl里面有handler,这个handler是对应主线程的,WMS完成刷新回到ViewRootImpl,此时Handler再帮忙切换到主线程刷新
-
屏幕每秒有60帧的刷新,这个是依赖屏幕的刷新频率,每一帧的绘制会通过handler通知,只要屏幕亮着就不存在死循环
-
总得来说,安卓系统的显示原理,是安卓程序把经过测量布局绘制后的surface缓存数据,通过surfaceFlinger把数据渲染到显示屏,通过安卓的刷新机制来刷新数据,也就是说应用层负责绘制,系统层负责渲染,通过进程间通信把应用层需要绘制的数据传递到系统层服务,系统层服务通过刷新机制把数据更新到屏幕
-
view的创建和绘制在onResume里,所以onCreate时拿不到view的宽高,可以通过onWindowFocusChanged和view.post或者ViewTreeObserver获取
-
setText源码分析,该方法会重绘,从调用链来看,会将视图事件包装成一个Runnable,添加到MessageQueue中,Looper轮询取出,Handler#handleMessage接受到绘制事件回调,并调用ViewRootImpl的performTraversals完成最终绘制
一、View的基础知识
-
对于坐标系需要搞清楚getX()和getRawX()的区别,前者是相对于当前View的左上角的x坐标,后者是相对于整个屏幕
-
TouchSlop是系统所能识别的被认为是滑动的最小距离,如果滑动距离小于这个常量,系统将认为你没有滑动。通过ViewConfiguration.get(getContext()).getScaledTouchSlop()得到,这个常量处于frameworks/base/core/res/res/values/config.xml中,我们可以利用这个常量来过滤一些操作来提升用户体验
-
VelocityTracker是一个速度追踪类,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。例如想获取1秒内手指移动的速度
VelocityTracker velocityTracker = VelocityTracker.obtain(); velocityTracker.addMovement(event); velocityTracker.computeCurrentVelocity(1000);//1000毫秒内 int xVelocity = (int)velocityTracker.getXVelocity(); int yVelocity = (int)velocityTracker.getYVelocity(); //回收 velocityTracker.clear(); velocityTracker.recycle();
-
GestureDetector类是用来进行手势检测的,例如用户单击、滑动、长按、双击等等。一般只需要实现相应的监听事件,接管View的onTouchEvent就可以正常工作了。
-
View的scrollBy和scrollTo区别,前者最终也是调用后者,scrollTo属于绝对滑动,以(0,0)坐标为基准,scrollBy属于基于当前位置的相对滑动,具体查看View中关于这两个方法的源码即可。scrollTo/scrollBy相关介绍
-
View的三种滑动方式,scrollTo/scrollBy操作简单,适合对View内容的滑动;动画,操作简单,主要适用于没有交互的View和实现复杂的动画效果;改变布局参数,操作复杂点,适用于有交互的View。
-
弹性滑动相关
-
利用Scroller类来实现有动画效果的滑动,以下是惯用代码
Scroller scroller = new Scroller(mContext); private void smoothScrollTo(int dstX, int dstY) { int scrollX = getScrollX(); int scrollY = getScrollY(); int deltaX = dstX - scrollX; int deltaY = dstY - scrollY; scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000); //开始滑动 invalidate(); } @Override public void computeScroll() { if (scroller.computeScrollOffset()) { scrollTo(scroller.getCurrX(), scroller.getCurY()); postInvalidate(); } }
-
对上述代码解释:当执行了invalidate()后,会导致View重绘,view的draw方法中又会去调用computeScroll方法,computeScroll方法又会去向Scroller获取当前的scrollX和scrollY,再通过scrollTo方法滑动,然后调用postInvalidate()又会导致view重绘,以此循环,直到滑动过程结束。
-
当然通过动画也可以实现弹性滑动,利用ObjectAnimator类,这里不过多介绍,详细可以用BING搜索
-
还可以通过延时策略,主要是利用Handler的postDelayed方法,每次移动一点。
-
二、View的事件分发机制
-
首先可以了解一下View是怎么显示在界面上的 Activity和VIEW以及Window的关联
-
一个常见的图
-
点击事件的传递顺序 Activity -> Window -> DecorView,事件先传递给Activity,然后再传递给Window,然后再传递给顶级View,顶级View接受到事件后,按照事件分发机制去分发事件。
-
下面这是自己画的图片
-
事件分发过程可以查看该图片,建议先把源码看一遍,然后对该图会有比较深的印象。
-
首先会在Activity中调用dispatchTouchEvent分发,然后再调用ViewGroup的dispatchTouchEvent分发,如果ViewGroup调用onInterceptTouchEvent拦截,这个事件就会交给ViewGroup的onTouchEvent处理,如果不拦截,那么该事件将会传递给子类,子类的dispatchTouchEvent会被调用,只要有一个子类消费了此事件,事件传递就结束了
-
它们的关系可以通过以下伪代码表示
public boolean dispatchTouchEvent(MotionEvent ev){ boolean consume = false; if(onInterceptTouchEvent(ev)){ consume = onTouchEvent(ev); }else{ consume = child.dispatchTouchEvent(ev); } return consume; }
-
需要注意的是,在View的dispatchTouchEvent中有如下代码,从代码中可以看出,设置了onTouch监听事件,返回true将不会执行onTouchEvent方法了,所以一般监听事件内都默认了返回false。并且在onTouchEvent内,会执行onClickListener事件。
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; }
-
在ViewGroup的dispatchTouch代码中有如下代码,其中resetTouchState是重置FLAG_DISALLOW_INTERCEPT标记位的,一般用于子View当中,这里这么做的效果是,当面对ACTION_DOWN事件时,ViewGroup总是会调用onInterceptTouchEvent来询问自己是否要拦截事件。如果ViewGroup拦截事件的话,后续所有事件将会交由它处理,不再调用onInterceptTouchEvent。
@Override public boolean dispatchTouchEvent(MotionEvent ev) { ........ // Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { // Throw away all previous state when starting a new touch gesture. // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. cancelAndClearTouchTargets(ev); resetTouchState(); } // Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; } ........
-
三、滑动冲突
-
常见的滑动冲突场景
-
外部滑动方向和内部滑动方向不一致
-
外部滑动方向和内部滑动方向一致
-
上面两种情况的嵌套
-
-
解决办法有外部拦截法和内部拦截法
-
这里对上述场景的第一点进行分析,例如ViewPager + ListView场景,这里ViewPager已经帮我们处理好了,所以接下来需要模拟一个左右滑动的控件HorizontalScrollViewEx
-
外部拦截法,只需要在HorizontalScrollViewEx内,重写onInterceptTouchEvent方法,需要注意的是,不能屏蔽ACTION_DOWN方法,从源码中可以看出,一旦屏蔽了该事件,后续的ACTION_UP等事件都会直接交由自己处理,传递不到子元素。代码如下
//重写父容器HorizontalScrollViewEx的拦截方法 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://onClick事件在UP事件中调用,一旦拦截子View的onClick事件将不会触发 intercepted = false; break; default : break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; }
-
内部拦截法,这个比较复杂点,一般能用外部解决还是用外部解决吧。这个方法利用到了FLAG_DISALLOW_INTERCEPT标记位,具体代码如下
//重写父类HorizontalScrollViewEx的onInterceptTouchEvent方法 public boolean onInterceptTouchEvent (MotionEvent event) { int action = event.getAction(); if(action == MotionEvent.ACTION_DOWN) { return false; } else { return true; } } //重写子类ListView的dispatchTouchEvent方法 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); }
-
四、View的工作原理
-
在Android 8.0源码的handlerResumeActivity方法内performResumeActivity后会执行将Activity和DecorView和Window关联起来,其中ViewRoot的实现类是ViewRootImpl,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。
-
View的绘制是从ViewRoot的performTraversal方法开始的,它经过measure、layout、和draw三个过程最终将一个View绘制出来的,源码内调用流程如下图
-
measure过程决定了View的宽高,Measure完成后,可以通过getMeasureWidth和getMeasureHeight方法来获取测量后的宽高,获取的时机最后是在onResume中或者通过开头回答部分的三种方法。该measure过程,就是反复遍历整个View数进行测量。
-
measure过程中会遇到MeasureSpec,它代表了一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(测量模式下的规格大小)。其中SpecMode有三类,如以下描述
-
UNSPECIFIED 父容器不对View限制,要多大有多大
-
EXACTLY 精确大小,为SpecSize值,一般对应于match_parent和具体数据两种模式
-
AT_MOST 父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值
-
-
在源码performMeasure前,调用了getRootMeasureSpec,然后再将结果作为参数传递给performMeasure进行测量,这就是DecorView的创建过程之一,
private static int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: // Window can't resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; }
-
对于普通的View来说,一般就是指ViewGroup,它调用getChildMeasureSpce得到子元素的MeasureSpec,与父容器的Measure和子元素本身的LayoutParams有关,这就是为什么自定义View时使用了warp_content,最后却还是布满整个屏幕的原因
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us case MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us case MeasureSpec.AT_MOST: if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent asked to see how big we want to be case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
-
从上面两点可以得出普通View的MeasureSpce规则,结论如下表
-
从上表中可以知道,如果子View使用wrap_content,那么会遇到宽度直接是match_parent,所以需要重写onMeasure方法,(详细查看自定义View warp_content不起作用的原因)模板如下
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec,heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); //根据不同的模式来设置 if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(mWidth,mHeight); }else if(widthSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(mWidth,heightSpecSize); }else if(heightSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(widthSpecSize,mHeight); } }
-
ViewGroup内并没有定义measure()具体测量的过程,因为ViewGroup布局特性不同,例如LinearLayout和RelativeLayout,显然处理方式不一样,layout()方法也一样并没有实现
-
-
当ViewGroup位置确定后,它在onLayout中会遍历所有的子元素,调用其layout方法,以下是View的layout方法源码
public void layout(int l, int t, int r, int b) { if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);//这里会利用setFrame初始化View的四个顶点 if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b);//该方法并未实现,比如线性布局和相对布局一样,特性不同,子View也不定 if (shouldDrawRoundScrollbar()) { if(mRoundScrollbarRenderer == null) { mRoundScrollbarRenderer = new RoundScrollbarRenderer(this); } } else { mRoundScrollbarRenderer = null; } mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; ListenerInfo li = mListenerInfo; if (li != null && li.mOnLayoutChangeListeners != null) { ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone(); int numListeners = listenersCopy.size(); for (int i = 0; i < numListeners; ++i) { listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } } mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) { mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT; notifyEnterOrExitForAutoFillIfNeeded(true); } }
-
draw过程,首先调用drawBakcground绘制背景,然后调用onDraw()方法绘制自己,再调用dispatchDraw()方法绘制Children,最后调用onDrawScrollBars()绘制装饰,通过源码很容易查看出来。
五、自定义View相关
-
自定义的分类
-
继承View重写onDraw方法,对于warp_content和padding需要自己处理
-
继承ViewGroup派生特殊的layout,需要同理子元素的测量和布局过程
-
继承特定的View(如TextView),大多数情况下不需要重写onDraw方法
-
继承特定的ViewGrou(如LinearLayout),和第二点类似,只不过第二点更接近View的底层
-
-
比如继承View画圆
//方法一 public void onDraw(Canvas canvas){ super.onDraw(canvas); int width = getWidth(); int height = getHeight(); int radius = Math.min(width,height)/2; canvas.drawCircle(width/2,height/2,radius,mPaint); } //方法二 public void onDraw(Canvas canvas){ super.onDraw(canvas); int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingRight(); int paddingTop = getPaddingTop(); int paddingBottom = getPaddingBottom(); int width = getWidth(); int height = getHeight(); int radius = Math.min(width,height)/2; canvas.drawCircle(paddingLeft+width/2,paddingTop+height/2,radius,mPaint); }
上述两种方法都可以画圆,当宽高属性都设置为match_parent时,效果一样,但是加上了padding后,例如设置paddingTop为20dp,在方法一的处理过程中就失效了。当宽高属性设置为wrap_content时,就需要使用下面模板了,这里的mWidth和mHeight设置为200dp
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec,heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); //根据不同的模式来设置 if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(mWidth,mHeight); }else if(widthSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(mWidth,heightSpecSize); }else if(heightSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(widthSpecSize,mHeight); } }
-
继承ViewGroup派生特殊Layout,以下是重写onMeasure、onLayout简单布局模板,其实onMeasure()可以借鉴measureChildren处理方式
//onMeasure protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){ super.onMeasure(widthMeasureSpec,heightMeasureSpec); int measureWidth = 0; int measureHeight = 0; int childCount = getChildCount(); measureChildren(widthMeasureSpec,heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if(childCount == 0){ setMeasureDimension(0,0); }else if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){ View childView = getChildAt(0);//拿到子View,或者遍历处理子View measureWidth = ....; measureHeight = ....; setMeasuredDimension(measureWidth,measureHeight); }else if(widthSpecMode == MeasureSpec.AT_MOST){ measureWidth = ....; setMeasuredDimension(measureWidth,heightSpecSize); }else if(heightSpecMode == MeasureSpec.AT_MOST){ measureHeight = ....; setMeasuredDimension(widthSpecSize,measureHeight); } } //onLayout protected void onLayout(boolean changed,int l,int t,int r,int b){ int childCount = getChildCount(); for(int i = 0;i<childCount;i++){ View childView = getChildAt(i); if(childView.getVisibility() != View.GONE){ ....处理各种padding margin childView.layout(childLeft,childTop,childRight,childBottom); } } }