Android小疑问解答:为什么ScrollView嵌套ListView高度不正确

Android小疑问解答:为什么ScrollView嵌套ListView高度不正确

  • 前言
  • 代码
  • 布局的测绘过程
  • 总结

前言

最近为了研究滑动冲突,所以就将ScrollView内部放了ListView。ListView高度设置为750dp。
结果一运行,什么贵,为什么我的listview高度就剩这么点了?说好的750dp呢?这糊鬼呢?

这是ScrollView的原因?但是ScrollView内部放其他控件,也没有这问题啊?
那这是ListView的原因?但是ListView放在LinearLayout等viewgroup内部,也没有这样 的问题啊?

所以,我带着这样的疑问看了一下ScrollView和ListView的测量源码。终于搞懂了为啥。

代码

首先老规矩,先上源码
布局



    
    

代码

public class ViewInterceptTestActivity extends AppCompatActivity {
    private static final String TAG = "ViewInterceptTestActivi";
    private ListView listView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_view_intercept_test);

        listView= (ListView) findViewById(R.id.list_intercept_test);

        ArrayList list=new ArrayList();
        for (int i=0;i<99;i++){
            list.add("i=="+i);
        }
        ArrayAdapter arrayAdapter=new ArrayAdapter(this,android.R.layout.simple_list_item_1,list);
        listView.setAdapter(arrayAdapter);

        listView.post(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "run: listviewHeight"+listView.getMeasuredHeight()); // 打印ListView的测量高度
                Log.d(TAG, "run: listviewFatherHeight"+((ViewGroup)listView.getParent()).getMeasuredHeight()); //打印ScrollView高度
            
            }
        });

布局的测绘过程

如果想看完整的测绘原理和过程,可以查看我之前写的view工作原理的笔记。这里只从ScrollView的onMeasure方法开始看
ScrollView

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    	//由于ScrollView是FrameLayout的子类,所以这里调用了ViewGroup的onMeasure
        super.onMeasure(widthMeasureSpec, heightMeasureSpec); 
        //这里代码,先暂时不看一会儿再看
    }

FrameLayout

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
		//....
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
            	//这里ScrollView 重写了measureChildWithMargins方法。
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                //.....
            }
        }
        //....
    }

ScrollView

    @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);
          //这里需要大家注意一点很特殊的地方,就是childHeightMeasureSpec 最后构建的时候传递的是UNSPECIFIED类型
          //这就是和父类measureChildWithMargins方法不一样的地方
        final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                MeasureSpec.getSize(parentHeightMeasureSpec), MeasureSpec.UNSPECIFIED);
		
		//调用了子元素的measure,也就是我们这里ListView的measure。
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

这里的measure方法就是View中的measure方法,所以根据我们已有知道,知道最后的measure处理,还是在onMeasure中,所以这里我们直接看onMeasure方法。

    @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();
        //还记得,我们上面ScrollView给ListView传递的mode是UNSPECIFIED了吗?
        //现在就走到这个方法里面了
        if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
                || heightMode == MeasureSpec.UNSPECIFIED)) {
               //取出Listview第一个元素的高度
            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();
            childState = combineMeasuredStates(childState, child.getMeasuredState());

            if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
                    ((LayoutParams) child.getLayoutParams()).viewType)) {
                mRecycler.addScrapView(child, 0);
            }
        }

        if (widthMode == MeasureSpec.UNSPECIFIED) {
            widthSize = mListPadding.left + mListPadding.right + childWidth +
                    getVerticalScrollbarWidth();
        } else {
            widthSize |= (childState & MEASURED_STATE_MASK);
        }

        if (heightMode == MeasureSpec.UNSPECIFIED) {
        	//如果高度的mode为UNSPECIFIED,那么listview 的高度就为第一个childHeight的高度加上padding等。
            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);

        mWidthMeasureSpec = widthMeasureSpec;
    }

总结

所以,ScrollView 嵌套listview导致listview高度不正常的原因就是水落石出了。就是由于ScrollView将子元素的高度测量模式都更改为UNSPECIFIED。而Listview中,如果测量模式为UNSPECIFIED,则listview的高度直接采用第一个子元素的高度,

你可能感兴趣的:(android)