[关闭]
@guhuizaifeiyang 2019-08-23T11:59:23.000000Z 字数 13731 阅读 1116

View的工作流程:Measure,Layout,Draw

Android自定义控件

Android开发艺术探索
WindowManagerService-窗口管理服务分析
全屏、沉浸式、fitSystemWindow使用及原理分析:全方位控制“沉浸式”的实现

本文仅作为总结学习用途,感谢开放的互联网,感谢乐于分享的前辈。

此处输入图片的描述

浅析Activity中View的生命周期方法回调

此处输入图片的描述

在讲View的三大流程之前,先做一下准备工作,熟悉与其相关的类。

MeasureSpec

官方文档的介绍:

A MeasureSpec encapsulates the layout requirements passed from parent to child.Each MeasureSpec represents a requirement for either the width or the height.A MeasureSpec is comprised of a size and a mode.

MeasureSpec类似于测量指导,包括size(低30位)和mode(高2位)两个属性,用来规范子元素的测量标准。
很明显,size表示大小,mode有UNSPECIFIED,EXACTLY,AT_MOST三种模式:

measureChildWithMargins

ViewGroup.java

  1. protected void measureChildWithMargins(View child,
  2. int parentWidthMeasureSpec, int widthUsed,
  3. int parentHeightMeasureSpec, int heightUsed) {
  4. final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
  5. final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
  6. mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
  7. + widthUsed, lp.width);
  8. final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
  9. mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
  10. + heightUsed, lp.height);
  11. child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
  12. }
  1. 第4行获取子View的LayoutParams。
  2. 第6行-11行通过getChildMeasureSpec获取子View的MeasureSpec。
  3. 第13行调用子View的measure进行测量

getChildMeasureSpec

ViewGroup.java

  1. public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
  2. int specMode = MeasureSpec.getMode(spec);
  3. int specSize = MeasureSpec.getSize(spec);
  4. int size = Math.max(0, specSize - padding);
  5. int resultSize = 0;
  6. int resultMode = 0;
  7. switch (specMode) {
  8. // Parent has imposed an exact size on us
  9. case MeasureSpec.EXACTLY:
  10. if (childDimension >= 0) {
  11. resultSize = childDimension;
  12. resultMode = MeasureSpec.EXACTLY;
  13. } else if (childDimension == LayoutParams.MATCH_PARENT) {
  14. // Child wants to be our size. So be it.
  15. resultSize = size;
  16. resultMode = MeasureSpec.EXACTLY;
  17. } else if (childDimension == LayoutParams.WRAP_CONTENT) {
  18. // Child wants to determine its own size. It can't be
  19. // bigger than us.
  20. resultSize = size;
  21. resultMode = MeasureSpec.AT_MOST;
  22. }
  23. break;
  24. // Parent has imposed a maximum size on us
  25. case MeasureSpec.AT_MOST:
  26. if (childDimension >= 0) {
  27. // Child wants a specific size... so be it
  28. resultSize = childDimension;
  29. resultMode = MeasureSpec.EXACTLY;
  30. } else if (childDimension == LayoutParams.MATCH_PARENT) {
  31. // Child wants to be our size, but our size is not fixed.
  32. // Constrain child to not be bigger than us.
  33. resultSize = size;
  34. resultMode = MeasureSpec.AT_MOST;
  35. } else if (childDimension == LayoutParams.WRAP_CONTENT) {
  36. // Child wants to determine its own size. It can't be
  37. // bigger than us.
  38. resultSize = size;
  39. resultMode = MeasureSpec.AT_MOST;
  40. }
  41. break;
  42. // Parent asked to see how big we want to be
  43. case MeasureSpec.UNSPECIFIED:
  44. if (childDimension >= 0) {
  45. // Child wants a specific size... let him have it
  46. resultSize = childDimension;
  47. resultMode = MeasureSpec.EXACTLY;
  48. } else if (childDimension == LayoutParams.MATCH_PARENT) {
  49. // Child wants to be our size... find out how big it should
  50. // be
  51. resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
  52. resultMode = MeasureSpec.UNSPECIFIED;
  53. } else if (childDimension == LayoutParams.WRAP_CONTENT) {
  54. // Child wants to determine its own size.... find out how
  55. // big it should be
  56. resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
  57. resultMode = MeasureSpec.UNSPECIFIED;
  58. }
  59. break;
  60. }
  61. return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
  62. }

