【Android 应用开发】UI绘制流程 ( 生命周期机制 | 布局加载机制 | UI 绘制流程 | 布局测量 | 布局摆放 | 组件绘制 | 瀑布流布局案例 )

2023-03-27 15:41:46 浏览数 (1)

文章目录

  • 一. 博客相关资料 及 下载地址
    • 1. 代码查看方法 ( ① 直接获取代码 | ② JAR 包替换 )
    • 2. 本博客涉及到的源码查看说明
  • 二. Activity 生命周期回调机制
    • 1. Android 程序的方法入口 ( ActivityThread 类 main() 函数 | 处理 Looper Handler | 创建 ActivityThread )
    • 2. ActivityThread 与 ApplicationThread 关联
    • 3. ApplicationThread 内部类解析 ( 系统触发的scheduleXXXActivity方法 | 通过 Handler 将消息发送出去 )
    • 4. H 类 ( Handler子类 处理各种 Message 信息 )
    • 5. handleXXXActivity 方法 处理Activity 不同生命周期的操作
    • 6. perforXXXActivity 方法 ( 该方法中 处理 实际的 每个生命周期复杂逻辑 )
  • 三. UI 布局加载机制解析
    • 1. 分析查找 setContentView 调用层次 ( 从 Activity 的 onCreate 中的 setContentView 开始 | 发现是调用 Window 的 setContentView 方法)
    • 2. Window 抽象类 和 PhoneWindow 唯一实现类
    • 3. PhoneWindow 实现的 setContentView 方法解析 ( 创建 DecorView 布局容器 | 加载基础布局 )
  • 四. UI 绘制流程 概述
    • 1. ActivityThread 中进行生命周期调度 ( handleResumeActivity 是绘制入口 | 调用 wm.addView 方法 )
    • 2. WindowManager 分析 ( WindowManager 接口 | WindowManagerImpl 实现类 | WindowManagerGlobal addView 方法 负责处理实际问题 | 调用 root.setView 方法 )
    • 3. ViewRootImpl 调用分析 ( 最终调用 performTraversals )
    • 4. UI 绘制核心 ①测量 performMeasure ②摆放 performLayout ③绘制 performDraw
  • 五. View 测量流程
    • 1. View 测量流程简介 ( ViewRootImpl.performTraversals 调用 getRootMeasureSpec 方法 和 performMeasure 方法 | getRootMeasureSpec 生成 View 测量参数 | performMeasure 调用 View 的 measure 方法 )
    • 2. MeasureSpec 打包过程 ( 2位模式位 30 位 数值位 | 模式 ① UNSPECIFIED 子组件想要多大就多大 | 模式 ② EXACTLY 对应 match_parent 和 dip px 设置 不能超过父控件大小 | 模式 ③ AT_MOST 对应 wrap_content 最大不能超过父控件 )
    • 3. onMeasure 方法的作用
      • ( 1 ) onMeasure 方法调用层次 ( ViewRootImpl.performTraversals 调用 performMeasure 方法 | 主要调用 View onMeasure 方法 )
      • ( 2 ) onMeasure 方法作用 ( 自己实现 )
  • 六.布局摆放
    • 1. View 布局摆放流程 ( ViewRootImpl.performTraversals 方法 )
  • 七.组件绘制
    • 1. 组件绘制方法调用解析 ( ViewRootImpl performDraw 方法 | )
    • 2. View draw方法解析 ( ① 绘制背景, ② 图层保存, ③ 绘制组件内容, ④ 绘制子组件, ⑤ 图层恢复, ⑥ 绘制装饰内容 )
  • 七.自定义瀑布流布局
    • 1. onMeasure 涉及到的测量优化问题
    • 2. onMeasure 测量详细流程设计
    • 3. onLayout 布局摆放流程
    • 4. 总体代码示例

博客相关资料 :

  • 1.项目地址 : https://github.com/han1202012/UI_Demos_4_CSDN_Blog
  • 2.博客资料下载 : https://download.csdn.net/download/han1202012/10797632

一. 博客相关资料 及 下载地址

1. 代码查看方法 ( ① 直接获取代码 | ② JAR 包替换 )

涉及到的 Android 源码查看方法 :

  • 1.代码无法获取到 : 分析 Android 源码时, 在 Android Studio 或 eclipse 中是无法查看到 底层隐藏的 Java 或 C 代码的;
  • 2.@hide 注解隐藏代码 : Android 中的代码 如果与开发者调用无关的, 就是在 文档注释中使用 {@hide} 进行隐藏, 即使导入了这个 jar 包, 也看不到对应的源码, 如本博客中分析的 ActivityThread.java , 其注释中就包含 {@hide} 注解;
代码语言:javascript复制
/**
 * This manages the execution of the main thread in an
 * application process, scheduling and executing activities,
 * broadcasts, and other operations on it as the activity
 * manager requests.
 *
 * {@hide}
 */
public final class ActivityThread {
    /** @hide */
    public static final String TAG = "ActivityThread";
  • 2.下载整套源码 ( 推荐 ) : 可以下载整套的 Android 源码, 可以获取到 所有的 C C Java 代码; 本博客的附件中包含了 android.jar 涉及到的源码目录中的所有源码;
  • 3.使用不隐藏代码的 jar 包 : 将下载下来的代码打开, 并删除其中的 {@hide} 注解, 然后打成 jar 包, 放入到 SDK 下的 platforms 对应的平台; 博客的附件中有对应的 android.jar 文件下载;

2. 本博客涉及到的源码查看说明

本博客源码查看说明 :

  • 1.博客中提到的源码在附件中有提供 :
    • 1> ActivityThread.java : 在 Android 源码 ( 需要手动使用 repo 脚本下载 ) 目录 中的 frameworksbasecorejavaandroid 路径下, 该文件已经扒出放在博客附件中;
  • 2.使用不带 @hide 注解的 jar 包 : 该 android.jar 是基于 android-27 版本的 源码 删除 @hide 注解重新打包的, 因此必须将该 android.jar 文件拷贝到 SDk 的 Sdkplatformsandroid-27 目录下, 该 jar 包相关资源库没有打包好, 仅用于在 Android Studio 中方便查看源码, 不能编译运行应用, 因此最好将原始的 android.jar 备份下;

二. Activity 生命周期回调机制

简要概述 : 下面先对生命周期回调机制进行简要概述, 在进行详细解析;

  • 1.设置 Loop 线程 :ActivityThread 中的 main 函数中, 将主线程设置为 Loop 线程, 在H类中的 handleMessage 中处理 Activity 中的各种生命周期事件.
  • 2.关联 ApplicationThread :ActivityThreadApplicationThread 进行关联, Activity 中的状态变化会通过 ApplicationThread 通知给具体的 Activity 进行处理;
  • 3.系统回调起点 : 当 Activity 生命周期发生改变, 会回调 ApplicationThreadscheduleXXXActivity, 如 scheduleLaunchActivity , 在 scheduleXXXActivity 中会调用 H 发送先关 Message 到主线程中进行处理;
  • 4.handleXXXActivity 方法 : 在 H 的 handleMessage 方法中, 处理 Activity 对应的生命周期状态会调用声明状态对应的 handleXXXActivity 方法, 如 handleResumeActivity 方法; 在该方法中主要调用 performXXXActivity 方法处理主要逻辑;
  • 5.performXXXActivity 方法 : 处理生命周期的主要方法逻辑;

1. Android 程序的方法入口 ( ActivityThread 类 main() 函数 | 处理 Looper Handler | 创建 ActivityThread )

Android 应用启动主方法入口 :

  • 1.代码源文件 及 位置 : ActivityThread.java ;
  • 2.Java 程序入口 : Java 程序执行需要一个 main 函数作为程序入口, 是所有程序的最开始的方法;
  • 3.main 方法调用时机 : 每次在手机中 点击一个应用图标, Android 系统就会调用 ActivityThread.java 中的 main()方法, 启动一个新的应用线程;
  • 4.主线程 : ActivityThread 代表了 Android 应用的主线程 , 该 main 方法就是主线程的启动方法;
  • 5.代码分步解析 : 主要进行两步操作, ① 处理 Looper, Handler 机制, ② 创建 ActivityThread 对象;
    • 1> 处理 Loop Handler 消息机制 : 将该线程转化为 loop 线程, 执行 Looper.prepareMainLooper()方法后, 该主线程开始进入无线循环状态, 等待接受 Message 信息; 信息最终由 H 对象处理; 生命周期实际实处的方法是通过该 Loop Handler 机制进行处理;
    • 2> 创建 ActivityThread 并运行 : 创建 ActivityThread 对象, 并进行初始化操作;
  • 5.程序主方法内容 :
代码语言:javascript复制
public final class ActivityThread {
	...
    public static void main(String[] args) {
        ...

        Looper.prepareMainLooper();
		
		//创建 ActivityThread 线程, 并运行
        ActivityThread thread = new ActivityThread();
        //attach 方法 进行 thread 的最初初始化操作 
        thread.attach(false);

        ...

        Looper.loop();

        ...
    }//main
    ...
}//ActivityThread

2. ActivityThread 与 ApplicationThread 关联

attach() 方法 进行上述关联操作 :

