记录一次子View超出父布局部分被裁减问题引起的measure再思考

背景

在项目中做到了一个需求,需要做一个类似于从底部滑出遮挡住的屏幕外的View出来,大概效果图如下:

下面的黄色View开始时是在底部固定位置,下半部分超出屏幕外不可见,随着滑动往上滑出,而且要考虑到不同的设备的屏幕高度问题,每个设备都要只显示到底部文字3这一块内容,那么意味着要在代码中动态设置margin。

提前声明

代码是demo,所以没有对滑动做多余的处理,只是有个简单的滑动效果,本文重点在于View布局相关。拿到手的时候本想就一个简单的ViewDragHelper辅助类做滑动处理就没有问题。结果在做的过程却发现了布局绘制相关的一些小细节问题。在这里记录一下,顺便分享出来也可以让各位大佬一起分析分析。

问题

先抛出问题效果:

显然发现滑动布局的开始超出屏幕外的部分不见了??

就这么...不见了?

这么...不见了?

...不见了?

见了?

说实话看了这个效果心里第一反应:

糟糕,是心肌梗塞的感觉。。。(╥╯^╰╥) (╥╯^╰╥)

仔细看了一下,滑出屏幕外的部分并没有绘制出来。 以前从未遇到过这种问题,解决后写下本文记录一下。 那接下来我们来正式进入正文来看看到底是哪里出了问题。


首先这是xml布局代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.coder.scrolldemo.Main2Activity">


    <LinearLayout
        android:id="@+id/mContentLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="vertical">
        <TextView
            android:id="@+id/text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/colorAccent"
            android:paddingLeft="5dp"
            android:text="这是文字"
            android:textColor="#202020"
            android:textSize="16sp"/>
    </LinearLayout>

    <com.example.coder.scrolldemo.ViewDragLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <!--下面就是我的滑动内容View,黄色背景的Layout-->
        <LinearLayout
            android:id="@+id/mScrollContentLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#ffff00"
            android:orientation="vertical">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:text="底部文字1"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:text="底部文字2"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:text="底部文字3"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:text="底部文字4"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:text="底部文字5"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:text="底部文字6"/>
        </LinearLayout>
    </com.example.coder.scrolldemo.ViewDragLayout>
</LinearLayout>
复制代码

布局很简单,主要是就是自定义了一个ViewDragLayout自定义布局来实现滑动的效果。


然后贴出ViewDragLayout的代码如下:

//这里通过了ViewDragHelper辅助类来实现的滑动
//不知道的童鞋可以自己自行去查阅如何实现,我们本文的重点也不在滑动而是布局异常
//简单来说就是滑动的辅助类,比我们自己手动去捕获事件坐标去做处理简单很多。
//安卓的侧边栏就是通过这个类去实现的
public class ViewDragLayout extends LinearLayout {
    /**
     * 要滑动的内容
     */
    private ViewGroup mScrollContentLayout;

    private ViewDragHelper mViewDragHelper;
    /**
     * 初始marginTop值
     */
    private int mInitMarginTop;

    public ViewDragLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    @Override
    protected void onFinishInflate() {
        mScrollContentLayout = (ViewGroup) getChildAt(0);
        mScrollContentLayout.setClickable(true);
        super.onFinishInflate();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //设置滑动布局的marginTop
        mInitMarginTop = (int) ((DimenUtil.getScreenHeight(getContext())) * 0.65);
        LayoutParams layoutParams = (LayoutParams) mScrollContentLayout.getLayoutParams();
        layoutParams.topMargin = mInitMarginTop;
    }

    @Override
    public void computeScroll() {
        if (mViewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    private void init() {
        mViewDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
    }

    class DragHelperCallback extends ViewDragHelper.Callback {

        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return mScrollContentLayout == child;
        }


        @Override
        public int getViewVerticalDragRange(View child) {
            return 1000;
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            if (child == mScrollContentLayout) {
                int newTop = top;
                return newTop;
            }
            return top;
        }


        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            if (releasedChild == mScrollContentLayout) {
                if (yvel > 0) {
                    close();
                } else {
                    open();
                }
            }
        }
    }

