打造View的逻辑闭环——展示,事件,与绘制全流程
如果你不了解View,那就说明你没有真正入门android
无论是TextView小控件,还是LineLayout这种大容器,都是View演化而来,TextView也继承自View
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {}
LineLayout这类布局控件特殊一点,来自ViewGroup,而ViewGroup继承自View
public class RelativeLayout extends ViewGroup {} public abstract class ViewGroup extends View implements ViewParent, ViewManager {}
可以把view比作水,很多的水聚在一起是一滩水(viewGroup),但是本质上还是水(view)
除了展示之外,View必须要有完善的滑动,点击策略,这是手机上最高频的操作,接下来,我们就详细了解View的展示,滑动,事件和绘制原理。
展示方法
要想展示,知道哪个控件放在哪,就需要精确定位,这里我们使用坐标系,有两种
android坐标绝对定位
最简单的是是将左上角作为坐标原点,右侧是x轴正方向,下侧是y轴正方向
使用getRawX()和getRawY()方法获取x,y坐标,这是一种绝对定位的方法
view坐标相对定位
由于android中的空间是层层嵌套的,所以一个子控件可以通过其对于父控件的相对位置来看位置,具体方法如下(图来自网络)
常用的比如获取view的宽高,
width = getRight() - getLeft(); height = getBottom() - getTop ();
当然,系统已经有getWidth和getHeight方法了,而他们内部逻辑也是这个
/** * Return the width of your view. * * @return The width of your view, in pixels. */ @ViewDebug.ExportedProperty(category = "layout") public final int getWidth() { return mRight - mLeft; }
滑动与事件处理
滑动事件
在滑动方面,android和其他语言写的UI一样,都是点击的时候,记录下Down的坐标,然后记录手指滑动后的UP坐标,算出偏移量,通过偏移量来修改View的坐标,当手指在手机上滑动的时候,会触发onTouchEvent事件,如果你想自定义操作,可以重写这个方法
public boolean onTouchEvent(MotionEvent event) { switch (action) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_CANCEL://? case MotionEvent.ACTION_MOVE: }
另外三个都好理解,ACTION_CANCEL是什么情况?
举个例子,比如你一个LineLayout中滑动一个View,但是滑到了LineLayout之外的区域,此时的View肯定不能出去,此时就可以触发ACTION_CANCEL,你可以设置回到原位,或者是让View留在边缘
在ViewGroup中还有一个onInterceptTouchEvent方法,再配合上android中的各种嵌套的View,这也是令很多人困惑的地方,这里涉及到事件消费的问题。
事件处理
为什么要有这个问题?
试想一下,手机上巴掌大的地方,嵌套view肯定是到处都有的
当我点击蓝***域的TextView的时候,实际上也在点击RelativeLayout和LinearLayout
那么,android如何知道点击的是哪个控件呢?
方法就是事件拦截拦截这个事件并执行对应逻辑,就是一次事件消费,如果拦截到了,不执行逻辑,就会放掉,给其他控件拦截,依次递归下去
上面加粗的拦截和执行对应了view中的两个方法 onIntercerptTouchEvent和onTouchEvent
显然,第一个拦截到的view是最外层的view,LinearLayout
onIntercerptTouchEvent
如果你对外层的LinearLayout重写了onIntercerptTouchEvent方法,返回值为false,表示他放掉这个事件,进入内部的RelativeLayout,同理,哪个控件的onIntercerptTouchEvent方法返回为true,表示哪个控件要拦截此事件。
注意,android为了高效,拦截到的传入事件仅仅只有down(参考上文中onTouchEvent的不同case),当确认onIntercerptTouchEvent的返回值为true后,拦截事件的move,up等会和down直接传入到当前控件的onTouchEvent开始执行
如果返回值为false,证明当前控件放掉此事件,那么move和down一起会留在当前控件的onIntercerptTouchEvent中,一并传入下一个拦截控件。
注意onIntercerptTouchEvent只在ViewGroup中有,原因很简单,因为只有他能嵌套View,而默认返回值是false,一般ViewGoup不轻易处理事件,而是交给子View,这也符合我们对他“容器”的直观感受。
onTouchEvent
假设最后传到了TextView,他没有办法往下传了,难道他必须消费此事件吗?
不是的,他的onTouchEvent也有返回值,return false表示不愿意消费此事件,这样,打包好的事件(down,up,move等)会一并返回RelativeLaout中
如图
另外还有一个dispatchTouchEvent()方法,负责分发事件,在这里单独说,是方便大家拆开理解,更简单些,上面的逻辑虽然闭环了,但是还缺一个,当用户点击控件的时候,控件是怎么能够拦截到的呢?
dispatchTouchEvent内部包含一个onTouchListener,这个东西放在activity或者fragement中首先拿到事件,交给dispatchTouchEvent统一管理,然后分发给对应ViewGroup的onIntercerptTouchEvent,就可以走上面的逻辑了。由于本文更多是理解原理,所以不做具体实现。
绘制
上面的讲解都是为了本流程服务的,是分散的知识点,接下来,我们将其串起来
View的工作流程就是,测量,布局和绘制,分别通过三个方法,如果要自定义View,则需要对其进行重写
measure
View中的measure必定会测量View自己,但是如果这个View是一个ViewGroup,还会遍历里面的View,调用他们自己的measure来测量他们自己,这是一个递归的过程
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
这里的源代码只有一行,也就是获取默认宽高并测量
相信你一定用过wrap_content,就是让控件大小刚好包裹住内容,如果在xml中设置宽高为定值,就不需要measure了,正是因为我们会设置wrap_content或者match_parent,此时就会调用view的onMeasure()方法
Match_parent
对于match_parent,只需要知道当前View的父控件,将他的Size赋值给到当前View即可,所以我们要做两件事,1. 找到最初的ViewGroup控件测量,2. 将测量数据往下传递到最小的View
Wrap_content
Wrap_content是刚刚好包裹住内部内容的最小值,所以刚好相反,是算出子控件的大小
ViewGroup如何传信息给到子View?
MeasureSpec类,这个类保存两个数据
- 子View的父控件具体尺寸
- 父控件对子View的限制类型
第一个好理解,毕竟match_parent传递就靠这个
第二个的限制类型有三种
public static final int UNSPECIFIED = 0 << MODE_SHIFT; //不限制大小 public static final int EXACTLY = 1 << MODE_SHIFT; //我给你的精确尺寸 public static final int AT_MOST = 2 << MODE_SHIFT; //不超过我的大小情况下,给你恰好包裹内容的尺寸
所以整个测量的方法就是:
父布局先measure自己,然后在自己的onMeasure,调用child.measure,然后子View会根据父布局的限制信息,再结合自己的content大小,综合测量自己的尺寸,然后通过setMeasuredDimension方法保存数据,
layout
layout用来确认ViewGroup子元素的位置,
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); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b); ... }
可以看到,首先初始化左,顶,底,右坐标,然后setFrame进行设定,当四个顶点确定后,view在其父容器中的位置就定了,哪怕他再奇形怪状,也被关在了这四个坐标构成的矩形里面,接下来,调用onLayout()方法
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { }
View中的onLayout()为空,表示我们需要自己重写,我们可以看看RelativeLayout中的重写
protected void onLayout(boolean changed, int l, int t, int r, int b) { // The layout has actually already been performed and the positions // cached. Apply the cached values to the children. final int count = getChildCount(); for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child.getVisibility() != GONE) { RelativeLayout.LayoutParams st = (RelativeLayout.LayoutParams) child.getLayoutParams(); child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom); } } }
这里获得了所有的子控件,并调用子控件的layout方法,因为RelativeLayout可能嵌套其他Layout,这里的getLayoutParams就是用来获得具体的位置参数的,显然,如果要修改view的位置,可以直接调用setLayoutParams
总结一下,Layout方法确定自己的坐标,然后调用onLayout并执行子控件Layout()方法以获得子控件的坐标
draw
measure是测量View的大小,layout是确定View的位置,万事俱备,只剩下将View绘制出来了,在draw源码中有6个步骤
/* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background 绘制背景 * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content 绘制内容 * 4. Draw children 绘制子控件 * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) 绘制装饰 */
其中2,5步骤是图层相关操作,但是我们正常开发一般不用,所以可以跳过,在draw()源码中,上面的步骤对应下面的源码
// Step 1, 绘制背景 drawBackground(canvas); // Step 3, 绘制内容 onDraw(canvas); // Step 4, 绘制子控件 dispatchDraw(canvas); // Step 6, 绘制装饰 onDrawForeground(canvas);
当然,我们自定义画一个view也不需要全部都重写,一般onDraw()方法绘制内容即可,其他的使用draw()默认的即可,最简单的onDraw就是画一个圆形,使用Canvas绘制
protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawCircle(width, height, radius, mPaint) }
上面的传入的参数只需要自己,这样就可以画出一个View了,更复杂有趣的View,我们将在自定义View中具体实现,这里只需了解原理。
总结与面试提高
对于View,我们需要掌握他的展示,滑动,事件处理机制和绘制机制,其中
展示需要明白相对位置和绝对位置表示,
滑动与事件处理需要明白down,up,move这些事件从用户点击到最终被消费所经历的过程,
绘制需要明白view如何知道自己的宽高,位置和图像
下面是一些常见的面试题
面试官:MotionEvent是什么?包含几种事件?
小松内心os(这就是滑动与事件处理章节)
于是不慌不忙的忽悠道:MotionEvent是用户点击屏幕后所生成的事件,主要分为
ACTION_DOWN ACTION_MOV ACTION_UP ACTION_CANCELL
他们分别表示……
那么这些事件从用户点击后是如何生效的呢?
是因为有onTouchEvent……
那么这些事件处理后是如何绘制新图像的呢?
是因为有measure……
balabala……
面试官:好吧,View的内容我们过了,下一道!
不论面试官问什么问题,可参考上面的思路娓娓道来,更加清晰符合逻辑,也很好记
android面试