  • 1.代码源文件 及 位置 : ActivityThread.java ;
  • 2.调用过程 : ActivityThread main() -> ActivityThread attach(false) , 在 程序入口 main() 方法中创建了 ActivityThread 对象, 然后调用 ActivityThreadattach()方法, 对 ActivityThread 进行设置;
  • 3.代码解析 :
    • 1> 跨进程调用获取 ActivityManager 对象 : IActivityManager 开始是系统持有的, 不属于本应用进程, 通过 Binder 机制, 由系统调用应用进程程序, 产生 IActivityManager 对象;
    • 2> 关联 ActivityThread 和 ApplicationThread 操作 : ActivityThread 通过 获取到 ApplicationThread 后, 通过 mgr.attachApplication(this.mAppThread) 步骤是将 ActivityThreadApplicationThread 进行关联, 此时在 ApplicationThread 中存储这个 Activity 的相关信息, 为之后的 该 Activity 各种状态下的操作进行一些准备工作;
  • 4.attach 方法代码主要内容 :
代码语言:javascript复制
	...
	final ActivityThread.ApplicationThread mAppThread = new ActivityThread.ApplicationThread(null);
	...
    private void attach(boolean system) {
        ...
        if (!system) {
            ...
			//获取 Activity 管理对象
			final IActivityManager mgr = ActivityManager.getService();
            try {
                //将本 ActivityThread 与 ApplicationThread 进行关联
              	//其中 ActivityThread 代表当前页面, ApplicationThread 代表整个应用进程
                mgr.attachApplication(this.mAppThread);
            } 
            ...
        } 
       ...
  • 5.getService 方法先关的代码内容 : ActivityThread attach 方法中跨进程调用 getService 获取 IActivityManager 对象 ;
代码语言:javascript复制
public class ActivityManager {
	...
	//ActivityThread attach 方法中跨进程调用 getService 获取 IActivityManager 对象
    public static IActivityManager getService() {
        return (IActivityManager)IActivityManagerSingleton.get();
    }
    ...
    //使用了 Singleton<T> 单例类结构
    private static final Singleton<IActivityManager> IActivityManagerSingleton = new Singleton<IActivityManager>() {
        protected IActivityManager create() {
          	//通过 ServiceManager 获取 Activity 的 Service
            IBinder b = ServiceManager.getService("activity");
            //这里通过 Activity 的 Service 提供的 Binder 进行跨进程调用
            IActivityManager am = Stub.asInterface(b);
            return am;
        }
    };
    ...
}//ActivityManager

3. ApplicationThread 内部类解析 ( 系统触发的scheduleXXXActivity方法 | 通过 Handler 将消息发送出去 )

ApplicationThread 内部类 : 该类与上面的代码没有直接调用关系, ActivityThreadApplicationThread 关联后, 系统就可以通过调用 一系列的 schedule 方法控制 Activity的各种状态了;

  • 1.生命周期调用机制 : Activity 状态改变 , 会回调 其中 对应的 方法 ;
  • 2.由系统触发的scheduleXXXActivity方法 : 这一系列的 schedule 方法都是Activity 运行过程中触发某种状态调用的方法. 当进入新页面 系统会自动调用 scheduleLaunchActivity 方法, 当页面退出时由系统调用 schedulePauseActivityscheduleStopActivity 方法; 这些 scheduleXXXActivity 方法都会通过 H 对象发送消息并进行处理;
  • 3.scheduleLaunchActivity 代码分析 : 在该方法中, 创建了一个 ActivityClientRecord 对象, 然后将对象发送给 H 对象, 通过调用 sendMessage(100, r) 方法;
    • 1> ActivityClientRecord 对象 : 该对象可以等价看做 Activity, 在 ActivityClientRecord 中定义有 Activity成员变量, 这个 Activity 就是要显示的界面;
    • 2> 发送消息 : 通过调用 ActivityThread 中的 sendMessage 方法, 将 Activity 相关信息发送给 H 内部类, 该类是 Handler 的子类;
  • 4.代码主要内容 :
代码语言:javascript复制
public final class ActivityThread {
 	...
 	//消息处理的类
 	final ActivityThread.H mH = new ActivityThread.H(null);
 	...
    private class ApplicationThread extends android.app.IApplicationThread.Stub {
		...
        public final void scheduleLaunchActivity(...) {
            ...
            //创建 Activity, ActivityClientRecord 中有 Activity 成员变量
            ActivityThread.ActivityClientRecord r = new ActivityThread.ActivityClientRecord();
            ...
            //将 Activity 发送出去
            ActivityThread.this.sendMessage(100, r);
        }
        public final void schedulePauseActivity(IBinder token, boolean finished, boolean userLeaving, int configChanges, boolean dontReport) {
            ...
        }

        public final void scheduleStopActivity(IBinder token, boolean showWindow, int configChanges) {
            ...
        }
        ...
        public final void scheduleResumeActivity(IBinder token, int processState, boolean isForward, Bundle resumeArgs) {
            ...
        }
	...
	}//ApplicationThread
	...
	static final class ActivityClientRecord {
	        ...
	        //在 ActivityClientRecord 中定义有 Activity 成员变量
	        Activity activity;
	        ...
	} 
	...
 	//发送消息调用的方法
    private void sendMessage(int what, Object obj) {
        this.sendMessage(what, obj, 0, 0, false);
    }
    ...
}//ActivityThread

4. H 类 ( Handler子类 处理各种 Message 信息 )

H 类 :

  • 1.代码源文件 及 位置 : ActivityThread.java 文件, H 内部类;
  • 2.代码解析:H类的 handleMessage方法中可以处理各种传递来的 Message 信息;
    • 1> H 类 : 这是一个 Handler 类, H 继承了 Handler 类, 在 main 函数中 处理各种 Message 事件的就是这个 Handler;
  • 3.代码主要内容 :
代码语言:javascript复制
public final class ActivityThread {
 	...
 	//消息处理的类
 	final ActivityThread.H mH = new ActivityThread.H(null);
 	...
    //处理 Message 的 Handler 类
    private class H extends Handler {
        public static final int LAUNCH_ACTIVITY = 100;
        ...
        public void handleMessage(Message msg) {
            ActivityThread.ActivityClientRecord r;
            SomeArgs args;
            switch(msg.what) {
            case LAUNCH_ACTIVITY: {
                    ...
                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
                    ...
                    handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
                    ...
                } break;
                ...
			}//switch(msg.what)
		}//handleMessage     
    }//H
	...

	...
	
}//ActivityThread

5. handleXXXActivity 方法 处理Activity 不同生命周期的操作

handleXXXActivity 方法 :

  • 1.方法调用路径 :ApplicationThread 中系统会回调 scheduleXXXActivity 方法, 在该方法中向 H 发送 Message 信息, 在 handleMessage 中处理 对应状态的方法就是调用该 handleXXXActivity 方法;
  • 2.Activity 状态调用机制 :
    • 1> 调用机制 :H 内部类中, 处理 Activity 生命周期相关的操作时, 都调用 ActivityThread 的对应的 handleXXXActivity 方法; 可见 在 Activity 运行过程中, 其状态改变操作都是由 Handler 机制控制的;
    • 2> 事件来源 : 用户进行对应操作传给系统, 系统对 H 发送不同的信号, 主线程中 Looper 一直在进行循环, 收到 ApplicationThread 发送的 Message 信息后, 在 HhandleMessage 中执行对应的方法;
    • 3> 处理过程 : 在该方法中, 调用对应的 performXXXActivity 方法进行后续操作;
  • 2.代码解析: 该方法进行 Activity 启动的操作;
  • 3.代码主要内容 :
代码语言:javascript复制
public final class ActivityThread {
	...
    private void handleLaunchActivity(ActivityThread.ActivityClientRecord r, Intent customIntent, String reason) {
        ...
        Activity a = this.performLaunchActivity(r, customIntent);
        ...
    }
    ...
}//ActivityThread 

6. perforXXXActivity 方法 ( 该方法中 处理 实际的 每个生命周期复杂逻辑 )

performXXXActivity 方法 :

  • 1.方法说明 : 系统根据 Activity 生命周期变化 回调 ApplicationThreadscheduleXXXActivity 方法, 在 scheduleXXXActivity 中会向 H 发送对应状态的 Message 信息, 然后将
  • 2.代码解析: 该方法通过反射创建了一个 Activity 类, 然后调用了 Activity 的 attach 方法;
  • 3.主要的代码内容 :
代码语言:javascript复制
public final class ActivityThread {
	...
    private Activity performLaunchActivity(ActivityThread.ActivityClientRecord r, Intent customIntent) {
        ...   
		//通过反射构建 Activity 
        Activity activity = null;
        try {
            ClassLoader cl = appContext.getClassLoader();
            activity = this.mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
            ...
        } ...

        try {
            	...
				//
                activity.attach(appContext, this, this.getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, r.parent, r.embeddedID, r.lastNonConfigurationInstances, config, r.referrer, r.voiceInteractor, window, r.configCallback);
                ...
                //在此处回调 Activity 的 OnCreate 方法
                if (r.isPersistable()) {
                    this.mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    this.mInstrumentation.callActivityOnCreate(activity, r.state);
                }
                ...
        } ...

        return activity;
    }
    ...
}//ActivityThread 

三. UI 布局加载机制解析

简要概述 :

  • 1.先查找

1. 分析查找 setContentView 调用层次 ( 从 Activity 的 onCreate 中的 setContentView 开始 | 发现是调用 Window 的 setContentView 方法)

setContentView 方法调用分析 :Activity 中调用 setContentView 处, 向上查询代码, 发现最终调用的是 Window 类的 setContentView 方法;

  • 1.调用位置 : 每个 Activity 的 OnCreate 方法中, 都会先调用 setContentView 方法设置布局文件;
代码语言:javascript复制
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}
  • 2.Activity 中定义的 setContentView 方法 :Activity 中调用 this.getWindow().setContentView(layoutResID) 方法, 下面分析ActivitygetWindowWindow 抽象类的 setContentView 抽象方法;
代码语言:javascript复制
public class Activity extends ContextThemeWrapper implements ... {
	...
    public void setContentView(int layoutResID) {
        this.getWindow().setContentView(layoutResID);
        this.initWindowDecorActionBar();
    }
	...
}

2. Window 抽象类 和 PhoneWindow 唯一实现类

Window 和 PhoneWindow 解析 :

