ScrollView 嵌套ListView、ExpandableListView等列表

很多时候在需求上不仅仅是ListView等列表单独滑动,而是在ListView等列表上加上其他的试图,整个试图整体一起滑动,这个时候就会用到ScrollView嵌套ListView等列表试图一起使用。 如下图所示:
这里写图片描述
但ScrollView嵌套Listview等列表试图比我们预想的实现起来要困难。会有以下的一些问题
1、ScrollView 和 ListView 滑动冲突
2、 Listview等列表高度显示不完全。


1.先说说List列表显示不完全的问题
注意:首先ScrollView只能包含一个根布局,所有一般吧ListView和其他试图统一包含到一个LinearLayout等布局下。

解决这个问题的方案有多种,看到网上有一篇文章把几种方法做了总结,以下内容基本上是转载自
minimicall 的 https://blog.csdn.net/MiniMicall/article/details/40983331那篇文章,借鉴学习。

1 通过代码计算设置ListView高度

/**
 1动态设置ListView的高度
 2@param listView
*/
public static void setListViewHeightBasedOnChildren(ListView listView) { 
    if(listView == null) return;

    ListAdapter listAdapter = listView.getAdapter(); 
    if (listAdapter == null) { 
        // pre-condition 
        return; 
    } 

    int totalHeight = 0; 
    for (int i = 0; i < listAdapter.getCount(); i++) { 
        View listItem = listAdapter.getView(i, null, listView); 
        listItem.measure(0, 0); 
        totalHeight += listItem.getMeasuredHeight();  //计算所有的item的高度
    } 

    ViewGroup.LayoutParams params = listView.getLayoutParams(); 
    params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1)); 
    listView.setLayoutParams(params); 
}

上面这个方法就是设定ListView的高度了,在为ListView设置了Adapter之后使用,就可以解决问题了。
但是这个方法有个两个细节需要注意

  • 一是Adapter中getView方法返回的View的必须由LinearLayout组成,因为只有LinearLayout才有measure()方法,如果使用其他的布局如RelativeLayout,在调用listItem.measure(0,0);时就会抛异常,因为除LinearLayout外的其他布局的这个方法就是直接抛异常的。

  • 二是需要手动把ScrollView滚动至最顶端,因为使用这个方法的话,默认在ScrollView顶端的项是ListView

2.使用LinearLayout取代ListView
既然ListView不能适应ScrollView,那就换一个可以适应ScrollView的控件,而LinearLayout是最好的选择,我们只需要自定义一个类继承自LinearLayout,为其加上对BaseAdapter的适配。
自定义的LinearLayout: LinearLayoutBindScorllView

public class LinearLayoutBindScorllView  extends LinearLayout {

    private BaseAdapter adapter;

    public LinearLayoutBindScorllView(Context context) {
        super(context);
    }

    public LinearLayoutBindScorllView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public LinearLayoutBindScorllView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void setAdapter(BaseAdapter adapter) {
        this.adapter = adapter;
        bindLinearLayout();
    }

    @Override
    public void setOnClickListener(OnClickListener onClickListener) {
        this.onClickListener = onClickListener;
    }

    /**
     * 绑定布局
     */
    public void bindLinearLayout() {
        if(adapter == null){
            return;
        }
        int count = adapter.getCount();
        this.removeAllViews();
        for (int i = 0; i < count; i++) {
            View v = adapter.getView(i, null, null);
            v.setOnClickListener(this.onClickListener);
            addView(v, i);
        }
        Log.v("countTAG", "" + count);
    }
}

使用: MyAdapter是继承自BaseAdapter的一个适配器,在getView()的方法中填入想要的视图,这个和普通的适配器一样编码。我用的方式如下代码

LinearLayoutBindScorllView linearLayout = (LinearLayoutBindScorllView) findViewById(R.id.act_solution_3_mylinearlayout);
        linearLayout.setAdapter(new MyAdapter(this));
public View getView(int position, View convertView, ViewGroup parent) {
            //列表顶部视图
    if(position == 0){
       convertView = inflater.inflate(R.layout.item_top, null);
        return convertView;
    }
            //列表底部视图
    else if(position == 21){
        convertView = inflater.inflate(R.layout.item_bottom, null);
        return convertView;
    }

            //普通列表项
    ViewHolder h = null;
    if(convertView == null || convertView.getTag() == null){
        convertView = inflater.inflate(R.layout.item_listview_data, null);
        h = new ViewHolder();
        h.tv = (TextView) convertView.findViewById(R.id.item_listview_data_tv);
        convertView.setTag(h);
    }else{
        h = (ViewHolder) convertView.getTag();
    }

    h.tv.setText("第"+ position + "条数据");

    return convertView;
}

上面这样就可以通过自定义LinearLayout·实现这个ListView 并添加顶部、底部等区域的视图了。

3.自定义可适应ScrollView的ListView
自定义一个类继承自ListView,通过重写其onMeasure方法,达到对ScrollView适配的效果。
下面是继承了ListView的自定义类:

public class ListViewForScrollView extends ListView {
    public ListViewForScrollView(Context context) {
        super(context);
    }

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

