Android开发艺术探索 第四章 view的工作原理

mac2024-03-10  28

文章目录

第四章 view的工作原理ViewRoot与DecorViewMeasureSpecview的工作过程view工作过程的总结 获取view宽高layout过程draw过程自定义view

第四章 view的工作原理

目标: 掌握view的底层工作原理,如measure,layout,draw。通过继承view, viewGroup或现有的系统控件来实现自定义view。 再次需要注意的是viewGroup继承自view, 所以view中的方法viewGroup中都有.

ViewRoot与DecorView

DecorView是根view。在ActivityTread中, 当Activity被创建后,会将DecorView添加到window中, View decor = r.window.getDecorView(); wm.addView(decor, l); 同时创建ViewRootImpl对象(是ViewRoot的实现)并与DecorView建立联系。 ViewRootImpl对象在WindowManagerGlobal中被创建. root = new ViewRootImpl(view.getContext(), display);//这里的view就是decorview root.setView(view, wparams, panelParentView); 不懂ViewRootImpl看这里 View的三大流程均是通过ViewRootImpl来完成。performTraversals的工作流程图

MeasureSpec

是一个32位的int值,高2位代表SpecMode, 低30位代表SpecSize.是一种类似于测量规格的东西, 对view进行测量时需要用到.子view的测量需要父view的MeasureSpec(或窗口大小)和自身的LayoutParams来共同确定.SpecMode有三种类型UNSPECIFIED, EXACTLY, AT_MOST

view的工作过程

从DecorView开始分析

DecorView是窗口大小和自身LayoutParams两者共同确定自己的MeasureSpec. 此处是通过getRootMeasureSpec来获取返回值. 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; } 调用performMeasure方法执行measure函数. private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { if (mView == null) { return; } Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); try { mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);//在view中measure方法为final,不可更改 } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } } measure方法中会调用onMeasure方法所以DecorView(继承于FrameLayout)中重写了onMeasure. 同时FrameLayout(继承于viewGroup, viewGroup继承于view)自己也重写了onMeasure方法. 之后DecorView将处理结果(自身宽高的精确值)通过super.onMeasure传递到FrameLayout中处理.FrameLayout中的onMeasure方法会对子view进行遍历同时调用measureChildWithMargins. measureChildWithMargins又会调用view.measure, measure又会调用onMeasure. 直到最后只剩下view(不是viewGroup)时, 在onMeasure中调用setMeasuredDimension设置单独view的宽高.当每个字view宽高都计算完时, 加上子view各自的margin, 父view的padding后, viewGroup就能得出自身的大小.在第5条中的measureChildWithMargins, 调用了getChildMeasureSpec方法. 这个方法是普通view利用父MeasureSpec和自身lp来创建自己MeasureSpec的规则. 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);//父容器对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的属性为wrap时, SpecSize都是parentSize(即父view的剩余空间大小).所以此时在自定义view中, 要重写onMeasure方法, 在SpecMode模式为AT_MOST时设定一个默认的内部宽高.

view工作过程的总结

ViewRootImpl中调用performTraversals -> performMeasureperformMeasure -> view.measure -> onMeasure(decorView重写了view的onMeasure) -> super.OnMeasure(FrameLayout中). 因为decorView继承自FrameLayout.到了FrameLayout后, 遍历子view并调用measureChildWithMargins, measureChildWithMargins -> view.measure.decorView的工作结束(其MeasureSpec是由窗口和自身lp决定), 此时view.measure -> onMeasure系统对后面的处理是分为单独的view还是viewGroup来考虑如果是view, onMeasure -> setMeasuredDimension 从而确定好这个view的宽高.如果是viewGroup, 它没有提供一致的onMeasure方法. 因为不同的viewGroup其特性不同导致工作的逻辑不同.所以像在上面提到的FrameLayout或是LinearLayout等继承了viewGroup都是根据自己的特性重写了OnMeasure(可以查看相关的源码得到验证).在viewGroup中提供了便捷的方法 measureChildren(参数是父MeasureSpec) -> measureChild(参数是父MeasureSpec, 此方法中通过getChildMeasureSpec得到子MeasureSpec) -> child.measure(参数是子MeasureSpec), 这样形成循环, 一直到只有单个view的时候. 此时是6 中的情况.所以当自定义viewGroup时可以在重写的onMeasure中分析好逻辑, 然后灵活调用measureChildren方法. (在LinearLayout中的measureChildrenBeforeLayout方法实际上就是调用了measureChildren)不懂view测量过程看这里