  • 1.Activity中定义的 getWindow 方法 和 Window 成员变量 : this.getWindow() 获取的是一个 Window 对象, Window 是一个抽象类, 其 setContentView 是一个抽象方法, 在其子类中实现 , 下面分析 Window 对象;
代码语言:javascript复制
public class Activity extends ContextThemeWrapper implements ... {
	...
    private Window mWindow;
    ...
    public Window getWindow() {
        return this.mWindow;
    }
	...
}
  • 2.Window 抽象类 : Window抽象类只有一个子类, 那就是 PhoneWindow 类, setContentView 抽象方法在 PhoneWindow 中实现的; 下面分析 PhoneWindow 类;
代码语言:javascript复制
public abstract class Window {
	...
    public abstract void setContentView(int var1);
	...
}

3. PhoneWindow 实现的 setContentView 方法解析 ( 创建 DecorView 布局容器 | 加载基础布局 )

PhoneWindow 实现的 setContentView 方法解析 :

  • 1.PhoneWindow 实现的 setContentView 方法 :PhoneWindow 中的 setContentView 的方法, 执行了两个重要步骤, ① installDecor(), ② inflate(); 下面分别介绍这两个重要方法;
代码语言:javascript复制
public class PhoneWindow extends Window implements Callback {
	...
    @Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        //上述翻译 : FEATURE_CONTENT_TRANSITIONS 可能在 窗口安装过程 中被设置,
        // 此时各种主题设置等属性被具体化执行. 在安装窗口之前, 不检查 feature 特性;
        //就是设置 feature 属性需要在 setContentView 之前进行设置, 在 installDecor 方法中会对这些 feature 属性进行初始化生效操作
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            ...
        } else {
        	//加载自己的布局文件
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        ...
    }
	...
}
  • 2.installDecor 方法解析 : 在这个方法中主要做了两件事情, ①生成 DecorView mDecor 成员变量, ②生成 ViewGroup mContentParent 窗口容器;
    • 1> 生成 DecorView 成员变量 : DecorView mDecor是窗口的最顶层的 view, 其包含了 window decor;
    • 2> 生成 ViewGroup mContentParent 对象, 该容器存放窗口中的内容, 该容器要么是 mDecor 本身, 要么就是存放 mDecor 子类内容的容器; 该容器是 顶层 View 容器;
代码语言:javascript复制
public class PhoneWindow extends Window implements Callback {
	...
	//这是 窗口 最顶层的 View, 包含了 window decor
    private DecorView mDecor;
    ...
    //窗口的内容被放置在这个 View 中, 要么是 mDecor 本身, 要么就是 mDecor 子类内容存放容器
    ViewGroup mContentParent;
	...
    private void installDecor() {
        this.mForceDecorInstall = false;
        if (this.mDecor == null) {
        	//创建 DecorView 成员变量对象
            this.mDecor = this.generateDecor(-1);
            ...
        } else {
            this.mDecor.setWindow(this);
        }

        if (this.mContentParent == null) {
        	//创建内容存放容器对象
            this.mContentParent = this.generateLayout(this.mDecor);
            ...
        }
        ...
}
  • 3.generateDecor 生成 DecorView 对象 : 在该方法中 使用 new DecorView 创建了DecorView 对象;
代码语言:javascript复制
public class PhoneWindow extends Window implements Callback {
	...
    protected DecorView generateDecor(int featureId) {
        ...
        return new DecorView((Context)context, featureId, this, this.getAttributes());
    }
    ...
}
  • 4.generateLayout 创建 mContentParent 容器 : generateLayout 方法创建存放布局组件的 容器; 该方法中处理 requestFeature 中的设置;
    • 1> requestFeature 设置 :Activity中可以调用 getWindow().setRequestFeature() 方法, 但是该方法必须在 setContentView 方法之前调用;
    • 2> requestFeature 设置使用 : setContentView 调用 installDecor方法, 在 installDecor 中调用的 generateLayout 方法对这些设置进行处理, 如果在 setContentView 之后设置, 就无法进行处理;
    • 3> 默认布局 ( 基础布局 ) : 在最后没有任何设置时, 使用 R.layout.screen_simple 作为基础布局, 下面分析这个布局;
代码语言:javascript复制
public class PhoneWindow extends Window implements Callback {
	...
    protected ViewGroup generateLayout(DecorView decor) {
        ...
        
        //这一组代码处理 requestFeature 设置
        if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
            requestFeature(FEATURE_NO_TITLE);
        } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
            // Don't allow an action bar if there is no title.
            requestFeature(FEATURE_ACTION_BAR);
        }
        ...
        
        //这一组代码负责处理布局文件相关
        int layoutResource;
        int features = getLocalFeatures();
        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            ...
        } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
            ...
        } ...
        	else {
            // Embedded, so no decoration is needed.
            //默认状态下不做任何设置的布局
            layoutResource = R.layout.screen_simple;
        }
    }
    ...
}
  • 5.R.layout.screen_simple 布局解析 : 如果没有设置 requestFeature 属性, 那么就会使用这个布局当做顶层布局;
    • 1> 状态栏区域 : ViewStub 是状态栏;
    • 2> 自定义布局存放位置 : 我们在 Activity 中setContentView 设置的布局都存放在这个 content id 的 FrameLayout 中;
代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<!--  -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@ id/action_mode_bar_stub"
              android:inflatedId="@ id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
  • 6.installDecor 完成的工作 : ① 创建布局容器, ② 加载基础布局;
    • 1> 创建布局容器 : 先创建 DecorView mDecor, 然后通过 mDecor 创建 ViewGroup mContentParent 容器, 这就是布局容器;
    • 2> 加载基础布局 : 调用 generateLayout 方法, 处理 requestFeature 设置, 根据处理结果加载不同的基础布局;
  • 11.加载开发者自定义的布局 :PhoneWindow 中的 setContentView 中, mLayoutInflater.inflate(layoutResID, mContentParent); 语句执行的是加载开发者自己的布局文件; 其中 mLayoutInflater 是在 PhoneWindow 创建时就进行初始化的布局加载器;
代码语言:javascript复制
public class PhoneWindow extends Window implements Callback {
	...
    private LayoutInflater mLayoutInflater;
	...
    public PhoneWindow(Context context) {
        super(context);
        //PhoneWindow 创建时初始化 mLayoutInflater 成员变量
        mLayoutInflater = LayoutInflater.from(context);
    }
	...
    @Override
    public void setContentView(int layoutResID) {
        ...
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            ...
        } else {
        	//加载自己的布局文件
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        ...
    }
	...
}
  • 7.布局加载器加载过程 :
代码语言:javascript复制
public abstract class LayoutInflater {
	...
	//PhoneWindow 中的 setContentView 调用的该方法
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }
    ...
    //从特定的 xml 资源文件中初始化一个界面布局层级
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        ...
		//进行 xml 解析, 解析出具体的组件及层级
        final XmlResourceParser parser = res.getLayout(resource);
        ...
    }
	...
}

四. UI 绘制流程 概述

1. ActivityThread 中进行生命周期调度 ( handleResumeActivity 是绘制入口 | 调用 wm.addView 方法 )

UI 绘制起点分析 :

  • 1.UI 绘制起点 : 进入界面后, 系统会根据 Activity 状态发送不同的指令, 将要绘制界面时, 系统会调用 ActivityThread 中 的 H 发送 RESUME_ACTIVITY 信息, 处理流程如下, 由代码可以看出主要是调用了 handleResumeActivity 方法;
代码语言:javascript复制
public final class ActivityThread {
 	...
 	//消息处理的类
 	final ActivityThread.H mH = new ActivityThread.H(null);
 	...
    //处理 Message 的 Handler 类
    private class H extends Handler {
    	...
        public static final int RESUME_ACTIVITY = 107;
        ...
        public void handleMessage(Message msg) {
            ActivityThread.ActivityClientRecord r;
            SomeArgs args;
            switch(msg.what) {
            	...
            	case RESUME_ACTIVITY:
                    ...
                    SomeArgs args = (SomeArgs) msg.obj;
                    //核心步骤是调用 该方法
                    handleResumeActivity((IBinder) args.arg1, true, args.argi1 != 0, true,
                            args.argi3, "RESUME_ACTIVITY");
                    ...
                    break;
                ...
			}//switch(msg.what)
		}//handleMessage     
    }//H
	...

