为什么HorizontalScrollView嵌套一个子View,他的match_parent属性会失效,浅析View的测量工作

为什么HorizontalScrollView嵌套一个子View,他的match_parent属性会失效

这个问题的出现是在写一个需求时候,示例图如下
这里写图片描述
demo链接 https://github.com/nbwzlyd/TabLayoutDemo
当然这个问题的解决方案很简单,没什么难度。下面记录一下原因。

  1. 问题的抛出
    我们在开发的过程中,经常会用到HorizontalScrollView,通常情况下我们还需要在里面嵌套一个ViewGroup,并将其设置为match_parent,但是这个属性一定是无效的。我们需要重新设置
    fillViewport属性为true。这样才会得到我们要的效果。

示例图:
fillViewport 为false的情况下:
这里写图片描述

fillViewport 为true的情况下:
这里写图片描述

一个简单的属性就可以让我们需要的效果起死回生。这个问题的解决不难,和ScrollView的解决方案完全一致。
秉承知其然并知其所以然的态度,我们来看看为什么这个属性设置了会得到我们的效果,但是我们今天逆向处理,我们来看看为什么默认情况下,我们得不到我们想要的效果。

解决思路

1.查看源码,看onMeasure中是不是特殊处理了。简单粗暴
我们假设你知道view的大概测量流程,测量方法在onMeasure方法中,所以我们直捣黄龙,看源码中onMeasure方法中的处理:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if (!mFillViewport) {
            return;
        }

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        if (widthMode == MeasureSpec.UNSPECIFIED) {
            return;
        }

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

            int desiredWidth = getMeasuredWidth() - widthPadding;
            if (child.getMeasuredWidth() < desiredWidth) {
                final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                        desiredWidth, MeasureSpec.EXACTLY);
                final int childHeightMeasureSpec = getChildMeasureSpec(
                        heightMeasureSpec, heightPadding, lp.height);
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

代码很简单,在fillViewport为false的情况下,onMeasure方法没有对view的测量操作做任何的干预操作,直接调用的super.onMeasure();

难道是因为父View的测量工作并没有测量出子View的准确需要大小?我们看一眼父View。

public class HorizontalScrollView extends FrameLayout {......}

父View是FrameLayout。

emmm,不可能是父View测量工作出了问题。因为我们经常用这个控件,假设是父View的测量工作出错,虽然能解释通HorizontalScrollView导致子view的match_parent失效问题,但是这样的话我们用frameLayout控件就需要每次设置xx属性了,但是我们并没有这么设置过。(事实上这个时候我又把FrameLayout的onMeasure方法又看了一遍,冏,还debug了好几遍)。

  1. 思路2 widthMeasureSpec ,heightMeasureSpec
    咨询了群里的大牛,有赞大佬馍哥。给了一个思路,看一下父View的widthMeasureSpec ,heightMeasureSpec 包括mode,包括size。这个思路我一开始没想到。我们知道,子view的大小以及测量模式是受自己父View大小影响的。很有可能是传给我们的这两个属性在HSV 和FL中不同,于是按照这个思路,我们来看看。

定义一个类,继承FL

public class CustomLayout extends FrameLayout {
    private static final String TAG="CustomLayout";
    public CustomLayout(Context context) {super(context);}

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

    public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.d(TAG, "onMeasure: "+(MeasureSpec.getMode(widthMeasureSpec)==MeasureSpec.EXACTLY)

        +"    size=="+MeasureSpec.getSize(widthMeasureSpec));
    }
}

布局文件

<?xml version="1.0" encoding="utf-8"?>
<wiget.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:orientation="horizontal">

        <TextView
            android:layout_width="0dp"
            android:layout_height="40dp"
            android:layout_weight="1"
            android:background="@color/colorAccent"
            android:gravity="center"
            android:text="1"
            android:textColor="@android:color/white" />

        <TextView
            android:layout_width="0dp"
            android:layout_height="40dp"
            android:layout_weight="1"
            android:background="@color/colorPrimaryDark"
            android:gravity="center"
            android:text="2"
            android:textColor="@android:color/white" />
    </LinearLayout>

</wiget.CustomLayout>

运行,打印日志:
这里写图片描述

mode 是EXACTLY size 是720 ,我用的是720p的模拟器。

将继承的FL换成HSV,打印日志,很不幸,打印日志完全相同。

这也就说明,FL和HSV的measurespc是完全一致的,所以这个思路就死胡同了,不是这个原因引起的

