源码源码,你不要睡着啦。

都知道源码是好东西,一头扎进去往往容易淹死,楼主扔几个救生圈,剩下的看你们造化了。


问题:
1.为什么默认自定义View经常占满全屏(无论是设置wrap_content还是match_parent)?
2.为什么说view的最终测量尺寸是由view本身和其父容器共同决定,怎么决定的?
3.自定义View的状态保存。以及代码中new一个view 他的onSaveStateInstance会被调用么?
4.如何解决ScrollView嵌套中一个ListView的显示问题及滑动冲突?


1.写一个自定义View放入如下布局

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.test.fahionaly.customview2.MainActivity">

    <com.test.fahionaly.customview2.ImgView2
        android:id="@+id/id_imgView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="20dp"
        >
    </com.test.fahionaly.customview2.ImgView2>
    <Button
        android:id="@+id/id_group"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Fuck"
        android:background="#991212"/>
</LinearLayout>

效果图:
这里写图片描述

图片大小是完全不可能占满屏幕的,但是你会发现没有看到下面的Button,你试试改成match_parent一样。为啥子?看源码!

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

跟进去

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

你会发现只有确定他传入的两个参数才能确定自定义View的默认大小
那参数来自哪里?这就要追溯到View的根布局也就是Linearlayout的父布局DecorView。
这里写图片描述
我们的widthMeasureSpec和heightMeasureSpec应该从这里或者更上一层PhoneWindow传递进来,而PhoneWindow里面是有ViewRootImpl来具体实现果然我们在performTraversals中发现了痕迹

 WindowManager.LayoutParams lp = mWindowAttributes;
 ----------
         if (!mStopped || mReportNextDraw) {
                boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                        (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

可以看到在performTraversals方法中通过getRootMeasureSpec获取原始的测量规格并将其作为参数传递给performMeasure方法处理。其中lp.width和lp.height均为MATCH_PARENT,其在mWindowAttributes(WindowManager.LayoutParams类型)将值赋予给lp时就已被确定。

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

然后把值带入最开始我们要探索的getDefaultSize方法你会发现无论设置wrap_content还是match_parent都是windowsize
那如何改才能露出我们的Button?
我的思路:重写onMeasure重点说一下wrap_content首先应该测试图片的大小,拿图片的宽高跟windowSize对比取小,毕竟要听爹爹的话。match_parent当然就是windowSize啦
下面上代码:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 声明一个临时变量来存储计算出的测量值
        int resultWidth = 0;

        // 获取宽度测量规格中的mode
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);

        // 获取宽度测量规格中的size
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);

    /*
     * 如果爹心里有数
     */
        if (modeWidth == MeasureSpec.EXACTLY) {
            // 那么儿子也不要让爹难做就取爹给的大小吧
            resultWidth = sizeWidth;
        }
    /*
     * 如果爹心里没数
     */
        else {
            // 那么儿子可要自己看看自己需要多大了
            resultWidth = mBitmap.getWidth()-getPaddingLeft();

        /*
         * 如果爹给儿子的是一个限制值
         */
            if (modeWidth == MeasureSpec.AT_MOST) {
                // 那么儿子自己的需求就要跟爹的限制比比看谁小要谁
                resultWidth = Math.min(resultWidth, sizeWidth);
            }
        }

        int resultHeight = 0;
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);

        if (modeHeight == MeasureSpec.EXACTLY) {
            resultHeight = sizeHeight;
        } else {
            resultHeight = mBitmap.getHeight()+getPaddingTop()+getPaddingBottom();
            if (modeHeight == MeasureSpec.AT_MOST) {
                resultHeight = Math.min(resultHeight, sizeHeight);
            }
        }

        // 设置测量尺寸
        setMeasuredDimension(resultWidth, resultHeight);
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

下面来第二个问题:

2.为什么说view的最终测量尺寸是由view本身和其父容器共同决定,怎么决定的?

具体看一个方法-getChildMeasureSpec下面方法的主要作用是根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec(也就是说子元素的SPEC是由view本身和其父容器共同决定[原来理解为View自身layout决定 这个是错误的] )

 public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