	...
	
}//ActivityThread
  • 2.handleResumeActivity 方法解析 : 核心语句是 wm.addView(decor, l); 将开发者开发的 布局 加载到 decor 基础布局中; 其中 vm 是 WindowManager ( WindowManager 是 ViewManager 的子接口 );
代码语言:javascript复制
public final class ActivityThread {
 	...
    final void handleResumeActivity(...) {
    	...
    	if (r != null) {
    	 	...
			if (r.window == null && !a.mFinished && willBeVisible) {
				...
				ViewManager wm = a.getWindowManager();
				//加载开发者的资源
				WindowManager.LayoutParams l = r.window.getAttributes();
				//这是布局只有基础的布局及容器
				a.mDecor = decor;
				...
                if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        ...
                        //通过 ViewManager 将开发者开发的资源 加载到 decor 布局中
                        wm.addView(decor, l);
                    } else {
                        ...
                    }
                }
                ...
			}
			...
		}
		...
    }//handleResumeActivity
	...
	
}//ActivityThread

2. WindowManager 分析 ( WindowManager 接口 | WindowManagerImpl 实现类 | WindowManagerGlobal addView 方法 负责处理实际问题 | 调用 root.setView 方法 )

分析 WIndowManager :

  • 1.WindowManager 接口实现 : WindowManager 是一个接口, 其 继承了 ViewManager 接口, WindowManager 实际的 实现类是 WindowManagerImpl 类, 相关的核心逻辑都定义在 WindowManagerGlobal 中;
代码语言:javascript复制
public interface ViewManager{}
...
public interface WindowManager extends ViewManager {}
...
public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    ...
    //在 实现的 addView 方法中没有实际的逻辑, 最终的方法逻辑定义在 WindowManagerGlobal 中
    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }
}
  • 2.WIndowManager 的 addView 方法实现 : 由上面的接口关系可以看出, 最终的 addView 的实际方法逻辑 是定义在 WindowManagerGlobal 中; root.setView(view, wparams, panelParentView); 是核心语句;
代码语言:javascript复制
public final class WindowManagerGlobal {
	...
	//在该队列中保存 View 对象, DecorView 对象
    private final ArrayList<View> mViews = new ArrayList<View>();
    //保存与 顶层的 DecorView 关联的 ViewRootImpl 对象
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
    //保存创建顶层 DecorView 的 布局参数 LayoutParams 对象
    private final ArrayList<WindowManager.LayoutParams> mParams =
            new ArrayList<WindowManager.LayoutParams>();
	...
    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
		...
        ViewRootImpl root;
        View panelParentView = null;
		synchronized (mLock) {
        	...
        	root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
			
            try {
            	//将上述三种数据全部交给 root
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                ...
                throw e;
            }
		}
	}
	...
}

3. ViewRootImpl 调用分析 ( 最终调用 performTraversals )

ViewRootImpl 分析 :

  • 1.ViewRootImpl 相关方法解析 : 上面在 WindowManagerGlobal 中调用了 ViewRootImplsetView 方法, 从此处开始逐层分析下一步的操作;
    • ① 在 setView 方法中调用了 requestLayout 方法,
    • ② 在 requestLayout 方法中调用了 scheduleTraversals 方法,
    • ③ 在 scheduleTraversals 方法中通过回调调用了 TraversalRunnable 中的 run 方法,
    • ④ 在这个 run 方法中调用了 doTraversal 方法,
    • ⑤ 在 doTraversal 方法中调用了 performTraversals 方法, 这是 UI 绘制的核心方法;
代码语言:javascript复制
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
	...
	//1. 在该方法中调用了 requestLayout 方法
   public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
		...
		requestLayout();
		...
	}
	...
	//2. requestLayout 方法中调用了 scheduleTraversals 方法
    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            ...
            scheduleTraversals();
        }
    }
    ...
    //3. 在该方法中使用了回调, 调用了 mTraversalRunnable 中的run 方法
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            ...
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            ...
        }
    }
    ...
    //4. mTraversalRunnable 中的  run 方法主要执行了 doTraversal 方法
    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
    ...
    //5. 在 doTraversal 方法中调用了 UI 绘制的核心方法 performTraversals 方法
    void doTraversal() {
        if (mTraversalScheduled) {
            ...

            performTraversals();

            ...
        }
    }
    ...

}

4. UI 绘制核心 ①测量 performMeasure ②摆放 performLayout ③绘制 performDraw

UI 绘制核心流程 :

  • 1.确定控件位置 :performTraversals 中创建一个 Rect , 以确定控件的位置;
代码语言:javascript复制
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
	...
    private void performTraversals() {
		...
		//所有的布局加载的信息都再此处存放, 这是资源信息 
		WindowManager.LayoutParams lp = mWindowAttributes;
		...
		//绘制一个矩形, 确定 布局整体的位置 
		Rect frame = mWinFrame;
		...
	}    
	...

}
  • 2.布局测量 :performMeasure 中调用了 View 中的 measure 方法;
代码语言:javascript复制
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
	...
	//布局测量
    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        ...
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            ...
        }
    }   
	...

}
  • 3.View 中的 measure 方法 : 在该方法中进行了 组件的大小计算, 根据组件的 模式 和 设定宽高 设置 计算组件大小;
代码语言:javascript复制
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
	...
	public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
		...
		
		...
	}
	...
}
  • 4.布局摆放 : 布局的摆放主要在 performLayout 方法中进行; 该方法通过调用 View 的 layout 方法来摆放 组件, host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); ;
代码语言:javascript复制
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
	...
	//布局摆放
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
    	...
    	final View host = mView;
    	...
    	try {
    		//调用 View 的 layout 方法摆放组件
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
            ...
            if (numViewsRequestingLayout > 0) {
                // requestLayout() was called during layout.
                // If no layout-request flags are set on the requesting views, there is no problem.
                // If some requests are still pending, then we need to clear those flags and do
                // a full request/measure/layout pass to handle this situation.
                //得到资源中的所有 View 
                ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
                        false);
                if (validLayoutRequesters != null) {
                	...
                	int numValidRequests = validLayoutRequesters.size();
                    for (int i = 0; i < numValidRequests;   i) {
                        final View view = validLayoutRequesters.get(i);
                        ...
                        //轮询所有的资源中的 View, 如果存在, 那么在此调用 View 的 requestLayout 方法, 相当于递归调用该逻辑
                        view.requestLayout();
                    }
                    ...
                }
				...
				
			}
        }
        ...

	} 
	...

}
  • 5.组件绘制 : 组件绘制在 performDraw 方法中执行, 该方法主要调用 draw 方法执行核心逻辑;
代码语言:javascript复制
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
	...
	private void performDraw() {
		...
		try {
			//最终绘制的方法
            draw(fullRedrawNeeded);
        } 
		...
	}
	...
	//组件绘制的最终方法
	private void draw(boolean fullRedrawNeeded) {
		//绘制的画布
		Surface surface = mSurface;
		...
		...
	}
}

五. View 测量流程

1. View 测量流程简介 ( ViewRootImpl.performTraversals 调用 getRootMeasureSpec 方法 和 performMeasure 方法 | getRootMeasureSpec 生成 View 测量参数 | performMeasure 调用 View 的 measure 方法 )

测量流程 : ActivityThread 中

  • 1.测量起点 : ViewRootImplperformTraversals 方法中 调用的 performMeasure 方法是测量的起始位置;
代码语言:javascript复制
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
	...
    private void performTraversals() {
		...
		if (mFirst || windowShouldResize || insetsChanged ||
                viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
        	...
        	if (!mStopped || mReportNextDraw) {
        		...
        		if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                        updatedConfiguration) {
                    //获取布局测量的参数
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
					...
					//进行布局测量 
					 // Ask host how big it wants to be
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
					...
				}
				...
			}
			...
		}
		...
	}    
	...

}
  • 2.performMeasure 方法 : 在该方法中, 主要调用了 View 的 measure 方法; 先分析方法的两个参数 ① int childWidthMeasureSpec, ② int childHeightMeasureSpec ;
代码语言:javascript复制
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
	...
    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        ...
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            ...
        }
    }
    ...
}
  • 3.getRootMeasureSpec 方法分析 : 该方法生成 View 测量的 MeasureSpec 参数;
代码语言:javascript复制
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
    ...
    //基于窗口的根组件的布局参数计算出对应的 MeasureSpec 参数, windowSize 窗口的宽或高, rootDimension 窗口的某个方向的布局参数
    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            //窗口不可以重新设置大小, 强制使用根 view 当做窗口的大小
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            //窗口不可以重新设置大小, 设置根 viee 的最大大小;
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            //窗口想要被设定成一个固定的大小, 强制将根 view 设置成指定的大小; 
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }
    ...
}
  • 4.MeasureSpec 规格解析 : 该 int 类型值是在布局测量过程中, 将 组件的大小 结合 布局参数中设置的转换规则 ( 如 wrap_content 或 match_parent ) , 根据上述两种参数 计算出 MeasureSpec 规格;
    • 1> 与父容器相大小相等 MeasureSpec.EXACTLY : match_parent 设置 即 父容器已经测量出大小, 该组件与父容器宽高完全一致, 充满父容器;
    • 2> 与子控件相关 MeasureSpec.AT_MOST : wrap_content 设置就是与子容器相关, 其大小就是相关的子容器测量后的占用的大小, 最大不能超过父容器;
    • 3> 设置固定值 MeasureSpec.EXACTLY : 直接设置一个 dp 或 px 值;

