文章目录
- 一. 博客相关资料 及 下载地址
- 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} 注解;
/**
* 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
路径下, 该文件已经扒出放在博客附件中;
- 1> ActivityThread.java : 在 Android 源码 ( 需要手动使用 repo 脚本下载 ) 目录 中的
- 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 : 将
ActivityThread
与ApplicationThread
进行关联, Activity 中的状态变化会通过ApplicationThread
通知给具体的 Activity 进行处理; - 3.系统回调起点 : 当 Activity 生命周期发生改变, 会回调
ApplicationThread
的scheduleXXXActivity
, 如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
对象, 并进行初始化操作;
- 1> 处理 Loop Handler 消息机制 : 将该线程转化为
- 5.程序主方法内容 :
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
对象, 然后调用ActivityThread
的attach()
方法, 对ActivityThread
进行设置; - 3.代码解析 :
- 1> 跨进程调用获取 ActivityManager 对象 : IActivityManager 开始是系统持有的, 不属于本应用进程, 通过 Binder 机制, 由系统调用应用进程程序, 产生 IActivityManager 对象;
- 2> 关联 ActivityThread 和 ApplicationThread 操作 :
ActivityThread
通过 获取到ApplicationThread
后, 通过mgr.attachApplication(this.mAppThread)
步骤是将ActivityThread
与ApplicationThread
进行关联, 此时在ApplicationThread
中存储这个Activity
的相关信息, 为之后的 该Activity
各种状态下的操作进行一些准备工作;
- 4.attach 方法代码主要内容 :
...
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 对象 ;
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 内部类 : 该类与上面的代码没有直接调用关系, ActivityThread
与 ApplicationThread
关联后, 系统就可以通过调用 一系列的 schedule 方法控制 Activity
的各种状态了;
- 1.生命周期调用机制 : Activity 状态改变 , 会回调 其中 对应的 方法 ;
- 2.由系统触发的
scheduleXXXActivity
方法 : 这一系列的 schedule 方法都是Activity
运行过程中触发某种状态调用的方法. 当进入新页面 系统会自动调用scheduleLaunchActivity
方法, 当页面退出时由系统调用schedulePauseActivity
和scheduleStopActivity
方法; 这些scheduleXXXActivity
方法都会通过H
对象发送消息并进行处理; - 3.scheduleLaunchActivity 代码分析 : 在该方法中, 创建了一个 ActivityClientRecord 对象, 然后将对象发送给
H
对象, 通过调用sendMessage(100, r)
方法;- 1> ActivityClientRecord 对象 : 该对象可以等价看做
Activity
, 在ActivityClientRecord
中定义有Activity
成员变量, 这个Activity
就是要显示的界面; - 2> 发送消息 : 通过调用
ActivityThread
中的sendMessage
方法, 将Activity
相关信息发送给H
内部类, 该类是 Handler 的子类;
- 1> ActivityClientRecord 对象 : 该对象可以等价看做
- 4.代码主要内容 :
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
;
- 1> H 类 : 这是一个
- 3.代码主要内容 :
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
信息后, 在H
的handleMessage
中执行对应的方法; - 3> 处理过程 : 在该方法中, 调用对应的
performXXXActivity
方法进行后续操作;
- 1> 调用机制 : 在
- 2.代码解析: 该方法进行
Activity
启动的操作; - 3.代码主要内容 :
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 生命周期变化 回调
ApplicationThread
的scheduleXXXActivity
方法, 在scheduleXXXActivity
中会向H
发送对应状态的Message
信息, 然后将 - 2.代码解析: 该方法通过反射创建了一个 Activity 类, 然后调用了 Activity 的 attach 方法;
- 3.主要的代码内容 :
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 方法设置布局文件;
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)
方法, 下面分析Activity
的getWindow
和Window
抽象类的setContentView
抽象方法;
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
对象;
public class Activity extends ContextThemeWrapper implements ... {
...
private Window mWindow;
...
public Window getWindow() {
return this.mWindow;
}
...
}
- 2.Window 抽象类 :
Window
抽象类只有一个子类, 那就是PhoneWindow
类,setContentView
抽象方法在PhoneWindow
中实现的; 下面分析PhoneWindow
类;
public abstract class Window {
...
public abstract void setContentView(int var1);
...
}
3. PhoneWindow 实现的 setContentView 方法解析 ( 创建 DecorView 布局容器 | 加载基础布局 )
PhoneWindow 实现的 setContentView 方法解析 :
- 1.PhoneWindow 实现的 setContentView 方法 : 在
PhoneWindow
中的setContentView
的方法, 执行了两个重要步骤, ①installDecor()
, ②inflate()
; 下面分别介绍这两个重要方法;
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 容器;
- 1> 生成 DecorView 成员变量 :
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 对象;
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
作为基础布局, 下面分析这个布局;
- 1> requestFeature 设置 : 在
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 中;
<?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 设置, 根据处理结果加载不同的基础布局;
- 1> 创建布局容器 : 先创建
- 11.加载开发者自定义的布局 : 在
PhoneWindow
中的setContentView
中,mLayoutInflater.inflate(layoutResID, mContentParent);
语句执行的是加载开发者自己的布局文件; 其中mLayoutInflater
是在PhoneWindow
创建时就进行初始化的布局加载器;
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.布局加载器加载过程 :
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
方法;
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 的子接口 );
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
中;
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);
是核心语句;
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
中调用了ViewRootImpl
的setView
方法, 从此处开始逐层分析下一步的操作;- ① 在
setView
方法中调用了requestLayout
方法, - ② 在
requestLayout
方法中调用了scheduleTraversals
方法, - ③ 在
scheduleTraversals
方法中通过回调调用了TraversalRunnable
中的run
方法, - ④ 在这个
run
方法中调用了doTraversal
方法, - ⑤ 在
doTraversal
方法中调用了performTraversals
方法, 这是 UI 绘制的核心方法;
- ① 在
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
, 以确定控件的位置;
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
方法;
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 方法 : 在该方法中进行了 组件的大小计算, 根据组件的 模式 和 设定宽高 设置 计算组件大小;
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());
;
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
方法执行核心逻辑;
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.测量起点 :
ViewRootImpl
中performTraversals
方法中 调用的performMeasure
方法是测量的起始位置;
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
;
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 参数;
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 值;
- 1> 与父容器相大小相等 MeasureSpec.EXACTLY :
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 数值;
//根据传入的 大小值 和 模式 创建一个 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;
//提取布局测量模式
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
- 4.getSize 获取大小值 :
measureSpec & ~MODE_MASK
只要后 30 位, 前两位 设置成 0 ;
//提取最终的值
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
- 5.MeasureSpec 规格 相关代码 :
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 方法 : 该方法中的调用了
DecorView
的measure
方法;
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 方法负责处理主要的测量逻辑;
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.布局摆放引入 : 之前提到在
ViewRootImpl
的performTraversals
处理核心逻辑, 先后通过直接或间接调用下面方法 ① 调用 performMeasure 方法进行测量, ② 调用 performLayout 进行布局摆放, ③ 调用 performDraw 进行绘制 ; 在本章节详细介绍如何进行布局摆放; - 2.布局摆放 与 布局测量 流程的相同点 : 两个流程的核心逻辑方式基本一样, 由 根 组件
DecorView
调用layout
方法, 在其中调用onLayout
方法, 在onLayout
方法中递归调用子组件的onLayout
的方法, 直至调用到最底层的组件; 这个逻辑 与 布局测量基本一致, 布局测量也是 顶层 View 循环调用子组件的onMeasure
方法, 直至调用到 最底层的 组件; - 2.performLayout 方法介绍 : 核心是调用了
View
的layout
方法, 在layout
中调用了onLayout
方法; 这里注意,performMeasure
最终也是调用了 View 的onMeasure
方法;
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 方法需要组件自己实现, 不同的组件实现的逻辑不同;
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 ) 坐标对比 : 当传入新的初始化坐标时, 会将本次传入的坐标 和 之前存储的坐标进行对比, 如果一致, 那么继续使用之前的坐标, 如果不一致, 那么需要重新进行布局计算;
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 中, 该方法是一个空方法, 方法体重没有任何代码;
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
方法, 这个方法是调用的核心逻辑;
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
进行布局绘制;
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 的范围设置成整个屏幕大小;
- ( 1 ) fullRedrawNeeded 参数说明 ( 重绘标识 ) :
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;
- ( 1 ) 获取 Canvas 对象 : 由
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 方法解析 : 绘制的步骤 : ① 绘制背景, ② 图层保存, ③ 绘制组件内容, ④ 绘制子组件, ⑤ 图层恢复, ⑥ 绘制装饰内容; 下面会逐个步骤进行解析;
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 解析 :
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.两次调用产生原因 : 在
ViewRootImpl
中performTraversals
方法中 会调用performMeasure
方法,performMeasure
方法会调用onMeasure
方法, 同时performTraversals
方法中会再次调用ViewRootImpl
的scheduleTraversals
方法, 从而又重新调用了一次performTraversals
方法, 因此onMeasure
方法被调用了两次; - 3.回顾上述调用层次 :
- ①
ActivityThread
中的ApplicationThread
回调对应的scheduleResumeActivity
方法, 在该方法中向 H ( Handler 子类 ) 发送 Message 信息; - ② H 中的
handleMessage
方法中, 通过识别 Message 信息的 what 值 来调用handleResumeActivity
方法; - ③ 在
handleResumeActivity
方法中调用了WindowManager
的addView
方法; - ④
WindowManager
的addView
方法实际上调用的是WindowManagerGlobal
的addView
方法; - ⑤
WindowManagerGlobal
中的addView
方法, 调用的是ViewRootImpl
的addView
方法; - ⑥
ViewRootImpl
的addView
调用了requestLayout
方法; - ⑦ 在
requestLayout
方法中调用了scheduleTraversals
方法; - ⑧
scheduleTraversals
方法中调用doTraversal
方法; - ⑨ 在
doTraversal
方法中调用了performTraversals
方法; - ⑩ 而在
performTraversals
方法中, 如果组件在显示的情况下, 又调用了一次scheduleTraversals
, 而在performTraversals
中进行 测量 摆放 绘制 流程, 因此 测量流程onMeasure
被调用了 两次;
- ①
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 代码示例 :
/**
* 宽和高 都不规则的组件 进行排列
*
* 基本思想 : 确定布局组件的宽度 和 高度, 根据 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.自定义瀑布流容器代码地址 : 瀑布流容器 ;
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);
}
}