美文网首页
学习笔记—Android 控件架构与自定义控件

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

作者: 哈皮阿呆 | 来源:发表于2016-04-03 23:07 被阅读108次

    《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;
    }```

    相关文章

      网友评论

          本文标题:学习笔记—Android 控件架构与自定义控件

          本文链接:https://www.haomeiwen.com/subject/lkuulttx.html