https://www.jianshu.com/p/5baa20d3d953
(我在简书上的文章)

场景1

往Fragment里动态添加一个自定义View

  final ToastView toastView = new ToastView(webViewFragment.getContext());
                    webViewFragment.getContentView().addView(toastView);
                    webViewFragmentController = new WebViewFragmentController(webViewFragment, toastView);
                    toastView.init(webViewFragment,toastParamsBean);

布局如下

需求就是当只显示tv_title时,rl_wrapper的宽高为wrapper,但是无论怎么测量啥的方法都试过了不行。

最终自己试出可行的代码,但是并不清楚什么个原理

 /**
     * 测量View设置具体大小(注意一二三步,一步也不能少。)
     * @param view
     */
    public static void measureViewWrapWrap(View view){
        //第一步
        ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
        layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
        layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
        view.setLayoutParams(layoutParams);

        //第二步
        int w = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
        int h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
        Logger.d(TAG, "measureView: " +  "w = " + w + ",h = " + h);
        view.measure(w, h);

        //第三步
        int mWidth = view.getMeasuredWidth();
        int mHeight = view.getMeasuredHeight();
        Logger.e(TAG, "measureViewWrapWrap: mWidth = " + mWidth + ",mHeight = " + mHeight);

        ViewGroup.LayoutParams layoutParams2 = view.getLayoutParams();
        layoutParams2.width = mWidth;
        layoutParams2.height = mHeight;
        view.setLayoutParams(layoutParams2);
    }

场景2

wrap指定无效
https://blog.csdn.net/u010648159/article/details/81112643

https://www.jianshu.com/p/ca118d704b5e

现如今,作为一个5年程序猿,不能再得过且过,懵懵懂懂地写代码了。准备以这个问题为导向,深入研究一下Android动态布局相关的知识。

https://blog.csdn.net/jiangxuqaz/article/details/46941479
它里面有句话:"子View的widthMeasureSpec和heightMeasureSpec是由父View的widthMeasureSpec和heightMeasureSpec,LayoutParams和子ViewMarginLayoutParams决定的"
https://www.jianshu.com/p/08047f7f8fcf
这2篇文章里我好像找到了答案。

自定义View核心

核心一:Measure(测量)

1.https://www.jianshu.com/p/08047f7f8fcf (说的可能有点羞涩难懂,貌似是一种哲理提炼,但是最详细。最后的示例,也没有给出完整代码,只能他自己知道怎么回事。)
作者一直强调的重点

,"需求"一词,并不是作者独创,其实官方类的说明就是这么写的。
另外一个重点:LayoutParams,子View的Measure并不是独立的,而是和父View是息息相关的,通过LayoutParams向父类表达自己的期望,父类才知道Measure子View传递什么需求。

2.https://blog.csdn.net/a553181867/article/details/51494058
这篇分析源码,十分到位,思路特别地清晰,建议多看几遍。

上面这句话也正好解决了我博客最开始提出的疑问。

测量流程整理

从顶级View开始了测量流程,然后由外到内,逐级测量,调用View#measure方法(final修饰,不可更改),measure方法调用onMeasure方法(可以被复写,不同的View的实现基本不一样 )

  • View的onMeasure方法
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

仅仅是保存了测量的宽高

  • ViewGroup的onMeasure方法
    和View一样,未做复写。

  • 其它View的onMeasure方法
    基本都会复写,基本都会结合自己本身的宽高、自己本身的布局参数、子View的LayoutParams去综合计算出子View的宽和高的MeasureSpec,然后调用View#measure方法对子View进行测量。

代码上表现为
都有measureChild[XXX]方法(自己定义的或者复用ViewGroup的,FrameLayout就是复用ViewGroup的,RelativeLayout就是自定义的方法,measureChild[XXX]方法子类可能会复写),然后在这个方法内调用getChildMeasureSpec方法(自己定义的或者复用ViewGroup的)计算出子View的宽和高的MeasureSpec值。

