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

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

Android控件架构

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

学习笔记—Android 控件架构与自定义控件_第1张图片
View 树结构
学习笔记—Android 控件架构与自定义控件_第2张图片
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;
}```

你可能感兴趣的:(学习笔记—Android 控件架构与自定义控件)