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