2. MeasureSpec 打包过程 ( 2位模式位 30 位 数值位 | 模式 ① UNSPECIFIED 子组件想要多大就多大 | 模式 ② EXACTLY 对应 match_parent 和 dip px 设置 不能超过父控件大小 | 模式 ③ AT_MOST 对应 wrap_content 最大不能超过父控件 )

MeasureSpec 打包流程 :

  • 1.MeasureSpec 说明 : 是一个 32 位的 int 类型值, 其中分为模式位 和 大小值, 组件大小由他们决定;
    • 1> 模式位 : 前两位是模式位, 注意模式值是一个 32 位的值, 其实际值大小很大;
    • 2> 大小值 : 后 30 位是大小的值, 组件的大小由 模式 和 大小 决定;
  • 2.模式解析 : MeasureSpec 有三种模式, 注意 与 布局设置不是 一一对应的;
    • 1> UNSPECIFIED 模式 : 该模式下, 父容器对子组件不做任何限制, 子组件想要多大就可以设置多大;
    • 2> EXACTLY 模式 : 该模式下, 子组件的大小根据测量的本身的大小决定, 这个大小不能大于父容器的大小, 对应布局中的 match_parent 和 px或dip值 两种设置;
    • 3> AT_MOST 模式 : 该模式下, 子组件的大小根据测量的本身的大小决定, 这个大小不能大于父容器的大小, 对应布局中的 wrap_content 设置;
  • 2.makeMeasureSpec 生成 规格 数值 : 通过 (size & ~MODE_MASK) | (mode & MODE_MASK) 位运算 或者 直接相加的方法, 将规格 与 size值 进行或运算, 将 int 值的最高两位设置成 模式, 后 30 位设置成 size 数值;
代码语言:javascript复制
        //根据传入的 大小值 和 模式 创建一个 MeasureSpec 值
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size   mode;
            } else {
                // MODE_MASK 取反后, 最高的2 位为0, 后面 30 位 为 1
                // 取反 结果 与上 size 那么就会得到 size 后 30 位的大小; 
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
  • 3.getMode 获取模式: 通过 measureSpec & MODE_MASK 计算, 只要前两位, 后面 30 位 设置成0;
代码语言:javascript复制
        //提取布局测量模式
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }
  • 4.getSize 获取大小值 : measureSpec & ~MODE_MASK只要后 30 位, 前两位 设置成 0 ;
代码语言:javascript复制
        //提取最终的值
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
  • 5.MeasureSpec 规格 相关代码 :
代码语言:javascript复制
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ...
    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        //UNSPECIFIED 模式: 父容器对子组件不做任何限制, 子组件想要多大就可以设置多大
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        //EXACTLY 模式 : 父容器已经计算出子组件的大小,该组件的大小就是后30位代表的值大小 
        //该 EXACTLY 模式 对应布局中的 match_parent 和 px或dip值 设置两种模式;
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        //AT_MOST 模式 : 子组件的大小根据测量的本身的大小决定, 这个大小不能大于父容器的大小
        public static final int AT_MOST     = 2 << MODE_SHIFT;
       
        //根据传入的 大小值 和 模式 创建一个 MeasureSpec 值
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size   mode;
            } else {
                // MODE_MASK 取反后, 最高的2 位为0, 后面 30 位 为 1
                // 取反 结果 与上 size 那么就会得到 size 后 30 位的大小; 
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
	...
        //提取布局测量模式, 保留前两位的值, 后 30 位设置成 0 
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }
        
        //提取最终的值, 将前两位设置成0, 保留后 30 位的数值
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

        static int adjust(int measureSpec, int delta) {
            ...
            return makeMeasureSpec(size, mode);
        }
        ...
    }
    ...
}

3. onMeasure 方法的作用

( 1 ) onMeasure 方法调用层次 ( ViewRootImpl.performTraversals 调用 performMeasure 方法 | 主要调用 View onMeasure 方法 )

onMeasure 方法调用 : 继续之前的测量流程分析, 之前分析到 ViewRootImpl 的 performTraversals 中进行布局 测量 摆放 绘制 的流程, 其中 测量 主要是调用了 performMeasure 方法;

  • 1.performMeasure 方法 : 该方法中的调用了 DecorViewmeasure 方法;
代码语言:javascript复制
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
	...
	//布局测量
    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        ...
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            ...
        }
    }   
    ...
}
  • 2.View 的 measure 方法分析 : 在该 View 方法中, 主要调用了其本身的 onMeasure 方法, onMeasure 方法负责处理主要的测量逻辑;
代码语言:javascript复制
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ...
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
	if (forceLayout || needsLayout) {
            ...
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                ...
            } 
            ...
        }
	...
    }
    ...
}

( 2 ) onMeasure 方法作用 ( 自己实现 )

onMeasure 方法作用 :

  • 1.方法作用 : 控件根据自己的业务需求, 在各自的方法实现中, 处理不同的业务逻辑;
  • 2.自定义控件核心逻辑 : 这个方法也是自定义控件中需要重写的核心方法; 每个组件或布局自己实现的的 onMeasure 方法都不一样;
  • 3.示例 : FrameLayout 布局的 onMeasure 方法中, 对布局的 子组件进行测量, 根据 FrameLayout 布局自己的特定规则, 对子组件进行 定位 测量;

六.布局摆放

1. View 布局摆放流程 ( ViewRootImpl.performTraversals 方法 )

布局摆放流程 :

  • 1.布局摆放引入 : 之前提到在 ViewRootImplperformTraversals 处理核心逻辑, 先后通过直接或间接调用下面方法 ① 调用 performMeasure 方法进行测量, ② 调用 performLayout 进行布局摆放, ③ 调用 performDraw 进行绘制 ; 在本章节详细介绍如何进行布局摆放;
  • 2.布局摆放 与 布局测量 流程的相同点 : 两个流程的核心逻辑方式基本一样, 由 根 组件 DecorView 调用 layout 方法, 在其中调用 onLayout 方法, 在 onLayout 方法中递归调用子组件的 onLayout 的方法, 直至调用到最底层的组件; 这个逻辑 与 布局测量基本一致, 布局测量也是 顶层 View 循环调用子组件的 onMeasure 方法, 直至调用到 最底层的 组件;
  • 2.performLayout 方法介绍 : 核心是调用了 Viewlayout 方法, 在 layout 中调用了 onLayout 方法; 这里注意, performMeasure 最终也是调用了 View 的 onMeasure 方法;
代码语言:javascript复制
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
    ...
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        ...

        final View host = mView;
        ...
        try {
            //调用了 View 的 onLayout 方法
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

            ...
        } 
        ...
    }
    ...
}
  • 3.View 的 layout 方法解析 : 首先通过 setFrame 设置一个布局摆放范围, 然后调用 onLayout 方法; 这个 onLayout 方法需要组件自己实现, 不同的组件实现的逻辑不同;
代码语言:javascript复制
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ....
        public void layout(int l, int t, int r, int b) {
        ...
	int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
	//setFrame 方法设置布局摆放的范围
        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);

            ...
        }
        ...
    }
    ....
}
  • 4.setFrame 方法 : 该方法主要是对 本组件的 左 上 右 下 四个坐标 进行初始化;
    • ( 1 ) 坐标记录 : 该方法中会将每次布局摆放的四个方向的坐标记录下来, 以提升性能;
    • ( 2 ) 坐标对比 : 当传入新的初始化坐标时, 会将本次传入的坐标 和 之前存储的坐标进行对比, 如果一致, 那么继续使用之前的坐标, 如果不一致, 那么需要重新进行布局计算;
代码语言:javascript复制
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ...
    /**
     * 为组件指定一个大小 和 位置
     *
     * 在 onLayout 中调用该方法
     *
     * @param 相对于父容器的左侧位置
     * @param 相对于父容器的顶部位置
     * @param 相对于父容器的右侧位置
     * @param 相对于父容器的底部位置
     * @return true if the new size and position are different than the
     *         previous ones
     * {@hide}
     */
    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;

        if (DBG) {
            Log.d("View", this   " View.setFrame("   left   ","   top   ","
                      right   ","   bottom   ")");
        }

        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;
            
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            //对比本次传入的宽高数据 与 记录的上一次的数据对比, 查看是否有改变, 如果没有改变, 那么继续使用之前的数据, 如果有改变, 那么需要重新进行布局
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            invalidate(sizeChanged);

	    //如果位置发生了改变, 那么重新开始 进行布局摆放
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

            mPrivateFlags |= PFLAG_HAS_BOUNDS;


            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }

            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
                // If we are visible, force the DRAWN bit to on so that
                // this invalidate will go through (at least to our parent).
                // This is because someone may have invalidated this view
                // before this call to setFrame came in, thereby clearing
                // the DRAWN bit.
                mPrivateFlags |= PFLAG_DRAWN;
                invalidate(sizeChanged);
                // parent display list may need to be recreated based on a change in the bounds
                // of any child
                invalidateParentCaches();
            }

            // Reset drawn bit to original value (invalidate turns it off)
            mPrivateFlags |= drawn;

            mBackgroundSizeChanged = true;
            mDefaultFocusHighlightSizeChanged = true;
            if (mForegroundInfo != null) {
                mForegroundInfo.mBoundsChanged = true;
            }

            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
        return changed;
    }
    ...
}
  • 4.onLayout 方法 : 再继续看 onLayout 方法, 在 View 中, 该方法是一个空方法, 方法体重没有任何代码;