子View的布局参数和测量大小,可能会导致当前View重新测量,如FrameLayout,看measureMatchParentChildren这个变量。

补充

但是,关于博客我有几点需要补充。
- 1.对于DecorView来说,它已经是顶层view了,没有父容器,那么它的MeasureSpec怎么来的呢
关于getRootMeasureSpec这个方法的第二个参数,一直追溯:lp ----> mWindowAttributes ----> WindowManager.LayoutParams

所以由此断定:DecorView的MeasureSpec的mode是EXACTLY,size就是窗口的宽高。

核心二:Layout(布局)

还是结合上面这位大神的博客分析https://blog.csdn.net/a553181867/article/details/51524527
和measure的形式差不多,子View也是通过复写onLayout达到不同的效果。
- View的onLayout方法
空实现,因为View不需要布局其它的View。
- ViewGroup的onLayout方法
抽象方法,这里有java语法有点特殊:View的onLayout是空实现,它的子类ViewGroup可以把它复写成抽象方法,而且方法域由protected变为public。
正因为ViewGroup没有复写onLayout方法,所以xml直接用ViewGroup嵌套View,View是不会显示的。

  • 其它View的onLayout方法
    根据子View的各种可能干扰布局位置的参数,子View的测量过的宽高,调用子View的layout方法进行布局。

补充

1.纠正一下博客的一个错误
View的layout并非是final的,而是DecorView没有复写这个方法。

核心三:Draw(绘制)

还是结合上面这位大神的博客分析
https://blog.csdn.net/a553181867/article/details/51570854
可以看出,View的Draw流程也比较复杂的。performDraw() ---> draw(boolean fullRedrawNeeded) ---> drawSoftware ---> mView.draw(canvas)
和measure、layout的形式差不多,子View也是通过复写onDraw达到不同的效果。如果View是透明的,就没有调用onDraw,因为透明的View没有绘制的意义

  • View的onDraw方法
    空实现
  • ViewGroup的onDraw方法
    和View一样,未复写。
  • 其它View的onDraw方法
    一般都未复写

循环绘制机制

和measure、layout不同,draw的循环递归不是在onXXX方法里实现。
View的draw方法里会调用dispatchDraw(Canvas canvas)方法,View本身这个方法是空实现,但是ViewGroup作了实现,会调用drawChild方法,此方法会调用子View的draw(Canvas canvas, ViewGroup parent, long drawingTime)方法,进而再调用子View的draw(Canvas canvas)方法,如此循环递归。

View的绘制不仅仅是onDraw方法

结合上面和博客还有源码分析可知,onDraw只是绘制View的content(内容),并不能代表绘制的全部。draw的过程其实有很多步骤:
- 第1步:绘制背景
调用drawBackground(Canvas canvas)方法

这个方法会调用背景Drawable的draw方法,不同的Drawable会有不同的实现。
- 第3步:绘制内容
调用onDraw(Canvas canvas)方法
- 第4步:绘制子View
调用dispatchDraw(Canvas canvas)方法
- 第6步:绘制前景
调用onDrawForeground(Canvas canvas) 方法,
绘制滚动指示器:onDrawScrollIndicators(canvas);
绘制滚动条:onDrawScrollBars(canvas);
最后,和绘制背景类似,这个方法会调用前景Drawable的draw方法,不同的Drawable会有不同的实现。

自定义View实践

上面写了这么多,准备再写一篇事件分发,然后出一篇自定义View实践文章。

绘制冲突

1.ScrollView与ListView嵌套冲突的问题

ScrollView嵌套ListView,会导致ListView只显示一个Item的高度, 无论ListView的高度设置成任何的值(match、wrap、exactly)都如此。这里属于View的绘制冲突,就不属于事件冲突了,不要搞混了。

网上提供了各种解决办法,下面我从源码的角度分析分析实质的原因:

先感性地写一些Demo