普通View的MeasureSpec创建规则(图来源于Android开发艺术探索):
1. 子View的LayoutParams是dp/px的情况下,表示子View已经独立自主,确立了自己的标准,所以MeasureSpec模式都是EXACTLY,MeasureSpec大小也是自己设定的dp/px。
2. 子View的LayoutParams是match_parent的情况下,表示子View向父View看齐,所以MeasureSpec模式跟父View保持一致,见表格第二行。
3. 子View的LayoutParams是wrap_content的情况下,表示子View想决定自己的大小,但还是受父View的监管,所以MeasureSpec模式是AT_MOST。见表格第三行。
4. 父View是UNSPECIFIED的情况下有点特殊,一般不需要亲关注,可以参考源码体会。
此处输入图片的描述

measure过程

View和ViewGroup的measure过程有所不同,View只要测量自己就可以了,而ViewGoup除了要测量自己外,还要遍历去调用子元素的measure方法完成测量。

view的measure分析

View$measure方法会去调用View的onMeasure方法。
measure->onMeasure->setMeasuredDimension->getDefaultSize

  1. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  2. setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
  3. getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
  4. }
  5. public static int getDefaultSize(int size, int measureSpec) {
  6. int result = size;
  7. int specMode = MeasureSpec.getMode(measureSpec);
  8. int specSize = MeasureSpec.getSize(measureSpec);
  9. switch (specMode) {
  10. case MeasureSpec.UNSPECIFIED:
  11. result = size;
  12. break;
  13. case MeasureSpec.AT_MOST:
  14. case MeasureSpec.EXACTLY:
  15. result = specSize;
  16. break;
  17. }
  18. return result;
  19. }

getDefaultSize中主要考虑AT_MOST和EXACTLY两种情况,getDefaultSize返回的大小就是measureSpec中的specSize,对应View的测量值。

setMeasuredDimension和getSuggestedMinimumWidth不是重点,略过。

注意:直接继承View的自定义控件要重写onMeasure方法并设置wrap_content时自身的大小。因为从前面的表格可以看出,当view的layoutParams为wrap_content时,它的specMode是AT_MOST模式。它的大小等于specSize,也就是父View当前剩余的大小。这种效果相当于在布局中使用match_parent完全一致了。

Android应用层View绘制流程与源码分析
- View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。

ViewGroup的measure过程

ViewGroup中没有onMeasure方法,而是提供了一个measureChildren方法去遍历所有子View的measure方法。
measureChildren->measureChild->getChildMeasureSpec->child.measure

由于不同的ViewGoup布局特性差异很大,所以ViewGoup把onMeasure方法留给其子类去实现。下面通过LinearLayout的onMeasure方法来分析ViewGroup的measure过程。

LinearLayout的measure过程分析

  1. @Override
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  3. if (mOrientation == VERTICAL) {
  4. measureVertical(widthMeasureSpec, heightMeasureSpec);
  5. } else {
  6. measureHorizontal(widthMeasureSpec, heightMeasureSpec);
  7. }
  8. }

/TODO:测量过程比较繁琐,后续再补充

正确获取View的的方法

1.Activity/View$onWindowFocusChanged
当Activity的窗口获得焦点和失去焦点的时候均会调用该方法,示例代码:

  1. @Override
  2. public void onWindowFocusChanged(boolean hasFocus) {
  3. super.onWindowFocusChanged(hasFocus);
  4. if (hasFocus) {
  5. int width = view.getMeasuredWidth();
  6. int height = view.getMeasuredHeight();
  7. }
  8. }

2.view.post(runnable)

  1. @Override
  2. protected void onStart() {
  3. super.onStart();
  4. view.post(new Runnable() {
  5. @Override
  6. public void run() {
  7. int width = view.getMeasuredWidth();
  8. int height = view.getMeasuredHeight();
  9. }
  10. });
  11. }

3.ViewTreeObserver
当View树的状态发生改变或者内部的View的可见性发现改变时,onGlobalLayout方法将被回调。

  1. @Override
  2. protected void onStart() {
  3. super.onStart();
  4. ViewTreeObserver observer = view.getViewTreeObserver();
  5. observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
  6. @Override
  7. public void onGlobalLayout() {
  8. int width = view.getMeasuredWidth();
  9. int height = view.getMeasuredHeight();
  10. }
  11. });
  12. }

4.view.measure(int widthMeasureSpec, int heightMeasureSpec)
手动对view进行measure,需要根据view的LayoutParams来区分:
4.1 match_parent
这种情况无法准确测量,因为不知道父view的剩余大小。