代码语言:javascript复制
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ....
    //该方法中没有定义内容, 需要组件自己定义
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }
    ....
}
  • 5.onLayout 的实现的目的 : 在组件自己实现的 onLayout 方法中, 就是计算出 组件的 左 上 右 下 的值; View 的 onLayout 对直接的 子组件 进行摆放, 调用 子组件的 layout 方法 对 间接 的孙组件 进行摆放, 一直调用到 最底层的 组件;
  • 6.举例说明 : 拿 FrameLayout 当做例子, 来分析其 实现的 onLayout 方法; 在 其 onLayout 中, 调用了 layoutChildren 方法, 这个方法是调用的核心逻辑;
代码语言:javascript复制
public class FrameLayout extends ViewGroup {
    ...
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

    //FrameLayout 中布局摆放的实际方法逻辑
    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();

        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        for (int i = 0; i < count; i  ) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;

                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft   (parentRight - parentLeft - width) / 2  
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft   lp.leftMargin;
                }

                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop   lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop   (parentBottom - parentTop - height) / 2  
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop   lp.topMargin;
                }

                child.layout(childLeft, childTop, childLeft   width, childTop   height);
            }
        }
    }
    ...
}

七.组件绘制

1. 组件绘制方法调用解析 ( ViewRootImpl performDraw 方法 | )

组件绘制调用解析 :

  • 1.布局绘制起点 ViewRootImpl 中 performDraw 方法 :performDraw 方法开始看起, 这是 组件绘制的起点方法, 经过 performMeasure 测量, performLayout 布局摆放后, 开始调用 performDraw 进行布局绘制;
代码语言:javascript复制
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
    ...
    private void performDraw() {
        ...
        final boolean fullRedrawNeeded = mFullRedrawNeeded;
    	mFullRedrawNeeded = false;
	...
	try {
	    //调用 draw 方法
            draw(fullRedrawNeeded);
        }
		...
    }
    ...
}
  • 2.ViewRootImpl 中的 draw 方法解析 : 在该方法中 获取了 Surface 画布, 并根据 fullRedrawNeeded 参数 获取组件要绘制的 Rect dirty 范围, 将绘制组件相关参数传入 drawSoftware 方法 后, 开始进行绘制;
    • ( 1 ) fullRedrawNeeded 参数说明 ( 重绘标识 ) : draw(fullRedrawNeeded); 代码中的 fullRedrawNeeded 参数, 其作用是标识是否需要绘制所有的视图, 如果是第一次调用该方法绘制组件, 那么显然绘制所有的组件, 即整个屏幕, 如果之后再次调用可能不需要重新值绘制所有的组件, 只需要绘制部分组件;
    • ( 2 ) 绘制区域 : final Rect dirty = mDirty; 中的 mDirty 区域就是需要绘制的范围; 如果 fullRedrawNeeded 参数为 true, 那么说明这是第一次绘制, 需要绘制整个屏幕, 即 将 mDirty 的范围设置成整个屏幕大小;
代码语言:javascript复制
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
    ...
    private void draw(boolean fullRedrawNeeded) {
    	//绘制组件的画布
    	Surface surface = mSurface;
    	...
    	//确定组件绘制的范围, 这个 mDirty 就是需要重新绘制的范围
    	final Rect dirty = mDirty;
    	if (mSurfaceHolder != null) {
            // The app owns the surface, we won't draw.
            dirty.setEmpty();
            if (animating && mScroller != null) {
                mScroller.abortAnimation();
            }
            return;
        }
	    //fullRedrawNeeded 为 true, 则说明这是第一次绘制, 需要绘制整个屏幕, 此时将 dirty 大小设置成屏幕大小
        if (fullRedrawNeeded) {
            mAttachInfo.mIgnoreDirtyState = true;
            dirty.set(0, 0, (int) (mWidth * appScale   0.5f), (int) (mHeight * appScale   0.5f));
        }
    	...
    	if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
            if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
        	...
        }else{
		...
		//绘制组件的核心方法, 传入画布, 绘制范围, 及相关信息
		if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                    return;
            }
		}
    }
    ...
}
  • 3.ViewRootImpl 中的 drawSoftware 方法解析 : 在该方法中先设置 Canvas 画布, 然后调用 View 的 draw 方法, 进行绘制;
    • ( 1 ) 获取 Canvas 对象 :mSurface.lockCanvas(dirty) 获取, Surface 画布 和 dirty 区域决定 Canvas 对象;
    • ( 2 ) 正式绘制 : mView.draw(canvas); 其中的 mView 就是根节点, 即 DecorView;
代码语言:javascript复制
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
    ...
    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {
        // 软件渲染绘制
        final Canvas canvas;
        try {
            final int left = dirty.left;
            final int top = dirty.top;
            final int right = dirty.right;
            final int bottom = dirty.bottom;
	    //从画布中定位一个区域, 交给 canvas 进行处理, 画布区域由 dirty 决定
            canvas = mSurface.lockCanvas(dirty);
			
            ...
        }
        ...
        try {
            ...

            //保证绘制之前画布的清洁, 保证绘制组件前画布是空白的
            if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
                canvas.drawColor(0, PorterDuff.Mode.CLEAR);
            }
            ...
            try {
                ...
				//绘制的主要方法
                mView.draw(canvas);

                ...
            } 
            ...
        }
		...
	}
    ...
}

2. View draw方法解析 ( ① 绘制背景, ② 图层保存, ③ 绘制组件内容, ④ 绘制子组件, ⑤ 图层恢复, ⑥ 绘制装饰内容 )

View 中的 draw 方法解析 : 简单介绍一下 draw 的绘制步骤, 本博客限于篇幅不在展开详细解析, 这是下一篇博客的重点;

  • 1.View 的 draw 方法解析 : 绘制的步骤 : ① 绘制背景, ② 图层保存, ③ 绘制组件内容, ④ 绘制子组件, ⑤ 图层恢复, ⑥ 绘制装饰内容; 下面会逐个步骤进行解析;
代码语言:javascript复制
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ...
    /**
     * Manually render this view (and all of its children) to the given Canvas.
     * The view must have already done a full layout before this function is
     * called.  When implementing a view, implement
     * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
     * If you do need to override this method, call the superclass version.
     *
     * @param canvas The Canvas to which the View is rendered.
     */
    @CallSuper
    public void draw(Canvas canvas) {
    	...
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. 绘制背景
         *      2. 图层保存 : 如果有必要, 保存当前画布层, 以便进行渐变绘制
         *      3. 绘制组件内容
         *      4. 绘制子组件
         *      5. 图层恢复 : 如果有必要, 恢复画布层, 绘制渐变内容If necessary, draw the fading edges and restore layers
         *      6. 绘制如滚动条等装饰内容
         */
        // 步骤 1 : 如果有必要的话, 先绘制背景
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
        //步骤 2 : 图层保存
        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;

	if (!verticalEdges && !horizontalEdges) {
            // 步骤 3 : 绘制组件内容, onDraw 方法需要组件自己实现, 在 View 中该方法是空的
            if (!dirtyOpaque) onDraw(canvas);

            // 步骤 4 : 绘制子组件, dispatchDraw 方法是空的, 需要组件自己实现
            dispatchDraw(canvas);

	    // 步骤 5 : 图层恢复
            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // 步骤 6 : 绘制前景 滚动条 等
            onDrawForeground(canvas);

            // 步骤 7 : 绘制默认的选中高亮状态
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we're done...
            return;
        }
    	...
    }
    ...
}
  • 2.绘制背景方法 drawBackground 解析 :
代码语言:javascript复制
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ...
    /**
     * Draws the background onto the specified canvas.
     *
     * @param canvas Canvas on which to draw the background
     */
    private void drawBackground(Canvas canvas) {
    	//绘制背景的图像
    	final Drawable background = mBackground;
    	...
    	//设置背景位置
    	setBackgroundBounds();
    	...
    	//将背景绘制到画布上
    	if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }
    ...
}
  • 3.绘制组件方法 : if (!dirtyOpaque) onDraw(canvas);, 其中的 onDraw 是一个空方法, 需要组件自己实现这个方法, 该方法与 onMeasure, onLayout 是一个性质的, 需要各个组件进行不同的实现;

此处只是简单的介绍, 下一篇博客详细讲解绘制的流程;


七.自定义瀑布流布局

瀑布流示例 :

  • 1.项目位置 : https://github.com/han1202012/UI_Demos_4_CSDN_Blog
  • 2.效果展示 : 横版时 先 进行 测量 布局 绘制, 然后翻转屏幕 重新进行 测量 布局 绘制 ;

1. onMeasure 涉及到的测量优化问题

