Android学习笔记---深入理解View#04

上次我们对View的测量过程有了了解,接着这次肯定就是要沿着View的3大流程往下走。我们本次的主角就是View的Layout过程。

performLayout()开始出发

我们的已经知道了View的布局过程layout pass就是从performLayout()开始的,那么我们就先来大概的浏览一下这个函数的代码。

TIP: 这里我们先简略的浏览整个函数的代码(不用认真看),后面我们会将代码分开几个部分,一个部分一个部分的进行分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight)
{

// 先将当前是否有布局请求的flag设置为false
mLayoutRequested = false;

mScrollMayChange = true;
// 将当前正在布局的flag设置为true
mInLayout = true;
// 获取decorView
final View host = mView;
if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
Log.v(TAG, "Laying out " + host + " to (" +
host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
}

Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
// 对decorView进行布局
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
// 将当前正在布局的flag设置为false
mInLayout = false;
// 获取当前请求布局的View的个数
int numViewsRequestingLayout = mLayoutRequesters.size();
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) {
// Set this flag to indicate that any further requests are happening during
// the second pass, which may result in posting those requests to the next
// frame instead
// 设置正在处理布局请求的flag为true
mHandlingLayoutInLayoutRequest = true;

// Process fresh layout requests, then measure and layout
// 获取需要进行布局的View的个数
int numValidRequests = validLayoutRequesters.size();
for (int i = 0; i < numValidRequests; ++i) {
final View view = validLayoutRequesters.get(i);
Log.w("View", "requestLayout() improperly called by " + view +
" during layout: running second layout pass");
view.requestLayout();
}
measureHierarchy(host, lp, mView.getContext().getResources(),
desiredWindowWidth, desiredWindowHeight);
mInLayout = true;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

mHandlingLayoutInLayoutRequest = false;

// Check the valid requests again, this time without checking/clearing the
// layout flags, since requests happening during the second pass get noop'd
validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
if (validLayoutRequesters != null) {
final ArrayList<View> finalRequesters = validLayoutRequesters;
// Post second-pass requests to the next frame
getRunQueue().post(new Runnable() {
@Override
public void run() {
int numValidRequests = finalRequesters.size();
for (int i = 0; i < numValidRequests; ++i) {
final View view = finalRequesters.get(i);
Log.w("View", "requestLayout() improperly called by " + view +
" during second layout pass: posting in next frame");
view.requestLayout();
}
}
});
}
}

}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;

}

Read More

Android学习笔记---深入理解View#03

上一篇的结尾中,我们发现了View的绘制发生在ViewRootImplperformTraversals()中.而且在其中先后调用了performMeasure(),performLayout(),performDraw().
如此一来,我们又有了新的猎物了.就像美食一样,好吃的东西一定要仔细地品尝.在上主菜之前,我们先来点开胃菜.我们先来了解一下Android是怎样绘制View的.(官方的文档How Android Draws Views)

开胃菜(关于View绘制时需要知道的常识)

Activity接受焦点时,就会被要求绘制其布局.虽然Android Framework会处理这个过程,但Activity必须提供整个布局层级的根节点,因为需要知道从哪开始绘制.

Activity的整个布局被转换成了一棵树,绘制整个布局就相当于了遍历整颗树并把每个节点的View绘制出来.相应地,ViewGroup负责要求它的每一个child进行绘制,而View则负责绘制自己.由于树的遍历是有序的,所以父View绘制之前会先绘制其子View,而兄弟节点会按照在树中出现的顺序进行绘制.
绘制布局需要进行两个传递过程(pass process):分别是测量时的传递(measure pass)布局时的传递(layout pass).这里所说的传递指的是在view tree的各个节点之间的传递.

  1. measure passmeasure(int,int)中实现,而且它是一个从上到下的传递.在view tree中每个View节点都将它的尺寸规格向下传递给它的孩子,在整个传递过程结束时,每个节点都应该拥有了自己的测量值(尺寸大小).
  2. layout passlayout(int,int,int,int)中发生,它同样也是一个从上到下的传递.在传递过程中每个parent都需要根据在measure pass时得到的测量值在布局中放置它的所有children.

下面贴上一张普通的view tree的图.

Read More

Android学习笔记---深入理解View#02

上篇文章中我们了解到了setContentView()背后所发生的事情.先用上一篇文章最后总结的图片来回顾一下setContentView()的流程.

但是后来我发现,我们只是知道了mDecor是怎样与mContentParent联系在一起并将我们的activity_main.xml布局添加到其中.而并不知道mDecor是如何添加到PhoneWindowActivity上的.所以本篇我们就来探究一下这个问题.

寻找我们的 mDecor