    public void open() {
        if (mViewDragHelper.smoothSlideViewTo(
                mScrollContentLayout, 0, 0)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    public void close() {
        if (mViewDragHelper.smoothSlideViewTo(mScrollContentLayout, 0, mInitMarginTop)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        try {
            mViewDragHelper.processTouchEvent(e);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return false;
    }
}
复制代码

可以看到我们并没有对布局做任何的处理,measure,layout,draw三个流程我们都没有去做任何处理,只是在measure之后,layout之前给滑动布局设置了一个动态的marginTop而已。 最开始产生怀疑可能是DragHelper类在滑动的过程中做了手脚,后来跟了源码发现DragHelper只是单纯的做了View的位置移动,并没有去做绘制相关的操作。

(ps:因为写了文章回头发现用滑动布局说明容易混乱,于是提前写明,根布局是最外层的LinearLayout,滑动布局指的是自定义的ViewDragLayout,滑动子布局指的是黄色区域的LinearLayout布局)

最开始为了更明显的看出是不是滑动布局的自己问题导致只绘制了这么一部分,我将margin设置为0,修改代码如下:

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //设置滑动布局的marginTop
         //修改前
        //mInitMarginTop = (int) ((DimenUtil.getScreenHeight(getContext())) * 0.65);
        //修改后
        mInitMarginTop = 0;

        LayoutParams layoutParams = (LayoutParams) mScrollContentLayout.getLayoutParams();
        layoutParams.topMargin = mInitMarginTop;
    }
复制代码

效果如下:

显然这是没有问题的,滑动内容全部展现了出来。这下我排除了是由于滑动布局自身绘制问题导致的显示不全的问题。那么接下来我不得不从滑动子布局,滑动布局和父布局之间的关系进行考虑。 回头再打开布局检查仔细看看效果:

这下我发现,黄色区域滑动内容的高度是明显被剪到了只有屏幕内的部分。原来只绘制一半就是因为滑动布局高度受到了挤压,导致滑动子布局也没有绘制完整。那么说明了滑动布局在measure流程的时候出现了问题。那么接下来继续排查问题所在。

因为我在java代码中并没有对任何View做特殊的改变宽高等操作。因此我又开始考虑是否是xml中滑动子布局,滑动布局和父布局之间的绘制流程出现了问题。而在xml代码中,能够影响到高度绘制的,无非就是layout_height这个属性。 看一下我们的布局代码:

1处是父布局,高度设置为match_parent

2处为滑动布局的兄弟布局,高度为自适应

3处是滑动布局自身的高度,为wrap_parent模式

4处是滑动子布局,里面包含几个高度都为50dp的TextView

首先2处兄弟Layout显然并不会去影响滑动Layout和父布局,所以排除问题出在它身上。


接下来试着将滑动布局的高度设置为固定值:

    <com.example.coder.scrolldemo.ViewDragLayout
        android:layout_width="match_parent"
        android:layout_height="600dp"
        android:orientation="vertical">
复制代码

或者在java代码中设置:

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // TODO Auto-generated method stub
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //根据需求中的效果来动态设置宽高,DimenUtil是自己的一个工具类
        int width =DimenUtil.getScreenWidth(getContext());
        int height =DimenUtil.getScreenHeight(getContext() * 0.7);
        setMeasuredDimension(width, height);
    }
复制代码

再来查看一下效果:

这下效果就成功出来了~~~~~

发现这样高度就完全出来了,为什么修改了固定高度就绘制出来了??我想这个时候有些小伙伴已经想到了问题了,会不会是measure的问题???

我试着将最外层的根布局的高度设置为一个固定值(这里为了展示设置成2000dp)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="2000dp"
    android:orientation="vertical">
     ~~~~~~~~省略代码
    <com.example.coder.scrolldemo.ViewDragLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
       ~~~~~~~~省略代码
    </com.example.coder.scrolldemo.ViewDragLayout>
</LinearLayout>
复制代码

来看一下效果:

效果还是成功的! 到这里可能有些小伙伴已经猜到了是什么问题~~~ 不急~~~我们再来验证一下,根布局2000dp不变,滑动布局高度改为match_parent:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="2000dp"
    android:orientation="vertical">
     ~~~~~~~~省略代码
    <com.example.coder.scrolldemo.ViewDragLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
       ~~~~~~~~省略代码
    </com.example.coder.scrolldemo.ViewDragLayout>
</LinearLayout>
复制代码

看看效果:

因为滑动布局改为match_parent并且滑动子布局也是match_parent,所以布局黄色区域占满了整个屏幕。

我们最后再改一下滑动子布局的高度,根布局和滑动布局改为最初样子,改为固定值1000dp:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    //省略
    <com.example.coder.scrolldemo.ViewDragLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <!--下面就是我的滑动内容View,黄色背景的Layout-->
        <LinearLayout
            android:id="@+id/mScrollContentLayout"
            android:layout_width="match_parent"
            android:layout_height="1000dp"
            android:background="#ffff00"
            android:orientation="vertical">
                //省略
        </LinearLayout>
    </com.example.coder.scrolldemo.ViewDragLayout>
</LinearLayout>
复制代码

效果:

还是一样的全部绘制了出来 总结一下效果: 我发现当我的根布局高度

  • 是match_parent或者warp_content,并且滑动布局和滑动子布局也是match_parent或者warp_content时候,滑动布局被裁剪了。
  • 是精确高度时候滑动布局和滑动子布局高度是三种模式之一都可以正常显示不会被裁剪
  • 是match_parent或者warp_content,但滑动布局或者滑动子布局其中一个是精确高度时,可以正常显示不会被裁剪

综合看一下,我发现只要其中一个布局是精确高度,就会正常显示。再回头看看错误效果,我猜想在错误场景是不是根布局的高度只有屏幕高度,因此滑动布局如果不是精确高度的话,超过父布局外的部分(视觉上就是超出屏幕的部分)就会被裁掉! 我们来打印日志来验证一下: 在ViewDragLayout的onLayout方法中打日志:

   @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        Log.d("sdafaff", "状态栏高度:   " + DimenUtil.getStatusBarHeight(getContext()));
        Log.d("sdafaff", "标题栏高度:   " + DimenUtil.getActionbarHeight(getContext()));
        Log.d("sdafaff", "根布局高度:   " + ((ViewGroup) getParent()).getHeight());
        Log.d("sdafaff", "滑动子布局高度:   " + mScrollContentLayout.getHeight());
        Log.d("sdafaff", "滑动子布局top坐标:   " + mScrollContentLayout.getTop());
        Log.d("sdafaff", "根布局bottom:   " + ((ViewGroup) getParent()).getBottom());
        Log.d("sdafaff", "滑动布局bottom坐标:   " + getBottom());
        Log.d("sdafaff", "滑动子布局bottom坐标:   " + mScrollContentLayout.getBottom());
        Log.d("sdafaff", "屏幕高度:   " + DimenUtil.getScreenHeight(getContext()));
    }
复制代码

结果:

com.example.coder.scrolldemo D/sdafaff: 状态栏高度:   63
com.example.coder.scrolldemo D/sdafaff: 标题栏高度:   147
com.example.coder.scrolldemo D/sdafaff: 根布局高度:   1584
com.example.coder.scrolldemo D/sdafaff: 滑动子布局高度:   361
com.example.coder.scrolldemo D/sdafaff: 滑动子布局top坐标:   1166
com.example.coder.scrolldemo D/sdafaff: 根布局bottom:   1584
com.example.coder.scrolldemo D/sdafaff: 滑动布局bottom坐标:   1584
com.example.coder.scrolldemo D/sdafaff: 滑动子布局bottom坐标:   1527
com.example.coder.scrolldemo D/sdafaff: 屏幕高度:   1794
复制代码

因为根布局位于标题栏和状态下面,所以打印出的前三者相加等于屏幕高度 可以看到,根布局的高度是只有屏幕高度的,而我们的滑动布局也因此被限制在屏幕内,也因此我们的滑动子布局高度受到了裁剪。 至此小伙伴们应该也想到是什么问题导致的了吧?精确高度两个字让我联想到了View的测量模式,事实正是测量模式导致了这个结果!

先说出我的解决方案就是既然高度被裁剪不能超过父布局:

