IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android View的工作流程(二) measure过程 -> 正文阅读

[移动开发]Android View的工作流程(二) measure过程

?一.View的measure过程

View的measure过程是由View的measure方法完成的,他是一个被final关键字修饰的方法,我们无法重写该方法,但是measure方法中会调用onMeasure方法来设置计算后的宽高,onMeasure方法是可以被重写的:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

getDefaultSize方法:

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

可以看到EXACTLY和AT_MOST两种模式下都是以specSize作为返回值,而这个specSize就是View测量后的大小。如果View采用AT_MOST模式即wrap_content来绘制那么结合上一篇文章中的图例:

?可以知道View最终在父布局中的绘制会以parentSize作为specSize的实际大小,即我们自定义的直接继承自View的View在布局中使用wrap_content的效果和match_parent是一样的,而解决这个问题的方式就需要重写onMeasure方法来对wrap_content做特殊处理

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
        val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
        val heightSpecSize = MeasureSpec.getSize(heightMeasureSpec)
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, mHeight)
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, heightSpecSize)
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, mHeight)
        }
    }

重写的onMeasure方法中我们需要提供一个View在wrap_content情况下使用的宽高mWidth和mHeight,非wrap_content的场景下就直接使用系统提供的测量值widthSpecSize/heightSpecSize即可。具体mWidth和mHeight该怎么取值要根据实际使用场景来定,参考TextView的onMeasure方法部分源码:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width;
        int height;

        ...

        if (widthMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            width = widthSize;
        } else {//AT_MOST

            ...

            if (boring == null || boring == UNKNOWN_BORING) {
                if (des < 0) {
                    des = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mTransformed, 0,
                            mTransformed.length(), mTextPaint, mTextDir, widthLimit));
                }
                width = des;
            } else {
                width = boring.width;
            }

            final Drawables dr = mDrawables;
            if (dr != null) {
                width = Math.max(width, dr.mDrawableWidthTop);
                width = Math.max(width, dr.mDrawableWidthBottom);
            }
            
            ...
        }

        ...

        if (heightMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            height = heightSize;
            mDesiredHeightAtMeasure = -1;
        } else {//AT_MOST
            int desired = getDesiredHeight();

            height = desired;
            mDesiredHeightAtMeasure = desired;

            if (heightMode == MeasureSpec.AT_MOST) {
                height = Math.min(desired, heightSize);
            }
        }
        
        ...
        
        setMeasuredDimension(width, height);
    }

可以看到TextView对AT_MOST模式下的宽高都进行了重新定义,具体赋值逻辑太过复杂就不细说了。

二.ViewGroup的measure过程

ViewGroup是一个继承自View的抽象类,它并没有实现onMeasure方法,这是因为ViewGroup作为一个父类他不能对不同需求和场景下的子类布局作统一测量,就像LinearLayout和RelativeLayout一样是两种完全不同的布局方式,他们的测量方式需要他们自己去实现。但是ViewGroup也提供了一个measureChildren方法:

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

逻辑上很简单,就是按顺序调用measureChild方法来测量子View:

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

measureChild方法和上一篇文章提到的measureChildWithMargins方法原理是一样的,只不过没有把子View的外边距加进去。不过这个measureChildren方法在已知的几种布局中只在AbsoluteLayout布局中有使用,而AbsoluteLayout作为最简单粗暴的一种布局也是几乎没有使用场景,所以可以看出对于子View的测量LinearLayout、RelativeLayout等常用布局都是需要自己去实现的。下面以LinearLayout的onMeasure方法为例:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

measureVertical方法:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    ...

    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            mTotalLength += measureNullChild(i);
            continue;
        }

        ...
        
        if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                // Optimization: don't bother measuring children who are only
                // laid out using excess space. These views will get measured
                // later if we have space to distribute.
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {
                if (useExcessSpace) {
                    // The heightMode is either UNSPECIFIED or AT_MOST, and
                    // this child is only laid out using excess space. Measure
                    // using WRAP_CONTENT so that we can find out the view's
                    // optimal height. We'll restore the original height of 0
                    // after measurement.
                    lp.height = LayoutParams.WRAP_CONTENT;
                }

                // Determine how big this child would like to be. If this or
                // previous children have given a weight, then we allow it to
                // use all available space (and we will shrink things later
                // if needed).
                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight);

                final int childHeight = child.getMeasuredHeight();
                if (useExcessSpace) {
                    // Restore the original height and record how much space
                    // we've allocated to excess-only children so that we can
                    // match the behavior of EXACTLY measurement.
                    lp.height = 0;
                    consumedExcessSpace += childHeight;
                }

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
                
                ...
            }
    }
}