既然我们在PhoneWindow里没有发现mDecor是如何添加到window上的.那么我就需要到别的地方去寻找mDecor的线索,那我们要该从哪入手呢?还记得上一篇我们从Activity的setContentView()出发时,最终调用的是mWindow(PhoneWindow)setContentView()函数,而mDecorPhoneWindow的成员变量,mWindowActivity的成员变量,那么我们可以在Activity的代码中寻找一下我们的线索mDecor.
果然通过在Activity代码中进行mDecor的关键字搜索,我们找到了线索.

1
2
3
4
5
6
7
8
9
View mDecor = null;
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}

从上面的代码可以看到Activity通过WindowManageraddView()方法将mDecor添加到Window上.但我们发现这里的mDecorPhoneWindow中的mDecor好像并不相同.而且在代码中(Activity代码中的mDecor)与(PhoneWindowmDecor)并没有明显的联系在一起.难道线索就这么断了?我决定使用绝招(搜索引擎),果然我找到了另一个切入点.
经过一番Internet后,我知道了View的布局绘制是发生在Activity的创建过程,而Activity的创建是由一个名为ActivityThread的类进行控制的.下面我们来验证一下,看看能否找到线索.
首先我们找到ActivityThread的源码,这里有两种方法:

  1. 直接在Internet上阅读在线的源码
  2. 在Android Studio中新建一个简单的工程,在MainActivityonCreate()函数中下一个断点,然后运行调试.我们可以在调试工具的调用栈中找到ActivityThread.

找到了ActivityThread的源码后,对makeVisible关键字进行搜索,果然找到了调用的地方,在handleResumeActivity()中出现了makeVisible()的调用,既然出现了相关的调用,那我们就看一下handleResumeActivity()的代码吧.

Read More

Android学习笔记---深入理解View#01

上次的两篇文章,我们讨论了创建自定义View的基本流程.对View有了基本的了解后,有好奇心的同学可能会对View的基本原理充满好奇(其实我也非常好奇View在Android系统下是怎样实现的),所以我就本着好奇心看了很多关于View的实现原理和View的基本工作流程的文章,也看了一些源码,对View有了更加深入的理解.在这我就跟大家分享一下我对View的理解,希望能对大家的学习有所帮助.若有什么错误的地方还希望大家帮我指出.

setContentView开始

写过Android应用的都知道,我们最开始接触View是在布局文件activity_main.xml中.在我们的MainActivityonCreate()方法中,通过setContentView(R.layout.activity_main)设置我们的Activity布局.View的所有工作都是从这里开始的,为了一探究竟,我们就进到setContentView()中去,看看它究竟是怎样实现的.下面就是Activity的setContentView的代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Set the activity content from a layout resource. The resource will be
* inflated, adding all top-level views to the activity.
*
* @param layoutResID Resource ID to be inflated.
*
* @see #setContentView(android.view.View)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/

public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

注意:我们这里的代码是Activity的代码,而不是AppCompatActivity的代码.AppCompatActivity是Android为使用兼容库的应用所提供的一个Activity基类,这里为了研究方便,减少其他无关代码的干扰,我们使用Activity类进行讨论.

我们可以看到这里最终调用的是getWindow().setContentView(layoutID)进行布局设置.

1
2
// Window 类下的setContentView()
public abstract void setContentView(View view);

Read More

Android学习笔记---重新学习自定义View#02

上次我们研究了View的构造函数,自定义View最重要的步骤就是完成我们View的绘制.我们本篇就来好好的研究一下它.
我们都知道重写onDraw()方法便可对View进行各种绘制操作,但是在绘制之前我们还需要对View的布局进行处理,因为我们的自定义View可能需要在多种不同的布局环境下展现,如果不对其进行布局的处理,可能会使View的绘制得不到想要的结果.

onSizeChanged()

处理布局无非就是要知道View在屏幕展现时的大小和位置,在绘制前计算得到这些值以及其它与View的大小相关的值,我们便能将View正确的绘制到屏幕上.
而最简单的方法就是重写onSizeChanged()函数,这个函数会在View第一次分配大小时,以及当View的大小发生变化时被调用.因为当View的大小发生改变时才会对其布局造成影响,所以我们只需在onSizeChanged()函数中重新计算View的大小,位置,以及其他与View大小相关的值即可.这样我们可以避免在每次进行绘制时都在onDraw()函数中进行计算,可以提高效率.
下面是一段示例代码,使用的还是上次的相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private float mWidth, mHeight;

/**
* 在View大小发生变化是会被调用.可以在此函数进行View的大小,位置以及其他相关值的计算
*
* @param w 当前View的宽度
* @param h 当前View的高度
* @param oldw View改变前的宽度
* @param oldh View改变前的高度
*/

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
float xpad = (float)(getPaddingLeft()+getPaddingRight());
float ypad = (float)(getPaddingTop()+getPaddingBottom());

mWidth = w-xpad;
mHeight = h-ypad;

}

Read More