  • 通过各种手段将超出屏幕的滑动子布局的测量模式变为EXACTLY模式即可(比如给滑动子布局一个固定高度)。
  • 将滑动子布局的根布局ViewDragLayout设置为足够大的宽高,使得保证足够让ViewDragLayout能够完整的显示所有内容(这样子布局宽高无需设置固定宽高)

到此手拿党到此就不用再往下看了,接下来本着程序猿学习的精神,我又仔细查看源码,来探索一下为什么裁剪的根源(其实就是再bb一遍View绘制过程中的measure流程)

呵这个时候你肯定心里想算了懒得看了反正都是measure流程再bb一遍 不你错了,我之前已经有了一篇分析绘制流程的代码,所以我也不想再bb了···· 因为之前的分析都是针对View和ViewGroup的measure流程分析,并没有做具体的分析,这里我会简单贴出LinearLayout和RelativeLayout的measure流程来分析一下出现上述问题的原因。

一条华丽的分割线


LinearLayout

LinearLayout的onMeasure()方法:

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

在measureVertical()方法中看到有一个 measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight)方法,看一下代码:

  void measureChildBeforeLayout(View child, int childIndex,
            int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
            int totalHeight) {
        measureChildWithMargins(child, widthMeasureSpec, totalWidth,
                heightMeasureSpec, totalHeight);
    }