    public ListViewForScrollView(Context context, AttributeSet attrs,
        int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    /**
     * 重写该方法,达到使ListView适应ScrollView的效果
     */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
        MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }
}

其余完全不用修改,只要重写onMeasure方法,这个也是最简便的方法…
在xml布局中和Activty中使用的ListView改成这个自定义ListView就行了。代码就省了吧…
这个方法和方法1有一个同样的毛病,就是默认显示的首项是ListView,需要手动把ScrollView滚动至最顶端。

sv = (ScrollView) findViewById(R.id.act_solution_4_sv);
sv.smoothScrollTo(0, 0);

总结
上面一共给出了3中亲测可用的方法,各自有使用条件,复杂程度也各不相同。经过上述方法的比较,基本上都会采用最后一种方式,修改最少,同时思路也是最正的。至于把其余的方法也写出来是为了掌握这样分析问题的方式,从不同的角度去尝试,从而找到最好的方法。至于最后一种方法的实现原理,我需要再去学习研究。以上仅供参考。

最后一种方法的原理,我学习研究结果补上:

1.MeasureSpec
MeasureSpec是View的一个静态内部类,MeasureSpec类封装了父View传递给子View的布局(layout)要求,每个MeasureSpec实例代表宽度或者高度(只能是其一)要求。MeasureSpec字面意思是测量规格或者测量属性,在measure方法中有两个参数widthMeasureSpec和heightMeasureSpec,如果使用widthMeasureSpec,我们就可以通过MeasureSpec计算出宽的模式Mode和宽度的实际值,当然了也可以通过模式Mode和宽度获得一个MeasureSpec,下面是MeasureSpec的部分核心逻辑。

先贴出以下源码:

public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        /**
         * Creates a measure specification based on the supplied size and mode.
         *
         * The mode must always be one of the following:
         * <ul>
         *  <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
         *  <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
         *  <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
         * </ul>
         *
         * <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's
         * implementation was such that the order of arguments did not matter
         * and overflow in either value could impact the resulting MeasureSpec.
         * {@link android.widget.RelativeLayout} was affected by this bug.
         * Apps targeting API levels greater than 17 will get the fixed, more strict
         * behavior.</p>
         *
         * @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
         */
        public static int makeMeasureSpec(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        /**
         * Extracts the mode from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the mode from
         * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
         *         {@link android.view.View.MeasureSpec#AT_MOST} or
         *         {@link android.view.View.MeasureSpec#EXACTLY}
         */
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }

        /**
         * Extracts the size from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the size from
         * @return the size in pixels defined in the supplied measure specification
         */
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

        static int adjust(int measureSpec, int delta) {
            final int mode = getMode(measureSpec);
            if (mode == UNSPECIFIED) {
                // No need to adjust size for UNSPECIFIED mode.
                return makeMeasureSpec(0, UNSPECIFIED);
            }
            int size = getSize(measureSpec) + delta;
            if (size < 0) {
                Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                        ") spec: " + toString(measureSpec) + " delta: " + delta);
                size = 0;
            }
            return makeMeasureSpec(size, mode);
        }

        /**
         * Returns a String representation of the specified measure
         * specification.
         *
         * @param measureSpec the measure specification to convert to a String
         * @return a String with the following format: "MeasureSpec: MODE SIZE"
         */
        public static String toString(int measureSpec) {
            int mode = getMode(measureSpec);
            int size = getSize(measureSpec);

            StringBuilder sb = new StringBuilder("MeasureSpec: ");

            if (mode == UNSPECIFIED)
                sb.append("UNSPECIFIED ");
            else if (mode == EXACTLY)
                sb.append("EXACTLY ");
            else if (mode == AT_MOST)
                sb.append("AT_MOST ");
            else
                sb.append(mode).append(" ");

            sb.append(size);
            return sb.toString();
        }
    }

MeasureSpec实际上是对int类型的整数进行位运算的一个封装,其中前2位是Mode,后面30位是实际宽或高,Mode就三种情况:

  • UNSPECIFIED(未指定)父元素不会对子元素施加任何束缚,子元素可以得到任意想要的大小;
  • EXACTLY(完全)父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;
  • AT_MOST(至多)子元素至多达到指定大小的值。

注意:三种模式中最常用的是EXACTLY和AT_MOST两种模式,这两种模式分别对应layout布局文件中的match_parent和wrap_content

下面的代码是主要修改的代码 , 其中的MeasureSpec.makeMeasureSpec 方法是关键。

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

makeMeasureSpec传入两个参数:

  1. Integer.MAX_VALUE >> 2 : Integer.MAX_VALUE获取到int的最大值,但是表示大小的值size是int数值的底30位,所以把这个值右移两位,留出高两位表示布局模式。此时这个值仍旧是一个30位所能表示的最大数,用该数作为控件的size,应该足够满足控件大小的需求。
  2. MeasureSpec.AT_MOST表示这个控件适配父控件的最大空间。
    这样测量就可以实现ListView显示不全的问题了

猜你喜欢

转载自blog.csdn.net/ysq_chris/article/details/80804280
今日推荐