其实仔细想一下,HSV是继承自FL的,本是同根生,measureSpec怎么会不一致呢,不过这个思路是很赞的,这篇文章也是记录一下解决问题的思路。

3.回归本质,我们继续在HSV中探索

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

        final int horizontalPadding = mPaddingLeft + mPaddingRight;
        final int childWidthMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - horizontalPadding),
                MeasureSpec.UNSPECIFIED);

        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    @Override
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
        final int usedTotal = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin +
                widthUsed;
        final int childWidthMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - usedTotal),
                MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

我们注意到,在HSV中重写了这两个方法,这个方法顾名思义,就是测量子view的大小的,所以,99%的问题就出现在这里了。
我们注意到,在measureChildWithMargins()中,有一段这样的代码

 final int childWidthMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - usedTotal),
                MeasureSpec.UNSPECIFIED);

代码中将子View的measureSpec的mode强制设置为UNSPECIFIED,这就说明我们无论怎么设置view的属性,在HorizontalScrollView中都会失效。设置100dp或者是match_parent.

既然原因找到了,我们就顺藤摸瓜,继续寻找。

这段代码的最后我们会调用

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

我们打开这段代码看:
嗯,基本看不懂。只记住他最后又会调用后onMeasure即可…

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }

        // Suppress sign extension for the low bytes
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

        // Optimize layout by avoiding an extra EXACTLY pass when the view is
        // already measured as the correct size. In API 23 and below, this
        // extra pass is required to make LinearLayout re-distribute weight.
        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                || heightMeasureSpec != mOldHeightMeasureSpec;
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                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;
            }

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;

        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }

由于我们的子View是textView,所以view的测量工作就交给了TextView的onMeasure函数,至于里面的测量,我们就不看了,大致得到的size就是text的size。

分析一下这段函数,对我们理解更有帮助:

 if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;

View的measure方法,里面调用的onMeasure方法,view的onMeasure如下:

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

代码很简单,直接调用了:setMeasuredDimension();
看一下getDefaultSize()函数:
这里写图片描述

再看一下 getSuggestedMinimumWidth()函数
这里写图片描述

可以很清晰的看出来,当specMode为UNSPECIFIED时View的大小以传入的为准,那么传入值是多少呢?就是getSuggestedMinimumWidth(),这个值,当有背景时,以背景大小为准,否则就是以minWidth为准。

由于我们的TextView复写了OnMeasure方法,不适用这套,至于TextView的测量方式,看里面代码即可,就不贴了。

花絮

知道了原因,我们就有解决的方案,1就是设置fillViewport 为true。
2呢,就是重写measureChildWithMargins方法啦,但是一般我们不会这么做,这里只是一种思路罢了,代码如下:

@Override
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
  	...........
  	  final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                    Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - usedTotal),
                    MeasureSpec.EXACTLY);
  
    }

将UNSPECIFIED 改为 EXACTLY即可。
知道这些,我们可以解决很多scrollView嵌套导致显示不全的问题了。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
当一个View超出屏幕时,你可以使用ScrollView或者HorizontalScrollView来解决这个问题。 如果你想拖动一个ImageView,并且希望它不超出屏幕,你可以使用以下代码: ```java imageView.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { // 手指按下时记录位置 lastX = event.getRawX(); lastY = event.getRawY(); break; } case MotionEvent.ACTION_MOVE: { // 移动的距离 float dx = event.getRawX() - lastX; float dy = event.getRawY() - lastY; // 获取imageView的参数 RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) v.getLayoutParams(); // 移动imageView lp.leftMargin += dx; lp.topMargin += dy; // 确定imageView超出屏幕 int screenWidth = getResources().getDisplayMetrics().widthPixels; int screenHeight = getResources().getDisplayMetrics().heightPixels; if (lp.leftMargin < 0) { lp.leftMargin = 0; } else if (lp.leftMargin + v.getWidth() > screenWidth) { lp.leftMargin = screenWidth - v.getWidth(); } if (lp.topMargin < 0) { lp.topMargin = 0; } else if (lp.topMargin + v.getHeight() > screenHeight) { lp.topMargin = screenHeight - v.getHeight(); } // 重新设置imageView的参数 v.setLayoutParams(lp); // 更新位置记录 lastX = event.getRawX(); lastY = event.getRawY(); break; } case MotionEvent.ACTION_UP: { // 手指抬起时不需要做任何事情 break; } } return true; } }); ``` 这个代码片段允许你拖动ImageView,但是它不被拖动超出屏幕。如果ImageView被拖动到了屏幕边缘,它停在那里并且无法继续拖动。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值