获取测量模式的工具类

public class CUtil {
    public static String getMeasureModeName( int measureSpec){
        int result = View.MeasureSpec.getMode(measureSpec);
        if(View.MeasureSpec.UNSPECIFIED == result){
            return "UNSPECIFIED";
        }else if(View.MeasureSpec.EXACTLY == result){
            return "EXACTLY";
        }else if(View.MeasureSpec.AT_MOST == result){
            return "EXACTLY";
        }
        return "UNKNOWN";
    }
}

自定义一个ListView,打印测量相关的日志。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Log.e(TAG, "onMeasure: " + CUtil.getMeasureModeName(heightMeasureSpec));
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int h  = getMeasuredHeightAndState();
        Log.e(TAG, "测量后的高度: "  + h);
    }
  • Demo1:有ScrollView嵌套
<com.example.eventdispatchdemo.scrollView.MyScrollView
        android:id="@+id/scrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/holo_blue_bright"
        >
        <com.example.eventdispatchdemo.scrollView.MyListView
            android:id="@+id/listview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/holo_red_dark"
            />
    </com.example.eventdispatchdemo.scrollView.MyScrollView>

输出日志:

MyListView: onMeasure: UNSPECIFIED
MyListView: 测量后的高度: 90
  • Demo2:正常情况,只有ListView。
     <com.example.eventdispatchdemo.scrollView.MyListView
            android:id="@+id/listview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/holo_red_dark"
            />

输出日志:

MyListView: onMeasure: EXACTLY
MyListView: 测量后的高度: 2088

找到突破口分析

从上面的日志分析可知,ScrollView的嵌套导致了ListView的测量模式和大小的异常,所以直接分析ScrollView。真别说,我在ScrollView里立马找到一个方法可以解决问题:

   public void setFillViewport(boolean fillViewport) {
        if (fillViewport != mFillViewport) {
            mFillViewport = fillViewport;
            requestLayout();
        }
    }

默认mFillViewport是false,通过调用setFillViewport即可解决冲突问题。
但是不管给ListView设置什么高度属性,ListView的高度始终是match_parent的,因为ListView进行第二次测量。

  • #### 分析ScrollView的测量源码(基于API28):
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if (!mFillViewport) {
            return;
        }
        //mFillViewport为true会执行下面的代码,将ChildView全部设置为和ScrollView一般的大小。

        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            return;
        }

        if (getChildCount() > 0) {
            final View child = getChildAt(0);
            final int widthPadding;
            final int heightPadding;
            final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
            final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (targetSdkVersion >= VERSION_CODES.M) {
                widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
                heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
            } else {
                widthPadding = mPaddingLeft + mPaddingRight;
                heightPadding = mPaddingTop + mPaddingBottom;
            }

            final int desiredHeight = getMeasuredHeight() - heightPadding;
            if (child.getMeasuredHeight() < desiredHeight) {
                final int childWidthMeasureSpec = getChildMeasureSpec(
                        widthMeasureSpec, widthPadding, lp.width);
                final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                        desiredHeight, MeasureSpec.EXACTLY);
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

所以我们将目光放到

super.onMeasure(widthMeasureSpec, heightMeasureSpec);

上,也就是FrameLayout的测量。通过打印MyScrollView的日志

MyScrollView: onMeasure: w-EXACTLY,h-EXACTLY,isFillViewPort:true

可以发现,ScrollView的宽高测量模式都是EXACTLY(因为布局是match_parent),而最终MyListView打印的高度测量模式是UNSPECIFIED,那么这中间又有怎样的故事?
到此处,可能有人就觉得没有必要再分析FrameLayout了,因为FrameLayout嵌套ListView不会有冲突。其实这是个错误的理解,虽然ScrollView是调用的FrameLayout的onMeasure方法,但是onMeasure方法的运行载体还是ScrollView。)
接着看FrameLayout的测量方法:

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                ...
            }
        }
        ...
    }