4.2 具体的数值
假设宽/高是100dp

  1. int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
  2. int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
  3. view.measure(widthMeasureSpec, heightMeasureSpec);

4.3 wrap_content
MeasureSpec高2位表示SpecMode,低30位表示SpecSize,所以这里使用(1<<30)-1表示View理论上能支持的最大值。

  1. int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30) -1, View.MeasureSpec.AT_MOST);
  2. int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30) -1, View.MeasureSpec.AT_MOST);
  3. view.measure(widthMeasureSpec, heightMeasureSpec);

layout过程

在完成了View的测量之后,接下来进行的是View的布局。

  1. public void layout(int l, int t, int r, int b) {
  2. int oldL = mLeft;
  3. int oldT = mTop;
  4. int oldB = mBottom;
  5. int oldR = mRight;
  6. boolean changed = isLayoutModeOptical(mParent) ?
  7. setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
  8. if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
  9. onLayout(changed, l, t, r, b);
  10. mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
  11. // ...
  12. }
  13. }

直接捡重点分析
l,t,r,b分别表示view相对于parent左,上,右,下端的距离。
1. setFrame确定View的四个顶点的位置。
2. onLayout确定子View的位置,由于onLayout的实现跟具体布局有关,所以View和ViewGoup都没有实现该方法。

以LinearLayout的onLayout为例,它会遍历子View,会一级一级调用子View的layout方法,完成整个View树的layout过程。
调用逻辑是onLayout->layoutVertical->setChildFrame->child.layout

  1. @Override
  2. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  3. if (mOrientation == VERTICAL) {
  4. layoutVertical(l, t, r, b);
  5. } else {
  6. layoutHorizontal(l, t, r, b);
  7. }
  8. }

getMeasuredWidth和getWidth的区别:
这两个方法都是用来获取View的宽度,一般情况下二者的值都是相同的。
不同的是getMeasuredWidth的值形成于measure过程,getWidth的值形成于layout过程。

draw过程

draw过程比较简单,步骤如下:
1. 绘制背景background.draw(canvas)
2. 绘制自己(onDraw)
3. 绘制children(dispatchDraw)
4. 绘制装饰(onDrawScrollBars)

FLAG介绍

资料来源:刨根问底-论Android“沉浸式”
使用方法如下:

  1. int flag = View.SYSTEM_UI_FLAG_FULLSCREEN;
  2. getWindow().getDecorView().setSystemUiVisibility(flag);
  3. int flag = View.SYSTEM_UI_FLAG_FULLSCREEN
  4. | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
  5. getWindow().getDecorView().setSystemUiVisibility(flag);

在Android4.4还为WindowManager.LayoutParams添加了两个flag:

FLAG_TRANSLUCENT_STATUS: 当使用这个flag时SYSTEM_UI_FLAG_LAYOUT_STABLE和SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN会被自动添加
FLAG_TRANSLUCENT_NAVIGATION:当使用这这个个flag时SYSTEM_UI_FLAG_LAYOUT_STABLE和SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION会被自动添加。

综上,我们可以给出全屏布局和隐藏状态栏的新方案

  1. //仅仅只是全屏布局:
  2. //getWindow().getDecorView().setSystemUiVisibility(
  3. View.SYSTEM_UI_FLAG_LAYOUT_STABLE
  4. | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
  5. | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
  6. //全屏布局并且隐藏状态栏与导航栏
  7. getWindow().getDecorView().setSystemUiVisibility(
  8. View.SYSTEM_UI_FLAG_FULLSCREEN
  9. | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
  10. | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
  11. | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
  12. | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
  13. | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);

隐藏状态栏

  1. View decorView = getWindow().getDecorView();
  2. int option = View.SYSTEM_UI_FLAG_FULLSCREEN;
  3. decorView.setSystemUiVisibility(option);
  4. ActionBar actionBar = getSupportActionBar();
  5. actionBar.hide();

此处输入图片的描述

状态栏透明

  1. if (Build.VERSION.SDK_INT >= 21) {
  2. View decorView = getWindow().getDecorView();
  3. int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
  4. | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
  5. decorView.setSystemUiVisibility(option);
  6. getWindow().setStatusBarColor(Color.TRANSPARENT);
  7. }
  8. ActionBar actionBar = getSupportActionBar();
  9. actionBar.hide();

此处输入图片的描述