可以看到LinearLayout会按序遍历每一个子View,并调用measureChildBeforeLayout方法来测量子View,测量完成后获取子View的measuredHeight累加到mTotalLength中作为最后LinearLayout的总高度。所有的子View都测量完成后,LinearLayout会用mTotalLength来测量自身的高度:

mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;

// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);

setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);

这里的resolveSizeAndState方法就是对不同测量模式下的LinearLayout高度分情况处理:

    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

当LinearLayout采用match_parent时就直接使用测量的specSize,如果采用wrap_content就使用累加得到的总高度值mTotalHeight,当然这个值也要小于等于父布局给的剩余高度,否则仍然以specSize作为LinearLayout的最终高度。

三.获取measure后的宽高

对于获取measure后的宽高View直接给我们提供了getMeasuredWidth/getMeasuredHeight方法,但是应该在什么时候使用这俩方法呢,首先View在很多情况下会出现多次测量的情况,所以在onMeasure方法中获取的measuredWidth/measuredHeight往往并不是最终正确的宽高,而onLayout是在onMeasure完全结束的情况下执行的,所以一般我们会在onLayout方法中去拿到最终的measuredWidth/measuredHeight。

如果在activity的声明周期方法里面去getMeasuredWidth/getMeasuredHeight会得到正确的宽高吗?答案往往是否定的,因为整个measure的过程和页面的生命周期并没有绑定,当我们在onStart、onResume方法里面去获取宽高时可能View还没有measure结束而获取到一个默认值0。Android为我们提供了以下几种方式去拿到正确的measuredWidth/measuredHeight:

1.onWindowFocusChanged

class DemoView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

    ...

    override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
        super.onWindowFocusChanged(hasWindowFocus)
        if (hasWindowFocus) {
            val width = this.measuredWidth
            val height = this.measuredHeight
        }
    }
    
    ...

}

这个回调在activity中也是可以设置的,但是需要注意的是它和activity的生命周期存在关联,可能会出现频繁回调的情况,当activity频繁的触发onResume和onPause时onWindowFocusChanged也会频繁的触发回调

2.view.post

View通过post方法把一个Runnable任务加到主线程消息队列的末尾,当这个Runnable执行时View早已经初始化好了:

btn1.post {
    val width = btn1.measuredWidth
    val height = btn1.measuredHeight
}

3.ViewTreeObserver

ViewTreeObserver提供了很多和视图树状态有关的接口,很多都是可以用来获取measuredWidth/measuredHeight,以OnGlobalLayoutListener为例:

class DemoView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
    
    ...    

    init {
        viewTreeObserver.addOnGlobalLayoutListener(object:ViewTreeObserver.OnGlobalLayoutListener{
            override fun onGlobalLayout() {
                viewTreeObserver.removeOnGlobalLayoutListener(this)
                val width = this@DemoView.measuredWidth
                val height = this@DemoView.measuredHeight
            }
        })
    }
    
    ...
}

当view树的状态改变或者里面的view可见性发生变化都会触发OnGlobalLayoutListener回调,此时measuredWidth/measuredHeight将是准确的。

4.measure(int widthMeasureSpec, int heightMeasureSpec)

通过手动对View进行measure来指定width和height,但是这里需要根据LayoutParams来分情况处理:

.match_parent

这种情况下无法直接手动measure,因为我们需要知道父布局的剩余空间大小parentSize,而在当前View中我们是无法知道父布局的剩余空间大小情况的

.具体数值dp/px

例如宽高都是100px:

class DemoView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
    
    ...    

    init {
        val widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY)
        val heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY)
        this.measure(widthMeasureSpec, heightMeasureSpec)
    }
    
    ...
}

.wrap_content

class DemoView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
    
    ...    

    init {
        val widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 shl 30) - 1, MeasureSpec.AT_MOST)
        val heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 shl 30) - 1, MeasureSpec.AT_MOST)
        this.measure(widthMeasureSpec, heightMeasureSpec)
    }
    
    ...
}

当使用AT_MOST模式时,我们可以指定specSize为其所能达到的最大值即measureSpec表示具体尺寸的后三十位全为1,所以specSize=1*10的30次方-1,用kotlin的代码表示就是(1 shl 30) - 1,java的代码表示就是(1 << 30) - 1。

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-07-17 16:34:46  更:2022-07-17 16:37:22 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 3:05:08-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码