onMeasure 两次调用优化问题 : 这是一个注意点 ;

  • 1.问题描述 : 在自定义布局时 onMeasure 方法会重复调用两次, 如果每次调用都重复相同的逻辑, 而且是递归调用到最底层组件, 这样比较浪费性能, 因此这里进行优化;
  • 2.两次调用产生原因 :ViewRootImplperformTraversals 方法中 会调用 performMeasure 方法, performMeasure 方法会调用 onMeasure 方法, 同时 performTraversals 方法中会再次调用 ViewRootImplscheduleTraversals 方法, 从而又重新调用了一次 performTraversals 方法, 因此 onMeasure 方法被调用了两次;
  • 3.回顾上述调用层次 :
    • ActivityThread 中的 ApplicationThread 回调对应的 scheduleResumeActivity 方法, 在该方法中向 H ( Handler 子类 ) 发送 Message 信息;
    • ② H 中的 handleMessage 方法中, 通过识别 Message 信息的 what 值 来调用 handleResumeActivity 方法;
    • ③ 在 handleResumeActivity 方法中调用了 WindowManageraddView 方法;
    • WindowManageraddView 方法实际上调用的是 WindowManagerGlobaladdView 方法;
    • WindowManagerGlobal 中的 addView 方法, 调用的是 ViewRootImpladdView 方法;
    • ViewRootImpladdView 调用了 requestLayout 方法;
    • ⑦ 在 requestLayout 方法中调用了 scheduleTraversals 方法;
    • scheduleTraversals 方法中调用 doTraversal 方法;
    • ⑨ 在 doTraversal 方法中调用了 performTraversals 方法;
    • ⑩ 而在 performTraversals 方法中, 如果组件在显示的情况下, 又调用了一次 scheduleTraversals, 而在 performTraversals 中进行 测量 摆放 绘制 流程, 因此 测量流程 onMeasure 被调用了 两次;
代码语言:javascript复制
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
	...
    private void performTraversals() {
		...
		//这是方法结尾处的代码块
		if (!cancelDraw && !newSurface) {
            ...
            performDraw();
        } else {
            if (isViewVisible) {
                // 又重新调用了 scheduleTraversals 方法
                scheduleTraversals();
            } 
            ...
        }
		...
	}    
	...

}

2. onMeasure 测量详细流程设计

onMeasure 测量流程 : 在测量过程中, 需要精确的测量每个子组件的 宽 和 高, 确保 瀑布流布局的实现;

  • 1.瀑布流需求 :TextView 组件放在 瀑布流容器中, 如果一行的宽度将要超过布局的宽度, 那么需要另起一行进行放置, 每行的宽度以该行中组件的最大宽度为准;
  • 2.行宽度控制 : 测量每个子组件的宽度, 并累加该宽度, 如果 子组件宽度 累加宽度 小于 容器宽度, 那么该组件还是在本行进行绘制, 宽度累加; 如果 子组件宽度 累加宽度 大于 容器宽度, 那么该组件在下一行控制, 此时进行换行操作;
  • 3.行高度控制 : 每次遍历 子组件 时, 如果不换行, 那么从本行组件中选出一个最大宽度当做本行的宽度 ;
  • 4.测量 onMeasure 代码示例 :
