学习笔记—Android 控件架构与自定义控件

《Android 群英传》第三章 “ Android 控件架构与自定义控件详解 ” 学习笔记

Android控件架构

在 Android 中,控件被分为两类,即 ViewGroup 与 View。在界面上,控件其实是一个矩形。ViewGroup 可作为父控件包含一个或多个View。通过ViewGroup,整个界面上的控件形成了一个树形结构(控件树)。上层控件负责下层子控件的测量与绘制,并传递交互事件。在控件树顶部,有一个 ViewParent 对象,它负责统一调度和分配所有的交互管理事件,对整个视图进行整体控制。

View 树结构
Android UI 界面架构图

关于 setContentView()

每个 Activity 都包含一个 Window 对象,在 Android中Window 对象通常由 PhoneWindow 来实现。 PhoneWindow 将一个 DecorView 设置为整个应用的根 View。DecorView 作为窗口的顶层视图,封装了一些窗口操作的通用方法。DecorView 将要显示的内容呈现在 PhoneWindow 上,这里面的所有 View 的监听事件,都通过 WindowManagerService 来进行接收。
DecorView 分为两部分,TitleView 与 ContentView。ContentView 实际是一个 ID 为content 的 FrameLayout,我们通过 setContentView() 方法设置的布局就会显示在这个 FrameLayout 中。
当onCreate() 方法中调用 setContentView() 方法后,ActivityManagerService 会回调 onResume() 方法,此时系统才会把整个 DecorView 添加到 PhoneWindow 中,并显示出来,从而完成界面绘制。

// 来自源码 
// 得到 Window 对象,设置布局(疑问:得到的Window对象是PhoneWindow对象吗?)
// 初始化ActionBar
public void setContentView(@LayoutRes int layoutResID) {    
    getWindow().setContentView(layoutResID);   
    initWindowDecorActionBar();
}

private void initWindowDecorActionBar() {
        Window window = getWindow();

        // Initializing the window decor can change window feature flags.
        // Make sure that we have the correct set before performing the test below.
        window.getDecorView();

        // 判断是否显示ActionBar 
        if (isChild() || !window.hasFeature(Window.FEATURE_ACTION_BAR) || mActionBar != null) {
            return;
        }

        mActionBar = new WindowDecorActionBar(this);
        mActionBar.setDefaultDisplayHomeAsUpEnabled(mEnableDefaultActionBarUp);

        mWindow.setDefaultIcon(mActivityInfo.getIconResource());
        mWindow.setDefaultLogo(mActivityInfo.getLogoResource());
    }```

## View 的测量
绘制 View 需要知道它的大小和位置。这个过程在 onMeasure() 方法中进行。同时 MeasureSpec 类是帮助我们测量 View 的。MeasureSpec 中有三个int型常量。
    /**
     * Measure specification mode: The parent has not imposed any constraint
     * on the child. It can be whatever size it wants.
     */
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    /**
     * Measure specification mode: The parent has determined an exact size
     * for the child. The child is going to be given those bounds regardless
     * of how big it wants to be.
     */
    public static final int EXACTLY     = 1 << MODE_SHIFT;

    /**
     * Measure specification mode: The child can be as large as it wants up
     * to the specified size.
     */
    public static final int AT_MOST     = 2 << MODE_SHIFT;
看英文注释,不是很明白。这里引用下书中的解释:
* EXACTLY 
精确值模式,当我们将控件的layout_width 属性或 layout_height 属性指定为具体数值时,比如设置宽为100dp,或者指定为match_parent 属性时,测量模式即为 EXACTLY 模式
*  AT_MOST
最大值模式,当控件的layout_width 属性或 layout_height 属性指定为 wrap_content 时,这时候的控件的大小一般随它的子控件或内容的变化而变化。
* UNSPECIFIED
 未指定模式(书中没有中文解释),不指定测量模式,View 想多大就多大。

View 类默认的 onMeasure() 方法只支持 EXACTLY 模式。自定义的 View 需要重写 onMeasure() 才能使用wrap_content 属性。

// View 默认的 onMeasure 方法
// 得到宽高后调用setMeasuredDimension
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

    // 得到宽高的测量模式和大小(来自 TextView 的 onMeasure() 方法)
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    // ...
自定义一个View,重写onMeasure() 方法,测量宽高

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    setMeasuredDimension(getWidthSize(widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

// 通过测量模式返回宽度大小(疑问:为何 onMeasure() 方法会被多次执行???)
private int getWidthSize(int widthMeasureSpec) {
    int mode = MeasureSpec.getMode(widthMeasureSpec);
    int size = MeasureSpec.getSize(widthMeasureSpec);

    int width;

    if (mode == MeasureSpec.EXACTLY) {
        width = size;
    } else {
        width = 200;
        if (mode == MeasureSpec.AT_MOST) {
            width = Math.min(width, size);
        }
    }
    Log.i("size", size + "");
    Log.i("width", width + "");

    return width;
}

## View 的绘制
当测量好 View 后,可以重写 onDraw() 方法,在 Canvas 对象上绘制图形。Canvas 就像一张画布,使用 Paint 就可以在上面画东西了。

## ViewGroup 的测量
当 ViewGroup 的大小为 wrap_content 时,ViewGroup 就需要对子 View 进行遍历,以便获取所有子 View 的大小,从而决定自身的大小。其他模式则通过具体的指定值来设置大小。
ViewGroup 通过遍历子 View,从而调用子 View 的 onMeasure() 来获取每一个子 View的测量结果。
当子 View 测量完毕后,还需要放置 View 在界面的位置,这个过程是 View 的 Layout过程。ViewGroup 在执行 Layout 过程中,同样使用遍历调用子 View 的Layout 方法,确定它的显示位置。从而决定布局位置。
自定义 ViewGroup 是,一般要重写 onLayout() 方法来控制子 View 显示位置的逻辑。支持 wrap_content 属性,还需要重写 onMeasure 来决定自身的大小。

阅读 LinearLayout 的部分源码,理解测量过程

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 判断布局方向,调用不同的测量方法
if (mOrientation == VERTICAL) {

        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}    

在 measureVertical(widthMeasureSpec, heightMeasureSpec) 发现如下代码
    final int count = getVirtualChildCount();
    
     //         .... 省略N行

    // See how tall everyone is. Also remember max width.
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);

        if (child == null) {
            mTotalLength += measureNullChild(i);
            continue;
        }

        if (child.getVisibility() == View.GONE) {
           i += getChildrenSkipCount(child, i);
           continue;
        }

        if (hasDividerBeforeChildAt(i)) {
            mTotalLength += mDividerHeight;
        }

        LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

        totalWeight += lp.weight;

// .... 省略N行

对 Child View 进行遍历,去测量 weight 等。接着

measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);


通过方法名我们也能理解这是在 Layout 之前测量 子 View。接着跟下去

protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

发现这里正是在获取子 View 宽高的 MeasureSpec,然后回调子 View 的measure() 方法,直接跳转去看这个方法在干什么。(猜测应该会去调用 onMeasure 方法了)

int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
// 这里的确发生了 调用 子 View 的 onMeasure() 去测量大小
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}```

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容