接着点击measureChildWithMargins方法,会跳转到ViewGroup,一直往下分析,想破了头也不可能得出“ScrollView的高度是EXACTLY,到了ListView的测量高度为UNSPECIFIED”的结论。
杜娘解决了我的疑惑:https://blog.csdn.net/u011247387/article/details/90759338
其实,ScrollView复写了measureChildWithMargins方法,只是代码跳转到了ViewGroup。
看ScrollView的measureChildWithMargins方法:

 @Override
    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 usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
                heightUsed;
        final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
                MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

果然,ScrollView为子类指定高度的测量模式都是UNSPECIFIED。
那么问题就在于:

MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal

为什么只给ListView一行的item高度?
那这个值到底是多少,复写一下MyScrollView的measureChildWithMargins方法:

  @Override
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        final int usedTotal = getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin +
                heightUsed;
        int ch = MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal;
        Log.e(TAG, "getPaddingTop: " + getPaddingTop());
        Log.e(TAG, "getPaddingBottom: " + getPaddingBottom());
        Log.e(TAG, "lp.topMargin: " + lp.topMargin);
        Log.e(TAG, "lp.bottomMargin: " + lp.bottomMargin);
        Log.e(TAG, "ch: " + ch);
        final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
                MeasureSpec.UNSPECIFIED);
        Log.e(TAG, "ch2: " + MeasureSpec.getSize(childHeightMeasureSpec));
        super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
    }

对于API23以上的手机,注意上面的makeMeasureSpec应该是makeSafeMeasureSpec方法,只不过这个方法是hide,需要通过反射调用。

@Override
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        final int usedTotal = getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin +
                heightUsed;
        int ch = MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal;
        Log.e(TAG, "getPaddingTop: " + getPaddingTop());
        Log.e(TAG, "getPaddingBottom: " + getPaddingBottom());
        Log.e(TAG, "lp.topMargin: " + lp.topMargin);
        Log.e(TAG, "lp.bottomMargin: " + lp.bottomMargin);
        Log.e(TAG, "ch: " + ch);
        int childHeightMeasureSpec = 0;
        try {
            childHeightMeasureSpec = (int) ReflectManger.invokeMethod(MeasureSpec.class,null,"makeSafeMeasureSpec",new Class[]{Integer.class,Integer.class},
                    new Object[]{ Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
                            MeasureSpec.UNSPECIFIED}
                    );
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

        Log.e(TAG, "ch2: " + MeasureSpec.getSize(childHeightMeasureSpec));
        super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
    }

日志输出:

MyScrollView: onMeasure: w-EXACTLY,h-EXACTLY,i
MyScrollView: getPaddingTop: 0
MyScrollView: getPaddingBottom: 0
MyScrollView: lp.topMargin: 0
MyScrollView: lp.bottomMargin: 0
MyScrollView: ch: 1557
MyScrollView: ch2: 1557
MyListView: onMeasure: UNSPECIFIED,测量前的H size = 0
MyListView: 测量后的高度: 90

所以,ScrollView是无辜的,它传递给ListView的测量高度确实是1557,到了ListView,ListView测量前是0,测量后就成了90了。那么最有可能就是measure方法搞的鬼了,这个先放一放。

  • #### 分析ListView的测量源码:
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Sets up mListPadding
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childWidth = 0;
        int childHeight = 0;
        int childState = 0;

        mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
        if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
                || heightMode == MeasureSpec.UNSPECIFIED)) {
            final View child = obtainView(0, mIsScrap);

            // Lay out child directly against the parent measure spec so that
            // we can obtain exected minimum width and height.
            measureScrapChild(child, 0, widthMeasureSpec, heightSize);

            childWidth = child.getMeasuredWidth();
            childHeight = child.getMeasuredHeight();                                      //测量ItemView的高度
            ...
        }
        ...
        if (heightMode == MeasureSpec.UNSPECIFIED) {                          //获取ListView的测量高度
            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                    getVerticalFadingEdgeLength() * 2;
        }

        if (heightMode == MeasureSpec.AT_MOST) {
            // TODO: after first layout we should maybe start at the first visible position, not 0
            heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
        }

        setMeasuredDimension(widthSize, heightSize);
         ...
    }