代码语言:javascript复制
    /**
     * 宽和高 都不规则的组件 进行排列
     *
     * 基本思想 : 确定布局组件的宽度 和 高度, 根据 WaterfallFlowLayout 布局的 width 和 height 属性进行计算
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        Log.i(TAG, "onMeasure");

        //onMeasure 可能会进行多次调用, 这里需要兼容 7.0 之后的情况
        childViewsLists.clear();
        heightLists.clear();

        //布局测量 : 1. 定义存储测量最终结果的变量
        int width = 0;
        int height = 0;

        //布局测量 : 2. 获取 宽 和 高 的 模式
        int widthMod = MeasureSpec.getMode(widthMeasureSpec);
        int heightMod = MeasureSpec.getMode(heightMeasureSpec);

        //布局测量 : 3. 获取 宽 和 高 的 值
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        //布局测量 : 4.  根据 宽 和 高 的 模式 和 大小 计算出组件的 宽 和 高
        if(widthMod == MeasureSpec.EXACTLY && heightMod == MeasureSpec.EXACTLY){
            //实际测量 : EXACTLY 模式 : EXACTLY 模式对应布局中的 match_parent 或者 实际px 或 dip 等实际值设置
            width = widthSize;
            height = heightSize;
        }else{
            //实际测量 : AT_MOST 模式 : 如果是其他模式, 那么就需要遍历所有的子组件, 并计算所需要的大小

            //AT_MOST 测量 : 1. 定义变量存储, 累加实时的宽高
            int currentWidth = 0;
            int currentHeight = 0;

            //AT_MOST 测量 : 2. 定义变量存储 子组件的 宽高
            int childWidth = 0;
            int childHeight = 0;

            //AT_MOST 测量 : 3. 存放一行的子组件, 换行时将该队列放入 childViewLists 集合中, 并创建新的集合
            ArrayList<View> childViewList = new ArrayList<>();

            //AT_MOST 测量 : 4. 遍历所有的子组件,
            for(int i = 0; i < getChildCount(); i   ){
                View child = getChildAt(i);

                //子组件测量 : 1. 测量子组件
                measureChild(child, widthMeasureSpec, heightMeasureSpec);

                //子组件测量 : 2. 获取子组件的布局参数
                //获取子组件 四个方向的 margin 值, 将布局参数强转为 MarginLayoutParams 类型, 需要重写 generateLayoutParams 方法, 让其返回 MarginLayoutParams 类型
                //注意 : 这里只计算 margin 值, 即 以组件大小为基准, 向外扩展的大小; padding 值是以组件宽高为基准, 向内部的压缩子组件的宽高, 不在测量的考虑范围内
                MarginLayoutParams marginLayoutParams = (MarginLayoutParams) child.getLayoutParams();

                //子组件测量 : 3. 获取子组件占用的实际宽度, 组件大小   左右 margin 大小
                childWidth = child.getMeasuredWidth()   marginLayoutParams.leftMargin   marginLayoutParams.rightMargin;
                childHeight = child.getMeasuredHeight()   marginLayoutParams.topMargin   marginLayoutParams.bottomMargin;


                //子组件测量 : 4. 计算该组件是否需要换行, 当组件实际占用宽度   累加宽度 大于组件宽度时, 进行换行操作
                if(childWidth   currentWidth > widthSize){
                    //累加超出了计算的大小, 换行

                    //子组件测量 换行逻辑 : 1.保存当前行, 每次换行的时候都
                    //取值策略 : 两个值相加大于总宽度, 此时该子组件的宽度取 currentWidth 累加值 或 childWidth 子组件中的最大值
                    width = Math.max(width, currentWidth);
                    //如果换行, 那么高度累加
                    height  = currentHeight;

                    //更新记录信息
                    heightLists.add(currentHeight);
                    childViewsLists.add(childViewList);


                    //子组件测量 换行逻辑 : 2. 记录新的行信息, 更新当前记录的 宽 和 高
                    currentWidth = childWidth;
                    currentHeight = childHeight;
                    //创建新的行组件记录集合
                    childViewList = new ArrayList<>();
                    childViewList.add(child);

                }else{//累加后可以在本行显示, 不换行

                    //子组件测量 不换行逻辑 :

                    // 不换行的话, 宽度累加
                    currentWidth  = childWidth;
                    //高度设置策略 : 取 childHeight 值 : 如果是第一次累加, currentHeight 为 0, 这时取 currentHeight = childHeight 为最大值
                    //             取 currentHeight 值 : 第一次之后的累加时都是 currentHeight = currentHeight;
                    currentHeight = Math.max(currentHeight, childHeight);
                    //向代表每行组件的 childViewList 集合中添加该子组件
                    childViewList.add(child);
                }

                if(i == getChildCount() - 1){
                    //处理换行逻辑, 虽然没有换行, 但是处理到了最后一个, 需要处理整行信息
                    width = Math.max(width, currentWidth);
                    height  = currentHeight;

                    heightLists.add(currentHeight);
                    childViewsLists.add(childViewList);
                }
            }
        }

        //布局测量 : 5. 设置最终测量出来的宽和高
        setMeasuredDimension(width, height);
        Log.i(TAG, "onMeasure width : "   width   " , height : "   height   " , heightLists : "   heightLists.size()   " , childViewsLists : "   childViewsLists.size());
    }

3. onLayout 布局摆放流程

onLayout 布局摆放流程 : 根据每个组件的 左右位置 和 其 margin 属性, 以及 每行的 高度, 之前行的累加高度, 计算出每个组件的位置, 使用 layout 方法, 放置每个组件 ;

代码语言:javascript复制
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        Log.i(TAG, "onLayout");

        //布局摆放 : 1. 定义用于记录每个组件的 左上右下 坐标的变量
        int left, top, right, bottom;

        //布局摆放 : 2. 定义 用于记录累加的 左 和 上 的坐标
        int currentLeft = 0, currentTop = 0;

        //布局摆放 : 3. 进行布局摆放, 遍历所有的子组件, 并放置子组件, 该层循环是遍历一行组件的 集合, 单个元素是一个组件集合
        for (int i = 0; i < childViewsLists.size(); i   ){
            ArrayList<View> childViewsList = childViewsLists.get(i);

            //该层循环的遍历的是 每行 具体的 子组件 集合, 单个元素是一个子组件
            for(int j = 0; j < childViewsList.size(); j   ){
                View child = childViewsList.get(j);

                //将布局参数转为可获取 左上右下 margin 的 MarginLayoutParams 类型布局参数
                MarginLayoutParams marginLayoutParams = (MarginLayoutParams) child.getLayoutParams();

                //组件的左 上 右 下 的四个位置
                left = currentLeft   marginLayoutParams.leftMargin;
                top = currentTop   marginLayoutParams.topMargin;
                right = left   child.getMeasuredWidth();
                bottom = top   child.getMeasuredHeight();

                //放置子组件
                child.layout(left, top, right, bottom);

                //处理累加数据值 : 累加水平方向的左侧值
                currentLeft = right   marginLayoutParams.rightMargin;
            }

            //处理累加数据 : 重置水平方向的左侧值 为 0, 累加垂直方向的高度值
            currentLeft = 0;
            currentTop  = heightLists.get(i);
            childViewsList.clear();
        }

        //布局摆放 : 4. 清空集合, 最大限度及时节省内存
        childViewsLists.clear();
        heightLists.clear();
    }

4. 总体代码示例

瀑布流布局的完整代码 :

  • 1.GitHub 项目地址 : https://github.com/han1202012/UI_Demos_4_CSDN_Blog
  • 2.自定义瀑布流容器代码地址 : 瀑布流容器 ;
代码语言:javascript复制
package hanshuliang.com.ui_demos_4_csdn_blog;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;
import java.util.List;

public class A_2_WaterfallFlowLayout extends ViewGroup {

    public static final String TAG = "A_2_WaterfallFlowLayout";

    /**
     * ArrayList<View> 存放每一行的组件集合
     * ArrayList<ArrayList<View>> 存放多行的 组件集合 的集合
     */
    private ArrayList<ArrayList<View>> childViewsLists = new ArrayList<>();

    /**
     * 每一行的高度集合
     */
    private ArrayList<Integer> heightLists = new ArrayList<>();


    public A_2_WaterfallFlowLayout(Context context) {
        super(context);
    }

    public A_2_WaterfallFlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public A_2_WaterfallFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    /**
     * 宽和高 都不规则的组件 进行排列
     *
     * 基本思想 : 确定布局组件的宽度 和 高度, 根据 WaterfallFlowLayout 布局的 width 和 height 属性进行计算
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        Log.i(TAG, "onMeasure");

        //onMeasure 可能会进行多次调用, 这里需要兼容 7.0 之后的情况
        childViewsLists.clear();
        heightLists.clear();

        //布局测量 : 1. 定义存储测量最终结果的变量
        int width = 0;
        int height = 0;

        //布局测量 : 2. 获取 宽 和 高 的 模式
        int widthMod = MeasureSpec.getMode(widthMeasureSpec);
        int heightMod = MeasureSpec.getMode(heightMeasureSpec);

        //布局测量 : 3. 获取 宽 和 高 的 值
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        //布局测量 : 4.  根据 宽 和 高 的 模式 和 大小 计算出组件的 宽 和 高
        if(widthMod == MeasureSpec.EXACTLY && heightMod == MeasureSpec.EXACTLY){
            //实际测量 : EXACTLY 模式 : EXACTLY 模式对应布局中的 match_parent 或者 实际px 或 dip 等实际值设置
            width = widthSize;
            height = heightSize;
        }else{
            //实际测量 : AT_MOST 模式 : 如果是其他模式, 那么就需要遍历所有的子组件, 并计算所需要的大小

            //AT_MOST 测量 : 1. 定义变量存储, 累加实时的宽高
            int currentWidth = 0;
            int currentHeight = 0;

            //AT_MOST 测量 : 2. 定义变量存储 子组件的 宽高
            int childWidth = 0;
            int childHeight = 0;

            //AT_MOST 测量 : 3. 存放一行的子组件, 换行时将该队列放入 childViewLists 集合中, 并创建新的集合
            ArrayList<View> childViewList = new ArrayList<>();

            //AT_MOST 测量 : 4. 遍历所有的子组件,
            for(int i = 0; i < getChildCount(); i   ){
                View child = getChildAt(i);

                //子组件测量 : 1. 测量子组件
                measureChild(child, widthMeasureSpec, heightMeasureSpec);

                //子组件测量 : 2. 获取子组件的布局参数
                //获取子组件 四个方向的 margin 值, 将布局参数强转为 MarginLayoutParams 类型, 需要重写 generateLayoutParams 方法, 让其返回 MarginLayoutParams 类型
                //注意 : 这里只计算 margin 值, 即 以组件大小为基准, 向外扩展的大小; padding 值是以组件宽高为基准, 向内部的压缩子组件的宽高, 不在测量的考虑范围内
                MarginLayoutParams marginLayoutParams = (MarginLayoutParams) child.getLayoutParams();

                //子组件测量 : 3. 获取子组件占用的实际宽度, 组件大小   左右 margin 大小
                childWidth = child.getMeasuredWidth()   marginLayoutParams.leftMargin   marginLayoutParams.rightMargin;
                childHeight = child.getMeasuredHeight()   marginLayoutParams.topMargin   marginLayoutParams.bottomMargin;


                //子组件测量 : 4. 计算该组件是否需要换行, 当组件实际占用宽度   累加宽度 大于组件宽度时, 进行换行操作
                if(childWidth   currentWidth > widthSize){
                    //累加超出了计算的大小, 换行

                    //子组件测量 换行逻辑 : 1.保存当前行, 每次换行的时候都
                    //取值策略 : 两个值相加大于总宽度, 此时该子组件的宽度取 currentWidth 累加值 或 childWidth 子组件中的最大值
                    width = Math.max(width, currentWidth);
                    //如果换行, 那么高度累加
                    height  = currentHeight;

                    //更新记录信息
                    heightLists.add(currentHeight);
                    childViewsLists.add(childViewList);


                    //子组件测量 换行逻辑 : 2. 记录新的行信息, 更新当前记录的 宽 和 高
                    currentWidth = childWidth;
                    currentHeight = childHeight;
                    //创建新的行组件记录集合
                    childViewList = new ArrayList<>();
                    childViewList.add(child);

                }else{//累加后可以在本行显示, 不换行

                    //子组件测量 不换行逻辑 :

                    // 不换行的话, 宽度累加
                    currentWidth  = childWidth;
                    //高度设置策略 : 取 childHeight 值 : 如果是第一次累加, currentHeight 为 0, 这时取 currentHeight = childHeight 为最大值
                    //             取 currentHeight 值 : 第一次之后的累加时都是 currentHeight = currentHeight;
                    currentHeight = Math.max(currentHeight, childHeight);
                    //向代表每行组件的 childViewList 集合中添加该子组件
                    childViewList.add(child);
                }

                if(i == getChildCount() - 1){
                    //处理换行逻辑, 虽然没有换行, 但是处理到了最后一个, 需要处理整行信息
                    width = Math.max(width, currentWidth);
                    height  = currentHeight;

                    heightLists.add(currentHeight);
                    childViewsLists.add(childViewList);
                }
            }
        }

        //布局测量 : 5. 设置最终测量出来的宽和高
        setMeasuredDimension(width, height);
        Log.i(TAG, "onMeasure width : "   width   " , height : "   height   " , heightLists : "   heightLists.size()   " , childViewsLists : "   childViewsLists.size());
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        Log.i(TAG, "onLayout");

        //布局摆放 : 1. 定义用于记录每个组件的 左上右下 坐标的变量
        int left, top, right, bottom;

        //布局摆放 : 2. 定义 用于记录累加的 左 和 上 的坐标
        int currentLeft = 0, currentTop = 0;

        //布局摆放 : 3. 进行布局摆放, 遍历所有的子组件, 并放置子组件, 该层循环是遍历一行组件的 集合, 单个元素是一个组件集合
        for (int i = 0; i < childViewsLists.size(); i   ){
            ArrayList<View> childViewsList = childViewsLists.get(i);

            //该层循环的遍历的是 每行 具体的 子组件 集合, 单个元素是一个子组件
            for(int j = 0; j < childViewsList.size(); j   ){
                View child = childViewsList.get(j);

                //将布局参数转为可获取 左上右下 margin 的 MarginLayoutParams 类型布局参数
                MarginLayoutParams marginLayoutParams = (MarginLayoutParams) child.getLayoutParams();

                //组件的左 上 右 下 的四个位置
                left = currentLeft   marginLayoutParams.leftMargin;
                top = currentTop   marginLayoutParams.topMargin;
                right = left   child.getMeasuredWidth();
                bottom = top   child.getMeasuredHeight();

                //放置子组件
                child.layout(left, top, right, bottom);

                //处理累加数据值 : 累加水平方向的左侧值
                currentLeft = right   marginLayoutParams.rightMargin;
            }

            //处理累加数据 : 重置水平方向的左侧值 为 0, 累加垂直方向的高度值
            currentLeft = 0;
            currentTop  = heightLists.get(i);
            childViewsList.clear();
        }

        //布局摆放 : 4. 清空集合, 最大限度及时节省内存
        childViewsLists.clear();
        heightLists.clear();
    }

    /**
     * 子组件获取的 LayoutParams 转成 MarginLayoutParams, 必须在此处将返回值修改为 MarginLayoutParams 对象
     * 否则获取子组件的 布局参数 转为 MarginLayoutParams 类型会出错
     * @param attrs
     * @return
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

}

0 人点赞