隐藏状态栏和导航栏

  1. View decorView = getWindow().getDecorView();
  2. int option = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
  3. | View.SYSTEM_UI_FLAG_FULLSCREEN;
  4. decorView.setSystemUiVisibility(option);
  5. ActionBar actionBar = getSupportActionBar();
  6. actionBar.hide();

此处输入图片的描述

透明状态栏和导航栏

  1. if (Build.VERSION.SDK_INT >= 21) {
  2. View decorView = getWindow().getDecorView();
  3. int option = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
  4. | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
  5. | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
  6. decorView.setSystemUiVisibility(option);
  7. getWindow().setNavigationBarColor(Color.TRANSPARENT);
  8. getWindow().setStatusBarColor(Color.TRANSPARENT);
  9. }
  10. ActionBar actionBar = getSupportActionBar();
  11. actionBar.hide();

此处输入图片的描述

StatusBar颜色更改

StatusBar的颜色更改分为两部分,一个是背景颜色的修改,一个是字体颜色的修改。

首先先说说背景颜色的修改,在Android 5.0之前,状态栏颜色并不可定制,5.0之后才可定制。首先,我们可以在主题里通过colorPrimaryDark来指定背景色,其次,我们可以调用 window.setStatusBarColor(@ColorInt int color) 来修改状态栏颜色,但是让这个方法生效有一个前提条件:
你必须给window添加FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS并且取消FLAG_TRANSLUCENT_STATUS

在Android6以后,我们只要给SystemUI加上SYSTEM_UI_FLAG_LIGHT_STATUS_BAR这个flag,就可以让字体和图标变为黑色。

窗口状态

View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR  //高亮状态栏

View.STATUS_BAR_TRANSLUCENT //半透明状态栏
View.STATUS_BAR_TRANSPARENT //透明状态栏,一般指定WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS系统会设置成全透明

View.SYSTEM_UI_FLAG_FULLSCREEN // 全屏显示,隐藏状态栏和状态栏
View.STATUS_BAR_UNHIDE     // 显示状态栏,用于传递给systemui处理

View.NAVIGATION_BAR_TRANSPARENT //半透明导航栏
View.NAVIGATION_BAR_TRANSLUCENT //透明导航栏,一般指定WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS系统会设置成全透明

View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // 隐藏导航栏
View.NAVIGATION_BAR_UNHIDE      // 显示状态栏,传递给systemui处理

View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY //身临其境的感觉,和SYSTEM_UI_FLAG_LIGHT_STATUS_BAR一起使用会隐藏状态栏和导航栏,从上面/下面滑出状态栏和导航栏,过几秒自动消失
View.SYSTEM_UI_FLAG_IMMERSIVE //身临其境的感觉,自动隐藏状态栏和导航栏,出上部/下部滑动状态栏出现,不自动隐藏

View.STATUS_BAR_TRANSIENT  //进入瞬态,状态栏出来和隐藏的过程
View.NAVIGATION_BAR_TRANSIENT //进入瞬态,导航栏出来和隐藏的过程

WindowManager.LayoutParams.FLAG_FULLSCREEN //全屏显示,隐藏状态栏
WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS //导航栏状态栏透明,客户端渲染状态栏导航栏背景
WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND //强制导航栏状态栏透明,客户端渲染状态栏导航栏背景
WindowManager.LayoutParams.PRIVATE_FLAG_INHERIT_TRANSLUCENT_DECOR  //当此窗口到达顶部的时候保持前一个窗口的透明状态

WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS //指定半透明status bar
WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION //指定半透明nav bar

WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND 强制渲染背景色

下面这几个状态用于status bar 和nav bar 切换时候的瞬态过程
private static final int TRANSIENT_BAR_NONE = 0; //无任何状态,当隐藏完成时候设置
private static final int TRANSIENT_BAR_SHOW_REQUESTED = 1; // 请求显示
private static final int TRANSIENT_BAR_SHOWING = 2; //正在显示的过程
private static final int TRANSIENT_BAR_HIDING = 3; //正在隐藏的过程,隐藏完成window变成不可见,设置TRANSIENT_BAR_NONE

还要记住,分屏模式不允许隐藏bar

资料参考:Android状态栏微技巧,带你真正理解沉浸式模式
郭霖的博客,对沉浸式讲解的作用讲解非常详细。

经验总结

  1. removeView可以多次移除同一个view,内部会自己判断view是否存在。
  2. addView添加同一个view时,如果view.getParent()!=null,是会抛出异常,因此也可以先作判断。
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注