初识 ViewRoot 和 DecorView
ViewRoot ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带。 在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象并将其和 DecorView 建立关联。
View 的三大流程均是通过它来完成的
Measure :测量 View 的宽/高 可通过 getMeasuredWidth 和 getMeasuredHeight 获取 View 测量后的宽/高。
Layout :确定 View 在父容器中的放置位置 可通过 getTop 等方法获取四个顶点的位置。 可通过 getWidth 和 getHeight 方法获取 View 的最终宽/高。
Draw :将 View 绘制在屏幕上 此方法完成后 View 的内容才能呈现在屏幕上。
View 绘制流程由 ViewRoot 的 performTraversals 方法开始 –> performMeasure –> measure –> onMeasure –> 对所有子元素进行 measure 过程 –> 接着由子元素重复这个过程,直到 View 树的遍历。其它两个流程类似,只不过 performDraw 的传递过程在 draw 方法中通过 dispatchDraw 来实现的。
DecorView 顶级 View,其实是一个 FrameLayout,一般内部包含一个竖直方向的 LinearLayout(两部分,上面标题栏,下面内容栏。具体情况和版本与主题有关)。 在 Activity 中通过 setContentView 设置的布局就是被加到内容栏(id 为 content 的 FrameLayout)中。
1 2 3 4 5 ViewGroup content = findViewById(android.R.id.content);content.getChildAt(0 );
理解 MeasureSpec
MeasureSpec 它代表一个 32 位 int 值,高 2 位代表 SpecMode(测量模式),低 30 位代表 SpecSize(在某种测量模式下的规格大小)。 SpecMode 有三类:
UNSPECIFIED 父容器不会 View 有任何限制,一般用于系统内部,表示一种测量的状态,多次 Measure 的情形,一般不需要关注此模式。
EXACTLY 父容器已经检测出 View 所需的精确大小,这时 View 的最终大小就是 SpecSize 所指定的值。 它对应于 LayoutParams 中的 match_parent 和具体数值这两种模式。
AT_MOST 父容器指定一个可用大小即 SpecSize,View 的大小不能大于这个值,具体什么值要看不同 View 的具体实现。 它对应于 LayoutParams 中的 wrap_content。
MeasureSpec 和 LayoutParams 的对应关系 对于 DecorView,其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 来共同决定。 DecorView 的 MeasureSpec 产生过程,遵守如下规则:
LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小。
LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。
固定大小(比如 100dp):精确模式,大小为 LayoutParams 中指定的大小。
对于普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定的。
当 View 采用固定宽/高时:不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec 都是精确模式并且其大小遵循 LayoutParams 中的大小。
当 View 的宽/高是 match_parent 时:如果父容器的模式是精准模式,那 View 也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式,那 View 也是最大模式并且其大小不会超过父容器的剩余空间。
当 View 的宽/高是 wrap_content 时:不管父容器的模式是什么,View 的模式总是最大化并且大小不能超过父容器的剩余空间。
View 的工作流程 measure 过程 View 的 measure 过程 由其 measure 方法(final 类型)完成,measure 会去调用 onMeasure 方法。 onMeausre –> 通过 setMeasuredDimension 设置 View 宽/高的测量值 –> 其参数为 getDefaultSize() –> 其内部会对三种模式进行赋值。
UNSPECIFIED View 宽/高为第一个参数也就是 getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 的返回值。 关于 getSuggestedMinimumWidth() ,如果 View 没有设置背景,则返回 android:minWidth 值,默认为 0,如果设置了背景,则返回 android:minWidth 和背景的最小宽度中的最大值。
另外两种才需要关注 View 的宽高由 specSize 决定。 直接继承 View 的自定义控件需重写 onMeasure() 并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content(specMode 是 AT_MOST 模式) 就相当于使用 match_parent。
ViewGroup 的 measure 过程 除了自己的 measure 过程外,还会遍历调用所有子元素的 measure 方法(通过 measureChild())。 measureChild() –> 取出子元素 LayoutParams –> 通过 getChildMeasureSpec 创建子元素的 MeasureSpec –>将其传递给 View 的 measure() 进行测量
某些情况下,系统可能需要多次 measure 才能确定最终宽/高,这种情况下得到的值可能不准确。建议在 onLayout() 中获宽/高取值。
启动三回调中均无法正确得到某个 View 的宽/高信息,因为 View 的 measure 过程和 Activity 的生命周期方法不是同步执行的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 public class MainActivity extends AppCompatActivity { private TextView tv; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv = findViewById(R.id.tv); log("" ); measure(); } @Override public void onWindowFocusChanged (boolean hasFocus) { super .onWindowFocusChanged(hasFocus); if (hasFocus){ log("onWindowFocusChanged:" ); } } @Override protected void onStart () { super .onStart(); tv.post(new Runnable () { @Override public void run () { log("post:" ); } }); final ViewTreeObserver observer = tv.getViewTreeObserver(); observer.addOnGlobalLayoutListener(new ViewTreeObserver .OnGlobalLayoutListener() { @Override public void onGlobalLayout () { tv.getViewTreeObserver().removeGlobalOnLayoutListener(this ); log("ViewTreeObserver:" ); } }); } private void measure () { int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30 )-1 , View.MeasureSpec.AT_MOST); int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30 )-1 , View.MeasureSpec.AT_MOST); tv.measure(widthMeasureSpec,heightMeasureSpec); log("measure:" ); } private void log (String str) { Log.e("TAG" ,str+" " +tv.getMeasuredWidth()+" " +tv.getMeasuredHeight()); } }
layout 过程 它的作用是 ViewGroup 用来确定子元素的位置,在 onLayout() 中遍历所有子元素并调用其 layout(),在 layout() 会先通过 setFrame() 设置 View 的四个顶点位置,接着会调用 onLayout()。 layout() 确定本身位置,而 onLayout() 确定所有子元素位置。
draw 过程 View 的绘制过程:
绘制背景 :background.draw(canvas)
绘制自己 :onDraw(canvas)
绘制 :children:dispatchDraw(canvas) 会遍历所有子元素的 draw()
绘制装饰 :onDrawScrollBars(canvas)
当一个 View 不需绘制任何内容,可通过 setWillNotDraw(boolean willNotDraw) 设置这个标记位为 true,系统会进行相应的优化。 默认,View 没启用这个标记位,而 ViewGroup 启动。
自定义 View
自定义 View 的分类
继承 View 重写 onDraw() 主要用于实现一些不规则效果。并且要自己支持 wrap_content 和 padding。
继承 ViewGroup 派生特殊的 Layout 主要用于实现自定义的布局。需要合适的处理 ViewGroup 和子元素的测量、布局这两个过程。
继承特定 View (比如 TextView) 一般用于扩展某种已有的 View 功能。这种不需自己支持 wrap_content 和 padding 等。
继承特定 ViewGroup(比如 LinearLayout) 这种方法不需自己处理 ViewGroup 的测量和布局两个过程。它与方法 2 的区别在于,后者更接近 View 的底层。
自定义 View 须知
让 View 支持 wrap_content
如有必要,让 View 支持 padding
尽量不要在 View 中使用 Handler
及时停止 View 中的线程和动画
处理好滑动冲突
自定义 View 示例 1、继承 View 重写 onDraw()
attrs.xml 1 2 3 4 5 6 7 <?xml version="1.0" encoding="utf-8" ?> <resources > <declare-styleable name ="CircleView" > <attr name ="circle_color" format ="color" /> </declare-styleable > </resources >
activity_main.xml 1 2 3 4 5 6 7 <com.example.myapplication.CircleView android:layout_width ="wrap_content" android:layout_height ="100dp" android:background ="#000000" android:layout_margin ="20dp" android:padding ="10dp" app:circle_color ="@android:color/holo_green_light" />
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 public class CircleView extends View { private int mColor = Color.RED; private Paint mPaint = new Paint (Paint.ANTI_ALIAS_FLAG); public CircleView (Context context) { super (context); init(); } public CircleView (Context context, @Nullable AttributeSet attrs) { this (context,attrs,0 ); } public CircleView (Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); TypedArray a= context.obtainStyledAttributes(attrs,R.styleable.CircleView); mColor = a.getColor(R.styleable.CircleView_circle_color,Color.RED); a.recycle(); init(); } private void init () { mPaint.setColor(mColor); } @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { super .onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(200 ,200 ); }else if (widthSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(200 ,heightSpecSize); }else if (heightSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(widthSpecSize,200 ); } } @Override protected void onDraw (Canvas canvas) { super .onDraw(canvas); final int paddingLeft = getPaddingLeft(); final int paddingRight = getPaddingRight(); final int paddingTop = getPaddingTop(); final int paddingBottom = getPaddingBottom(); int width = getWidth()-paddingLeft - paddingRight; int height = getHeight() - paddingTop - paddingBottom; int radius = Math.min(width,height)/2 ; canvas.drawCircle(paddingLeft+width/2 ,paddingTop+height/2 ,radius,mPaint); } }
2、继承 ViewGroup 派生特殊的 Layout
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 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 ; private int mLastXIntercept = 0 ; private int mLastYIntercept = 0 ; private Scroller mScroller; private VelocityTracker mVelocityTracker; public HorizontalScrollViewEx (Context context) { super (context); init(); } private void init () { if (mScroller == null ){ mScroller = new Scroller (getContext()); mVelocityTracker = VelocityTracker.obtain(); } } public HorizontalScrollViewEx (Context context, AttributeSet attrs) { super (context, attrs); init(); } public HorizontalScrollViewEx (Context context, AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); init(); } @Override public boolean onInterceptTouchEvent (MotionEvent ev) { boolean intercepted = false ; int x = (int ) ev.getX(); int y = (int ) ev.getY(); switch (ev.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.e(TAG,"intercepted = " +intercepted); mLastX = x; mLastY = y; mLastXIntercept = x; mLastYIntercept = y; return intercepted; } @Override public boolean onTouchEvent (MotionEvent ev) { mVelocityTracker.addMovement(ev); int x = (int ) ev.getX(); int y = (int ) ev.getY(); switch (ev.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 measureWidth = 0 ; int measureHeight = 0 ; final int childCount = getChildCount(); measureChildren(widthMeasureSpec,heightMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int heightSpecSize = 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 ); measureWidth = childView.getMeasuredWidth() * childCount; measureHeight = childView.getMeasuredHeight(); setMeasuredDimension(measureWidth,measureHeight); }else if (heightSpecMode == MeasureSpec.AT_MOST){ final View childView = getChildAt(0 ); measureHeight = childView.getMeasuredHeight(); setMeasuredDimension(widthSpecSize,measureHeight); }else if (widthSpecMode == MeasureSpec.AT_MOST){ final View childView = getChildAt(0 ); measureWidth = childView.getMeasuredWidth() * childCount; setMeasuredDimension(measureWidth,heightSpecSize); } } @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(); } }
链接
参考资料: Android 开发艺术探索