前世的五百次回眸原环,才換來(lái)今生的擦肩而過(guò)箕肃。自從ListView遇到了ScrollView整個(gè)人都不好了坝锰,各種深坑深幾許粹懒,今天就來(lái)深扒下ScrollView嵌套ListView這個(gè)老生常談的話題。
demo下載地址:ListViewHeaderFooter
一顷级、前世的謎團(tuán)重重
使用ScrollView嵌套ListView的初衷是想在列表頁(yè)提供更豐富的內(nèi)容展示形式凫乖,如下圖,通常會(huì)在列表上方增加廣告欄或者按鈕弓颈,上面廣告欄大家俗稱banner(順道也幫大家碼了一個(gè)banner帽芽,BannerView),并且廣告欄和按鈕可以隨著ListView滑動(dòng)翔冀。
二嚣镜、今生的難得糊涂
關(guān)于這個(gè)需求的解決方案網(wǎng)上已是漫山遍野,小編翻爛百度方有小成橘蜜,作為一個(gè)良心碼農(nóng)菊匿,本著不浪費(fèi)觀眾老爺?shù)囊环忠幻氲脑瓌t,先上結(jié)論:所有方案分為兩類(lèi)计福,第一類(lèi)是ScrollView里面嵌套ListView跌捆,包括自定義ListView、手動(dòng)設(shè)置ListView高度象颖、自定義LinearLayout模擬ListView三種實(shí)現(xiàn)方法佩厚;第二類(lèi)是只用ListView,包括自定義Adapter说订、用addHeaderView和addFooterView抄瓦。一共五種方案,最佳方案是用addHeaderView和addFooterView陶冷。
忙人已撤钙姊,噴子留下,待小編擺事實(shí)埂伦、講道理煞额,詳細(xì)分析到底哪種方案最好。
第一類(lèi)是ScrollView里面嵌套ListView,通常會(huì)出現(xiàn)兩個(gè)問(wèn)題:第一膊毁,ListView的高度顯示問(wèn)題胀莹,常見(jiàn)的問(wèn)題就是只顯示一行;第二婚温,ScrollView和ListView都有上下滑動(dòng)事件描焰,放在一起會(huì)存在滑動(dòng)沖突。
先說(shuō)下滑動(dòng)沖突的問(wèn)題栅螟,良心小編再次奉上結(jié)論:** ScrollView嵌套ListView本身對(duì)滑動(dòng)事件的處理已經(jīng)滿足需求荆秦,所以不需要自己對(duì)滑動(dòng)事件做處理。**
下面是我通過(guò)黑盒測(cè)試的實(shí)驗(yàn)過(guò)程:
第一次嵌巷,把ListView的高度設(shè)置成match_parent或wrap_content,把ScrollView的fillViewPort設(shè)置為false室抽,此時(shí)ListView只顯示一行搪哪,因此ScrollView和ListView都不能滑動(dòng)。
第二次坪圾,把ListView的高度設(shè)置成match_parent或wrap_content晓折,把ScrollView的fillViewPort設(shè)置為true,此時(shí)ScrollView和ListView都是全屏顯示兽泄,但是沒(méi)有超出屏幕漓概,所以ScrollView不能滑動(dòng),而ListView可以滑動(dòng)病梢。
第三次胃珍,把ListView的高度設(shè)置成較小的固定值,比如300dp蜓陌,把ScrollView的fillViewPort設(shè)置為false(默認(rèn)值)觅彰,此時(shí)ScrollView不能滑動(dòng),而ListView可以滑動(dòng)钮热。
第四次填抬,把ListView的高度設(shè)置成較大的固定值,比如1000dp隧期,把ScrollView的fillViewPort設(shè)置為false(默認(rèn)值)飒责,此時(shí)ScrollView和ListView的高度都超出了屏幕,但是只有ScrollView在滑動(dòng)仆潮,因?yàn)榛瑒?dòng)ListView的時(shí)候上面的布局被滑動(dòng)上去了宏蛉。
通過(guò)這四個(gè)實(shí)驗(yàn)可以看出ScrollView嵌套ListView時(shí)的滑動(dòng)處理邏輯:能滑動(dòng)的條件是ScrollView或ListView的內(nèi)容超出了其布局或控件的高度;當(dāng)ScrollView能滑動(dòng)時(shí)性置,ScrollView的滑動(dòng)會(huì)覆蓋掉ListView的滑動(dòng)檐晕,這也就意味著banner和按鈕都會(huì)隨著ListView滑動(dòng),所以ScrollView嵌套ListView時(shí)不需要自己做滑動(dòng)事件的處理。
第二個(gè)問(wèn)題是ListView的高度顯示問(wèn)題辟灰,常用方案有三種:第一是自定義ListView个榕,第二是手動(dòng)設(shè)置ListView高度,第三是自定義LinearLayout模擬ListView芥喇。
1. 自定義ListView
public class ListViewForScroll extendsListView
{
public ListViewForScroll(Context context)
{
super(context);
}
public ListViewForScroll(Context context, AttributeSet attrs)
{
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
intexpandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
}
主要就是重載了onMeasure方法西采,改變了heightMeasureSpec。這里widthMeasureSpec和heightMeasureSpec用了32位的int作為參數(shù)继控,高2位代表模式械馆,有三種UNSPECIFIED、EXACTLY武通、AT_MOST霹崎,這是自定義View的基礎(chǔ)知識(shí)。低30位代表數(shù)值冶忱。
MeasureSpec.makeMeasureSpec函數(shù)中第一個(gè)參數(shù)是高度的值尾菇,第二個(gè)參數(shù)是模式,makeMeasureSpec則是把模式和值合成為一個(gè)int值囚枪,這里賦給了高度派诬。
Integer.MAX_VALUE >> 2是int類(lèi)型取30位時(shí)的最大整數(shù),即Integer.MAX_VALUE是int的最大32位值链沼,再右移2位默赂,就是30位,同樣是最大值括勺,只不過(guò)是30位的最大值缆八,所以在模式上也只能選擇MeasureSpec.AT_MOST。最終這個(gè)ListView的顯示高度會(huì)是其能顯示出來(lái)的最大值疾捍,所有的條目都會(huì)顯示出來(lái)耀里。
優(yōu)點(diǎn):寫(xiě)法簡(jiǎn)單,不影響ListView使用拾氓。
缺點(diǎn):
i. 由于高度設(shè)置成最大值冯挎,所有條目都會(huì)進(jìn)行繪制,只是有些條目會(huì)在屏幕之外咙鞍。舉個(gè)例子房官,我傳遞的數(shù)據(jù)有20條,但是屏幕只夠顯示10條续滋,此時(shí)用自定義的ListView會(huì)調(diào)用20次getView把所有條目都繪制出來(lái)翰守,完全放棄了ListView的復(fù)用機(jī)制,跟直接寫(xiě)布局沒(méi)有什么區(qū)別了疲酌,會(huì)造成頁(yè)面加載速度緩慢的問(wèn)題蜡峰。
ii. ListView高度必須設(shè)置成match_parent了袁。
2. 手動(dòng)設(shè)置ListView高度
public static voidsetListViewHeightBasedOnChildren(ListView listView)
{
ListAdapter listAdapter = listView.getAdapter();
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();
}
ViewGroup.LayoutParams params = listView.getLayoutParams();
params.height= totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
listView.setLayoutParams(params);
}
這里就是去獲取每個(gè)條目的View高度,然后所有子View高度相加得到總高度湿颅,并設(shè)置給ListView的LayoutParams载绿。
優(yōu)點(diǎn):能夠?qū)崿F(xiàn)功能需求。
缺點(diǎn):
i. 每個(gè)條目的布局只能用LinearLayout油航,而不能用RelativeLayout崭庸,因?yàn)長(zhǎng)inearLayout重寫(xiě)了onMeasure方法,才能調(diào)用listItem.measure(0, 0)這句谊囚,而其他布局沒(méi)有怕享。
ii. ListView高度必須設(shè)置成match_parent。
iii. 在ListView設(shè)置Adaper和調(diào)用notifyDataSetChanged時(shí)候都要調(diào)用該方法镰踏。
iv. 由于高度設(shè)置成最大值函筋,所有條目都會(huì)進(jìn)行繪制,跟第一個(gè)方法“自定義ListView”存在同樣的問(wèn)題奠伪。
3. 自定義LinearLayout模擬ListView
public class LinearLayoutListView extends LinearLayout
{
private BaseAdapter adapter;
private MyOnItemClickListener onItemClickListener;
boolean footerViewAttached = false;
private View footerview;
public LinearLayoutListView(Context context)
{
super(context);
initAttr(null);
}
public LinearLayoutListView(Context context, AttributeSet attrs)
{
super(context, attrs);
initAttr(attrs);
}
public void initAttr(AttributeSet attrs)
{
setOrientation(VERTICAL);
}
/**
* 初始化footerview
*
* @param footerView
*/
public void initFooterView(final View footerView)
{
this.footerview = footerView;
}
/**
* 設(shè)置footerView監(jiān)聽(tīng)事件
*
* @param onClickListener
*/
public void setFooterViewListener(OnClickListener onClickListener)
{
this.footerview.setOnClickListener(onClickListener);
}
public BaseAdapter getAdapter()
{
return adapter;
}
/**
* 設(shè)置adapter并模擬listview添加????數(shù)據(jù)
*
* @param adpater
*/
public void setAdapter(BaseAdapter adpater)
{
this.adapter = adpater;
removeAllViews();
if (footerViewAttached)
addView(footerview);
notifyChange();
}
/**
* 設(shè)置條目監(jiān)聽(tīng)事件
*
* @param onClickListener
*/
public void setOnItemClickListener(MyOnItemClickListener onClickListener)
{
this.onItemClickListener = onClickListener;
}
/**
* 沒(méi)有下一頁(yè)了
*/
public void noMorePages()
{
if (footerview != null && footerViewAttached)
{
removeView(footerview);
footerViewAttached = false;
}
}
/**
* 可能還有下一??
*/
public void mayHaveMorePages()
{
if (!footerViewAttached && footerview != null)
{
addView(footerview);
footerViewAttached = true;
}
}
/**
* 通知更新listview
*/
public void notifyChange()
{
int count = getChildCount();
if (footerViewAttached)
{
count--;
}
LayoutParams params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT);
for (int i = count; i < adapter.getCount(); i++)
{
final int index = i;
final LinearLayout layout = new LinearLayout(getContext());
layout.setLayoutParams(params);
layout.setOrientation(VERTICAL);
View v = adapter.getView(i, null, null);
v.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v)
{
if (onItemClickListener != null)
{
onItemClickListener.onItemClick(LinearLayoutListView.this, layout, index,
adapter.getItem(index));
}
}
});
ImageView imageView = new ImageView(getContext());
imageView.setBackgroundResource(R.color.background);
imageView.setLayoutParams(params);
layout.addView(v);
layout.addView(imageView);
addView(layout, index);
}
}
public static interface MyOnItemClickListener
{
public void onItemClick(ViewGroup parent, View view, int position, Object o);
}
}
生硬的實(shí)現(xiàn)了ListView的基礎(chǔ)功能跌帐,但是ListView的復(fù)用機(jī)制完全沒(méi)有,跟直接寫(xiě)布局有何區(qū)別芳来。
優(yōu)點(diǎn):能夠?qū)崿F(xiàn)功能需求含末。
缺點(diǎn):
i. ListView高度要設(shè)置成match_parent
ii. 由于高度設(shè)置成最大值猜拾,所有條目都會(huì)進(jìn)行繪制即舌,跟“自定義ListView”存在同樣的問(wèn)題。
下面就是第二類(lèi)的兩種方法挎袜,都只用了ListView顽聂。
4. 自定義Adapter
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
if(position == 0)
{
viewHolderHeaderView = new ViewHolderHeaderView();
if(convertView == null)
{
convertView = LayoutInflater.from(context).inflate(R.layout.headerview, null);
convertView.setTag(viewHolderHeaderView);
}
else
{
viewHolderHeaderView = (ViewHolderHeaderView) convertView.getTag();
}
viewHolderHeaderView.tv_headerview = (TextView) convertView.findViewById(R.id.tv_headerview);
viewHolderHeaderView.tv_headerview.setText("customListView");
}
else if(position > 0)
{
viewHolderNormalListView = new ViewHolderNormalListView();
if(convertView == null)
{
convertView = LayoutInflater.from(context).inflate(R.layout.item_normallistview, null);
convertView.setTag(viewHolderNormalListView);
}
else
{
viewHolderNormalListView = (ViewHolderNormalListView) convertView.getTag();
}
viewHolderNormalListView.iv_item_normallistview = (ImageView) convertView.findViewById(R.id.iv_item_normallistview);
viewHolderNormalListView.tv_item_normallistview = (TextView) convertView.findViewById(R.id.tv_item_normallistview);
viewHolderNormalListView.iv_item_normallistview.setImageResource(itemAdapterBeanArrayList.get(position).icon);
viewHolderNormalListView.tv_item_normallistview.setText("item" + Integer.toString(position));
}
return convertView;
}
就是在Adapter中根據(jù)position分類(lèi)加載。
優(yōu)點(diǎn):只有ListView的情況下盯仪,RecycleIn的復(fù)用機(jī)制再次發(fā)揮作用紊搪,只會(huì)繪制顯示出來(lái)的條目,在頁(yè)面加載速度上好很多全景。
缺點(diǎn):寫(xiě)法稍微復(fù)雜耀石。
5. 用addHeaderView
listview.addHeaderView(banner1);
listview.addFooterView(banner2);
listview.setAdapter(adapter);
通過(guò)給ListView添加HeaderView和FooterView來(lái)實(shí)現(xiàn)。要注意的是爸黄,addHeaderView要在setAdapter前調(diào)用滞伟,多個(gè)addHeaderView會(huì)依次從上向下添加。通過(guò)ListView源碼可以看出炕贵,調(diào)用addHeaderView之后梆奈,再調(diào)用setAdapter,在ListView中會(huì)生成一個(gè)新的HeaderViewListAdapter設(shè)置給ListView称开。
優(yōu)點(diǎn):
i.只有ListView的情況下亩钟,RecycleIn的復(fù)用機(jī)制再次發(fā)揮作用乓梨,只會(huì)繪制顯示出來(lái)的條目,在頁(yè)面加載速度上好很多清酥。
ii.用addHeaderView和自定義Adapter在底層實(shí)現(xiàn)上是一樣的扶镀,但是用addHeaderView在adapter的position處理上更加方便,因?yàn)槠鋚ostion是跟傳進(jìn)來(lái)的數(shù)據(jù)的position對(duì)應(yīng)的总处,而用自定義adapter顯示position是比數(shù)據(jù)position多一條或幾條的狈惫,所以用addHeaderView比用自定義Adapter更加易用。
缺點(diǎn):目前還沒(méi)有發(fā)現(xiàn)鹦马。