看上面太繁琐?那么看主席书上的一个表格:
这里写图片描述

最后第三个问题

3.自定义View的状态保存。以及代码中new一个view 他的onSaveStateInstance会被调用么?

我们先看一下Activity如何保存状态如下图
这里写图片描述

没错就是使用onSaveInstanceState,当其他App切入前台时是由onSaveInstanceState来保存状态。
但是如果用户按下Back键显示关闭Activity是不会调用。onSaveInstanceState的。

那new出的view会执行onSaveInstanceState么?
楼主最初尝试是不会的于是乎查看了源码:

    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
        if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
            mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
            Parcelable state = onSaveInstanceState();
            if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
                throw new IllegalStateException(
                        "Derived class did not call super.onSaveInstanceState()");
            }
            if (state != null) {
                // Log.i("View", "Freezing #" + Integer.toHexString(mID)
                // + ": " + state);
                container.put(mID, state);
            }
        }
    }

发现mID 跟SAVE_DISABLED_MASK必须要符合条件才行
那么在代码中如何做到?

.setID();
.setSaveEnable(true);

4.如何解决ScrollView嵌套中一个ListView的显示问题及滑动冲突?

测试发现ScrollView布局中嵌套Listview显示是不正常的,确切地说是只会显示ListView的第一个项。
要想解决这个问题我们依然需要看一下源码:
ScrollView.java的measureChildWithMargins()代码片段:

 final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

可以看出child的MeasureSpec是UNSPECIFIED
这时我们看一下ListView的onMeasure源码片段:

  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();
            if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                    getVerticalFadingEdgeLength() * 2;
        }

可以看到如果MeasureSpec是UNSPECIFIED高度只会拿第一个childHeight和一些padding值,所以我们确定是由于ScrollView的measureChildWithMargins导致ListView的Spec发生了变化。
那我们该如何做?
我的思路是重写ListView的onMeasure,改变MeasureSpec。
改成那种呢?再看一段源码:

   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);

        mWidthMeasureSpec = widthMeasureSpec;
    }

measureHeightOfChildren这个是测量所有的子类,那就这个吧
我们改成At_Most,那我们势必要用到MeasureSpec.makeMeasureSpec这个方法这个方法两个参数
* @param size the size of the measure specification
* @param mode the mode of the measure specification
* @return the measure specification based on size and mode
由于MeasureSpec是Mode和Size组合而成,前2位是mode,后两位是size所以我们可以使用移位来实现,当然最后不能忘了设置。
最后代码如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSepc = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSepc);
    }

这样显示问题解决了,改撸一下嵌套滑动问题啦。

这又倒了老生常谈的事件传递机制

这里写图片描述

看完了流程我们开始撸源码,扒开ScrollView的onTouchEvent内衣

 @Override
    public boolean onTouchEvent(MotionEvent ev) {
    `````省略无数代码
        return true;
    }

发现ACTION_MOVE时都是返回true即被消费掉 子布局如何得到垂帘呢?
解决方案:
两种思路~
1.外部拦截-即重写父布局onInterceptTouchEvent 当需要拦截的时候retrun true,相反就返回 false;
2.内部拦截-重写子布局onInterceptTouchEvent 内部不让父布局拦截
有点意思——子布局 ACTION_MOVE时调用getParent().requestDisallowInterceptTouchEvent(true) ACTION_UP/CANEL时getParent().requestDisallowInterceptTouchEvent(false)
大功告成。

综上几个例子是程序开发中经常让初学者摸不到头脑的问题,遇到问题如何冷静的分析问题至关重要,尤其是当stackoverflow上都没有的答案就需要媛猿们从源码中找答案了。除了复制粘贴程序员更应该做的是多问一个为什么,多点进去看看,多写一篇总结。不要让错误一遍又一遍的产生,能持之以恒的做到才是真的进步。

猜你喜欢

转载自blog.csdn.net/lvwenbo0107/article/details/60139620