ListView的测量模式被ScrollView指定为UNSPECIFIED,那么ListView的高度就是由

    if (heightMode == MeasureSpec.UNSPECIFIED) {                          //获取ListView的测量高度
            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                    getVerticalFadingEdgeLength() * 2;
  }

这段代码来决定的,ListView不设置额外的属性的话,heightSize就是等于childHeight的高度。

总结:

ScrollView与ListView嵌套冲突的问题,其实是一个共性的问题:本质上就是ScrollView强制为子View的高设置了UNSPECIFIED的测量模式,如果子View的高针对UNSPECIFIED测量模式做了特殊处理,就会发生奇怪的现象,我们习惯称之为冲突。

果不奇然,ScrollView嵌套其它的View也会出现问题,下面写几个Demo研究一下:

ScrollView嵌套RelativeLayout冲突示例1:

现象

》》MyRelativeLayout的布局高度为match_parent,MyRelativeLayout的不显示。

打印日志:

MyRelativeLayout: onMeasure: UNSPECIFIED,测量前的H size = 0
MyRelativeLayout: 测量后的高度: 0

》》将MyRelativeLayout的布局高度写死成100dp,MyRelativeLayout的高度显示正常。

打印日志:

MyRelativeLayout: onMeasure: UNSPECIFIED,测量前的H size = 0
MyRelativeLayout: 测量后的高度: 300

分析RelativeLayout的测量方法(没有子View的情况)

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        int height = 0;
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        final boolean isWrapContentHeight = heightMode != MeasureSpec.EXACTLY;   //为true
        ... //中间省略子View的遍历
        if (isWrapContentHeight) {
            // Height already has top padding in it since it was calculated by looking at
            // the bottom of each child view
            height += mPaddingBottom;
            //我给RelativeLayout设置了高度mLayoutParams.height > 0
            if (mLayoutParams != null && mLayoutParams.height >= 0) {
                height = Math.max(height, mLayoutParams.height);
            }
            height = Math.max(height, getSuggestedMinimumHeight());
            height = resolveSize(height, heightMeasureSpec);
            ...
            setMeasuredDimension(width, height);
    }

RelativeLayout的高度模式为UNSPECIFIED,且没有子View,那么它的高度取决于它的布局参数heigth是否大于0。而MATCH_PARENT = -1,所以RelativeLayout的高度设置为MATCH_PARENT的时候它不显示,设置成一个常量它就显示。

ScrollView嵌套RelativeLayout冲突示例2:

》》MyRelativeLayout的布局高度还是match_parent,给MyRelativeLayout嵌套一个子View,子View的高度设置为MATCH_PARENT。子View和MyRelativeLayout都不显示。

输出日志:

MyRelativeLayout: onMeasure: UNSPECIFIED,测量前的H size = 0
MyView: onMeasure: UNSPECIFIED,测量前的H size = 0
MyView: 测量后的高度: 0
MyView: onMeasure: UNSPECIFIED,测量前的H size = 0
MyView: 测量后的高度: 0
MyRelativeLayout: 测量后的高度: 0

可见,UNSPECIFIED呈现了一定的传递性。

分析RelativeLayout的测量方法(有子View的情况)

请参照https://www.jianshu.com/p/8e3004494479

ScrollView嵌套View冲突示例:

》》MyRelativeLayout的布局高度还是match_parent,嵌套一个View(不是ViewGroup)。
View即使设置了高度,也不显示。

输出日志:

onMeasure: UNSPECIFIED,测量前的H size = 0
测量后的高度: 0
分类: view体系

0 条评论

发表回复

您的电子邮箱地址不会被公开。