获取view宽高

获取view的测量宽高: getMeasuredWidth, getMeasuredHeight获取view的最终宽高: getWidth, getHeight测量宽高和最终宽高基本上相同, 但在极端情况下系统需要多次测量才能确定最终宽高. 所以比较好的做法是在onLayout方法中去获取view的测量宽高或最终宽高如果想要在Activity已启动的时候获取某个view的宽高, 但由于Activity的生命周期与view的measure过程不是同步执行的, 所以不能直接调用1,2 中的方法. 解决方法: 1.在onWindowFocusChanged中使用 2.在view.post(runnable)的run方法中使用 3.手动使用view.measure.(P182) 4.使用ViewTreeObserver ViewTreeObserver observer = view.getViewTreeObserver(); observer.addGlobalLayoutListener(new OnGlobalLayoutListener(){ public void onGlobalLayout(){ view.getViewTreeObserver().removeGlobalLayoutListener(this); //这里使用 } })

layout过程

performTraversals 中 经过了performMeasure 到了 performLayoutperformLayout -> layout -> setFrame(设置自身位置) -> onLayout(确定所有子view的位置)view和viewGroup都没有实现onLayout方法所以自定义的时候需要自己实现onLayout方法onLayout -> child.layout

draw过程

draw 在源码中做了如下几步 绘制背景background.draw(canvas)绘制自己(onDraw)绘制children(dispatchDraw)绘制装饰(onDrawScrollBars) setWillNotDraw(Boolean ) 如果一个view不需要绘制任何内容,那么设置这个标志位为true, 系统会进行相应的优化默认情况view没有启用, viewGroup启用了在继承viewGroup的自定义控件需要通过onDraw来绘制内容时, 我们需要显示的关闭

自定义view

自定义view 继承view重写onDraw方法 1.重写onDraw方法,并且需要自己支持wrap_content(设置默认宽高setMeasuredDimension), padding.继承ViewGroup派生自己的layout 1.需要合理处理ViewGroup的 测量, 布局过程. 并同时处理子元素的测量和布局过程.继承特定的View(TextView之类) 1.较为容易实现, 不需要自己支持wrap_content, padding.继承特定的ViewGroup(LinearLayout之类) 1.比2中的方法方便一些, 一般2中能实现的效果, 4都能实现. 但2更接近底层. 此方法不需要自己处理ViewGroup的测量和布局过程 注意事项 view中如果有线程或动画, 需要及时停止, 否则会造成内存泄漏Activity中的onAttachedToWindow和onDetachedFromWindow分别是开启和关闭的好地方.处理好滑动嵌套中的滑动冲突问题 提供自定义属性 values目录下创建自定义属性的xml文件, 命名最好以attrs_开头 <resource> <declare-styleable name="xxx"> <--!自定义属性集合名称--> <attr name="circle_color" format="color"/> <--!格式(format)是相应的类型,可以查看相应的文档--> </declare-styleable> </resource> 在自定义的view构造方法中(有三种, 是其中attributeSet那种) TypeArray a = context.obtainStyledAttributes(自定义xml名, R.styleable.xxx) my = a.getColor(styleable.xxx_circle_color, 默认值) my.recycle();//实现资源 添加schemas声明(xmlns:app=http://schemas.android.com/apk/res-auto), app是前缀, 可以修改 继承ViewGroup实现自己的Layout 主要看onMeasure和onLayout方法, 不规范的地方有 没有处理自己的padding和子元素的margin(在onMeasure和onLayout中都要去处理)没有子元素时不应该直接把宽高设置为0, 而应该根据LayoutParams中的宽高来做相应的处理

手机上看不全下面代码HorizontalScrollViewEx

import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewGroup; import android.widget.Scroller; public class HorizontalScrollViewEx extends ViewGroup { private static final String TAG = "HorizontalScrollViewEx"; private int mChildrenSize; private int mChildWidth; private int mChildIndex; // 分别记录上次滑动的坐标 private int mLastX = 0; private int mLastY = 0; // 分别记录上次滑动的坐标(onInterceptTouchEvent) private int mLastXIntercept = 0; private int mLastYIntercept = 0; private Scroller mScroller; private VelocityTracker mVelocityTracker; public HorizontalScrollViewEx(Context context) { super(context); init(); } public HorizontalScrollViewEx(Context context, AttributeSet attrs) { super(context, attrs); init(); } public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init() { if (mScroller == null) { mScroller = new Scroller(getContext()); mVelocityTracker = VelocityTracker.obtain(); } } @Override 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; if (!mScroller.isFinished()) { mScroller.abortAnimation(); intercepted = true; } break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastXIntercept; int deltaY = y - mLastYIntercept; if (Math.abs(deltaX) > Math.abs(deltaY)) { intercepted = true; } else { intercepted = false; } break; } case MotionEvent.ACTION_UP: { intercepted = false; break; } default: break; } Log.d(TAG, "intercepted=" + intercepted); mLastX = x; mLastY = y; mLastXIntercept = x; mLastYIntercept = y; return intercepted; } @Override public boolean onTouchEvent(MotionEvent event) { mVelocityTracker.addMovement(event); int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { if (!mScroller.isFinished()) { mScroller.abortAnimation(); } break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastX; int deltaY = y - mLastY; scrollBy(-deltaX, 0); break; } case MotionEvent.ACTION_UP: { int scrollX = getScrollX(); mVelocityTracker.computeCurrentVelocity(1000); float xVelocity = mVelocityTracker.getXVelocity(); if (Math.abs(xVelocity) >= 50) { mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1; } else { mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth; } mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1)); int dx = mChildIndex * mChildWidth - scrollX; smoothScrollBy(dx, 0); mVelocityTracker.clear(); break; } default: break; } mLastX = x; mLastY = y; return true; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measuredWidth = 0; int measuredHeight = 0; final int childCount = getChildCount(); measureChildren(widthMeasureSpec, heightMeasureSpec); int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); if (childCount == 0) { setMeasuredDimension(0, 0); } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { final View childView = getChildAt(0); measuredWidth = childView.getMeasuredWidth() * childCount; measuredHeight = childView.getMeasuredHeight(); setMeasuredDimension(measuredWidth, measuredHeight); } else if (heightSpecMode == MeasureSpec.AT_MOST) { final View childView = getChildAt(0); measuredHeight = childView.getMeasuredHeight(); setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight()); } else if (widthSpecMode == MeasureSpec.AT_MOST) { final View childView = getChildAt(0); measuredWidth = childView.getMeasuredWidth() * childCount; setMeasuredDimension(measuredWidth, heightSpaceSize); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childLeft = 0; final int childCount = getChildCount(); mChildrenSize = childCount; for (int i = 0; i < childCount; i++) { final View childView = getChildAt(i); if (childView.getVisibility() != View.GONE) { final int childWidth = childView.getMeasuredWidth(); mChildWidth = childWidth; childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight()); childLeft += childWidth; } } } private void smoothScrollBy(int dx, int dy) { mScroller.startScroll(getScrollX(), 0, dx, 0, 500); invalidate(); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } } @Override protected void onDetachedFromWindow() { mVelocityTracker.recycle(); super.onDetachedFromWindow(); } }
最新回复(0)