复制代码

调用了measureChildWithMargins方法,值得注意的是,LinearLayout并没有重写measureChildWithMargins方法,因此这里的代码就是ViewGroup类中源码。 跟进去:

    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是通过自身的LayoutParams和父类的MeasureSpec传入getChildMeasureSpec方法最终来决定自身的,然后进行子view的measure流程。getChildMeasureSpec方法也是ViewGroup类中的方法,并没有重写。 继续跟进:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //获取父布局的spec和Size
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        //设置一个size为父类的大小减去padding值
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;
        //根据父类的布局模式来限定子View
        switch (specMode) {
        // 如果父布局是精确模式
        case MeasureSpec.EXACTLY:
            //如果子View设置了自身精准大小,那么子View的spec为自身大小加精确模式
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //如果子View没有设置了自身大小,那么子View的spec为父布局剩余大小加精确模式
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            //如果子View没有设置了自身大小,那么子View的spec为父布局剩余大小加AT_MOST模式
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 如果父布局是AT_MOST模式
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
复制代码

基本的注释都在代码中注明,上面的代码网上讲解measure流程的文章都会贴出,然后汇总出一张经典图:

图片取自《Android开发艺术探索》

所以在代码中,我们的根布局是AT_MOST模式,因此我们的滑动布局因此会变成MeasureSpec会得到AT_MOST+parentSize。 而当这些工作做完后,LinearLayout的onMeasure()中还会调用resolveSizeAndState()方法来最终确定自身高度:

  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);
    }
复制代码

我们看到此处传入了一个size,这个size实际上就是LinearLayout测量的最大高度,但是在AT_MOST模式下时,是不能超出父布局的,因此高度被设置为specSize,即上面Spec中的parentSize。所以最终正是这两处设置高度的代码,导致了我们的滑动布局不会完全被绘制导致。而我们可以看到,当我们的模式是EXACTLY模式时,我们的高度是直接被设置成我们的正确高度的,所以解决方案也是基于此,把滑动子布局设置为指定高度即可。或者将滑动布局设置为足够高,滑动子布局也能显示。


按理说到这里就应该结束,但是在后面的重构代码过程中,因为一些原因 我的自定义滑动布局改为继承自RelativeLayout,诡异的事情出现了。。。。 修改之前正确效果xml的代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/test_layout"
    android:orientation="vertical">
    <LinearLayout
        android:id="@+id/mContentLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="vertical">
        //省略
    </LinearLayout>

    <com.example.coder.scrolldemo.ViewDragLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <!--下面就是我的滑动内容View,黄色背景的Layout-->
        <LinearLayout
            android:id="@+id/mScrollContentLayout"
            android:layout_width="match_parent"
            android:layout_height="300dp"
            android:background="#ffff00"
            android:orientation="vertical">
          //省略
        </LinearLayout>
    </com.example.coder.scrolldemo.ViewDragLayout>
</LinearLayout>
复制代码

效果:

现在就把ViewDragLayout继承自RelativeLayout,其他xml代码不变,重新运行:

卧槽,居然又没了?这又是什么鬼?其实还是一样的原理,还是是测量的问题。还记得上面介绍LinearLayout的measureChildWithMargins方法时说的一句话吗? LinearLayout并没有重写measureChildWithMargins方法。所以测量子View的Spec就是上面分析的流程,但是RelativeLayout却重写了这个方法,意味着子View的Spec生成规则并不是上面的分析一样。 跟进去:

 //mySize指的是RelativeLayout自身的宽高值
 private int getChildMeasureSpec(int childStart, int childEnd,
            int childSize, int startMargin, int endMargin, int startPadding,
            int endPadding, int mySize) {
        int childSpecMode = 0;
        int childSpecSize = 0;

        // 如果RelativeLayout的大小为负的,那么表示 RelativeLayout的大小没有被指定 
        final boolean isUnspecified = mySize < 0;
        if (isUnspecified && !mAllowBrokenMeasureSpecs) {
            if (childStart != VALUE_NOT_SET && childEnd != VALUE_NOT_SET) {
                // Constraints fixed both edges, so child has an exact size.
                childSpecSize = Math.max(0, childEnd - childStart);
                childSpecMode = MeasureSpec.EXACTLY;
            } else if (childSize >= 0) {
                // The child specified an exact size.
                childSpecSize = childSize;
                childSpecMode = MeasureSpec.EXACTLY;
            } else {
                // Allow the child to be whatever size it wants.
                childSpecSize = 0;
                childSpecMode = MeasureSpec.UNSPECIFIED;
            }

            return MeasureSpec.makeMeasureSpec(childSpecSize, childSpecMode);
        }

        // Figure out start and end bounds.
        int tempStart = childStart;
        int tempEnd = childEnd;

        //如果左右(上下)没有依赖view,也就是说之前没有给lp赋值mLeft或mRight(mTop或mBottom)的话  
        //把上下边(左右边)先赋值(RelativeLayout的宽度(高度)去掉padding和margin)  
        if (tempStart == VALUE_NOT_SET) {
            tempStart = startPadding + startMargin;
        }
        if (tempEnd == VALUE_NOT_SET) {
            tempEnd = mySize - endPadding - endMargin;
        }

        //设置一个maxAvailable变量表明最多可用的宽度(高度)
        final int maxAvailable = tempEnd - tempStart;

        if (childStart != VALUE_NOT_SET && childEnd != VALUE_NOT_SET) {
            // Constraints fixed both edges, so child must be an exact size.
            childSpecMode = isUnspecified ? MeasureSpec.UNSPECIFIED : MeasureSpec.EXACTLY;
            childSpecSize = Math.max(0, maxAvailable);
        } else {
            //如果子View设置了自己的精确宽度(高度)
            if (childSize >= 0) {
*******************************************************************
                子View的Spec模式设置为精确模式
*******************************************************************
                childSpecMode = MeasureSpec.EXACTLY;
                //如果RelativeLayout现在可用的高度(宽度)大于0
                if (maxAvailable >= 0) {
                // 设置子View的Spec的大小为自身需要的高度(宽度)和RelativeLayout可用值之间的最小值
                    childSpecSize = Math.min(maxAvailable, childSize);
                } else {
                    //如果RelativeLayout已经没有可用空间了,那么直接让子View的宽度(高度)为自己需要的值
                    childSpecSize = childSize;
                }
            } else if (childSize == LayoutParams.MATCH_PARENT) {
                  //子View的模式为MATCH_PARENT,那么就把RelativeLayout剩下的所有空间都给它
                childSpecMode = isUnspecified ? MeasureSpec.UNSPECIFIED : MeasureSpec.EXACTLY;
                childSpecSize = Math.max(0, maxAvailable);
            } else if (childSize == LayoutParams.WRAP_CONTENT) {
               //子View的模式为WRAP_CONTENT
                if (maxAvailable >= 0) {
                  //如果RelativeLayout已经还有可用空间了,那么先暂时给子View剩余的所有空间,让子View可能尽可能的大,子View模式设置为AT_MOST
                    childSpecMode = MeasureSpec.AT_MOST;
                    childSpecSize = maxAvailable;
                } else {
                    //如果RelativeLayout已经没有可用空间了,那么就设置子View模式为UNSPECIFIED模式,表示我不给你子View限制大小了,你子View想多大就多大
                    // We can grow in this dimension. Child can be as big as it
                    // wants.
                    childSpecMode = MeasureSpec.UNSPECIFIED;
                    childSpecSize = 0;
                }
            }
        }
        return MeasureSpec.makeMeasureSpec(childSpecSize, childSpecMode);
    }
复制代码

因此通过上面的源码分析,我们可以看出,之所以效果出现偏差的原因,就在于上面的两行星号之间的代码,我们的滑动子布局虽然设置了400dp的高度,但是由于滑动布局自身可用空间只有不超过屏幕的部分,因此滑动子布局被强行裁剪掉啦~~~~

又是一条贼xx华丽的分割线


至此本文终于结束了。。。

最后再啰嗦一句,总结起来说就是虽然网上有很多measure相关的文章分析,但是很多大都是针对ViewGroup的父类的基础measure流程分析,而实际上很多Layout都会进行重写,做一些自己特殊的处理,所以我们也不能一概而论的认为什么都是这样,就像RelativeLayout,ScrollView(这里就不分析了,再分析要死了)这些,就是很常用的并且对子View的measure做了特殊处理的控件。因此我们还是需要去了解他们的特性,这样才能在一些问题下正确的去处理问题啦~~~~~

转载于:https://juejin.im/post/5b2f6c73e51d4558a21f8dd8

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值