內(nèi)容是博主照著書(shū)敲出來(lái)的,博主碼字挺辛苦的,轉(zhuǎn)載請(qǐng)注明出處,后序內(nèi)容陸續(xù)會(huì)碼出。
前言:ListView——列表,它作為一個(gè)非常重要的顯示方式,不管是在Web中還是移動(dòng)平臺(tái)中,都是一個(gè)非常好的、不開(kāi)或缺的展示信息的工具。在Android中,ListView控件接管了這一重?fù)?dān),在大量的場(chǎng)合下,我們都需要使用這個(gè)控件。雖然在Android 5.X時(shí)代,RecyclerView在很多地方都在逐漸取代ListView,但ListView的使用范圍依然非常的廣泛,它這萬(wàn)年老大哥的地位也不是輕易就能撼動(dòng)的。下面就介紹一下ListView常用優(yōu)化技巧。
使用ViewHolder模式提高效率
ViewHolder模式是提高ListView效率的一個(gè)很重要的方法。ViewHolder模式充分利用了ListView的視圖緩存機(jī)制,避免了每次在調(diào)用getView()的時(shí)候都去通過(guò)findViewById()實(shí)例化控件。據(jù)測(cè)試,使用ViewHolder將提高50%以上的效率。使用ViewHolder模式來(lái)優(yōu)化ListView非常簡(jiǎn)單,只需要在自定義Adapter中定義一個(gè)內(nèi)部類(lèi)ViewHolder,并將布局中的控件作為成員變量,代碼如下所示。
public final class ViewHolder {
public ImageView img;
public TextView title;
}
接下來(lái),只要在getView()方法中通過(guò)視圖緩存機(jī)制來(lái)重用以緩存即可,完整的使用ViewHolder創(chuàng)建ListView Adapter的實(shí)例代碼如下所示。
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;
/*********************************************
* author: Blankj on 2016/7/23 15:39
* blog: http://blankj.com
* e-mail: blankj@qq.com
*********************************************/
public class ViewHolderAdapter extends BaseAdapter {
private List<String> mData;
private LayoutInflater mInflater;
public ViewHolderAdapter(Context context, List<String> data) {
this.mData = data;
mInflater = LayoutInflater.from(context);
}
@Override
public int getCount() {
return mData.size();
}
@Override
public Object getItem(int position) {
return mData.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder = null;
// 判斷是否緩存
if (convertView == null) {
viewHolder = new ViewHolder();
// 通過(guò)LayoutInflater實(shí)例化布局
convertView = mInflater.inflate(R.layout.viewholder_item, null);
viewHolder.img = (ImageView) convertView.findViewById(R.id.imageView);
viewHolder.title = (TextView) convertView.findViewById(R.id.textView);
convertView.setTag(viewHolder);
}else {
// 通過(guò)tag找到緩存的布局
viewHolder = (ViewHolder) convertView.getTag();
}
// 設(shè)置布局中控件要顯示的視圖
viewHolder.img.setBackgroundResource(R.mipmap.ic_launcher);
viewHolder.title.setText(mData.get(position));
return convertView;
}
public final class ViewHolder {
public ImageView img;
public TextView title;
}
}
效果很簡(jiǎn)單,這就是一個(gè)簡(jiǎn)單的ListView,如下圖所示。

設(shè)置項(xiàng)目間分隔線(xiàn)
ListView的各個(gè)項(xiàng)目之間,可以通過(guò)設(shè)置分隔線(xiàn)來(lái)進(jìn)行區(qū)分,系統(tǒng)提供了divider和dividerHeight這樣兩個(gè)屬性來(lái)幫助我們實(shí)現(xiàn)這一功能。通過(guò)這兩個(gè)屬性,也可以控制ListView之間的分隔線(xiàn)和它的高度。當(dāng)然,分隔線(xiàn)不僅僅可以設(shè)置為一個(gè)顏色,同樣也可以設(shè)置為一個(gè)圖片資源,分隔線(xiàn)的使用代碼如下所示。
android:divider="@color/colorAccent"
android:dividerHeight="10dp"
以上代碼所實(shí)行的效果如下圖所示。

特殊情況下,當(dāng)設(shè)置分隔線(xiàn)為如下代碼時(shí),就可以把分隔線(xiàn)設(shè)置為透明了。
android:divider="@null"
隱藏ListView的滾動(dòng)條
默認(rèn)的ListView在滾動(dòng)時(shí),在右邊會(huì)顯示滾動(dòng)條,指示當(dāng)前滑動(dòng)的位置,我們可以設(shè)置scrollbars屬性,控制ListView的滾動(dòng)條狀態(tài)。特別地,當(dāng)設(shè)置scrollbars屬性為none的時(shí)候,ListView滾動(dòng)或者不滾動(dòng),就都不會(huì)出現(xiàn)滾動(dòng)條了,代碼如下所示。
android:scrollbars="none"
取消ListView的Item點(diǎn)擊效果
當(dāng)點(diǎn)擊ListView中的一項(xiàng)時(shí),系統(tǒng)默認(rèn)會(huì)出現(xiàn)一個(gè)點(diǎn)擊效果,在Android5.X上是一個(gè)波紋效果,而在Android5.X之下的版本則是一個(gè)改變背景顏色的效果,但可以通過(guò)修改listSelector屬性來(lái)取消掉點(diǎn)擊后的回饋效果,代碼如下所示。
android:listSelector="#00000000"
當(dāng)然,也可以直接使用Android自帶的透明色來(lái)實(shí)現(xiàn)這個(gè)效果,代碼如下所示。
android:listSelector="@android:color/transparent"
設(shè)置ListView需要顯示在第幾項(xiàng)
ListView以Item為單位進(jìn)行顯示,默認(rèn)顯示在第一個(gè)Item,當(dāng)需要指定具體顯示的Item時(shí),可以通過(guò)如下代碼來(lái)實(shí)現(xiàn)。
mListView.setSelection(N);
其中N就是需要顯示的第N個(gè)Item。
當(dāng)然,這個(gè)方法類(lèi)似scrollTo,是瞬間完成的移動(dòng)。除此以外,還可以使用如下代碼來(lái)實(shí)現(xiàn)平滑移動(dòng)。
mListView.smoothScrollBy(distance,duration);
mListView.smoothScrollByOffset(offset);
mListView.smoothScrollToPosition(index);
動(dòng)態(tài)修改ListView
ListView中的數(shù)據(jù)在某些情況下是需要變化的,當(dāng)然可以通過(guò)重新設(shè)置ListView的Adapter來(lái)更新ListView的顯示,但這也就需要重新獲取一下數(shù)據(jù),相當(dāng)于重新刷新創(chuàng)建的ListView,這樣顯然不是非常友好,而且效率也不會(huì)太高。因此,可以使用一個(gè)更簡(jiǎn)單的方法來(lái)實(shí)現(xiàn)ListView的動(dòng)態(tài)修改,代碼如下所示。
mData.add("new");
mAdapter.notifyDataSetChanged();
當(dāng)修改了傳遞給Adapter的映射List之后,只需要通過(guò)調(diào)用Adapter的notifyDataSetChanged()方法,通知ListView更改數(shù)據(jù)源即可完成對(duì)ListView的動(dòng)態(tài)修改。不過(guò),使用這個(gè)方法有一點(diǎn)需要注意的是,在使用mAdapter.notifyDataSetChanged()方法時(shí),必須保證傳進(jìn)Adapter的數(shù)據(jù)List是同一個(gè)List而不能是其他對(duì)象,否則將無(wú)法實(shí)現(xiàn)該效果。下面這個(gè)實(shí)例就演示了如何動(dòng)態(tài)地修改ListView。通過(guò)點(diǎn)擊按鈕,不斷地給原有的List增加一個(gè)新的Item,并調(diào)用notifyDataSetChanged()方法來(lái)實(shí)現(xiàn)ListView的動(dòng)態(tài)更新,完整代碼如下所示。
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.ListView;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
ListView mListView;
ViewHolderAdapter mAdapter;
ArrayList<String> mData;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = (ListView) findViewById(R.id.listView);
mData = new ArrayList<>();
for (int i = 0; i < 20; ++i) {
mData.add(i + "");
}
mAdapter = new ViewHolderAdapter(this, mData);
mListView.setAdapter(mAdapter);
mAdapter.notifyDataSetChanged();
for (int i = 0, len = mListView.getCheckedItemCount(); i < len; i++) {
View view = mListView.getChildAt(i);
}
}
public void btnAdd(View view) {
mData.add("new");
mAdapter.notifyDataSetChanged();
mListView.setSelection(mData.size() - 1);
}
}
實(shí)現(xiàn)的效果如下圖所示。

遍歷ListView中的所有Item
ListView作為一個(gè)ViewGroup,為我們提供了操縱子View的各種方法,最常用的就是通過(guò)getChildAt()來(lái)獲取第i個(gè)子View,代碼如下所示。
for (int i = 0, len = mListView.getCheckedItemCount(); i < len; i++) {
View view = mListView.getChildAt(i);
}
處理空ListView
ListView用于展示列表數(shù)據(jù),但當(dāng)列表中無(wú)數(shù)據(jù)時(shí),ListView不會(huì)顯示任何數(shù)據(jù)或提示,按照完善用戶(hù)體驗(yàn)的需求,這里應(yīng)該給以無(wú)數(shù)據(jù)的提示。幸好,ListView提供了一個(gè)方法——setEmptyView(),通過(guò)這個(gè)方法,我們可以給ListView設(shè)置一個(gè)在空數(shù)據(jù)下顯示的默認(rèn)提示。包含ListView的布局設(shè)置如下。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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"
tools:context="com.blankj.listviewskill.MainActivity">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@null"
android:listSelector="@android:color/transparent"
android:paddingBottom="40dp"/>
<ImageView
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@mipmap/ic_launcher"/>
</FrameLayout>
在代碼中,我們通過(guò)以下方式給ListView設(shè)置空數(shù)據(jù)時(shí)要顯示的布局,代碼如下所示。
mListView.setEmptyView(findViewById(R.id.empty_view));
通過(guò)以上代碼,就給ListView在空數(shù)據(jù)時(shí)顯示了一張默認(rèn)的圖片,用來(lái)提示用戶(hù);而在有數(shù)據(jù)時(shí),則不會(huì)顯示。
ListView的滑動(dòng)監(jiān)聽(tīng)
ListView的滑動(dòng)監(jiān)聽(tīng),是ListView中最重要的技巧,很多重寫(xiě)的ListView基本上都是在滑動(dòng)事件的處理上下功夫,通過(guò)判斷滑動(dòng)事件進(jìn)行不同的邏輯處理。而為了更佳精確地監(jiān)聽(tīng)滑動(dòng)事件,開(kāi)發(fā)者通常還需要使用GestureDetector手勢(shì)識(shí)別、VelocityTracker滑動(dòng)速度檢測(cè)等輔助類(lèi)來(lái)完成更好的監(jiān)聽(tīng)。這里介紹兩種監(jiān)聽(tīng)ListView滑動(dòng)事件的方法,一個(gè)是通過(guò)OnTouchListener來(lái)實(shí)現(xiàn)監(jiān)聽(tīng),另一個(gè)是使用OnScrollListener來(lái)實(shí)現(xiàn)監(jiān)聽(tīng)。
OnTouchListener
OnTouchListener是View中的監(jiān)聽(tīng)事件,通過(guò)監(jiān)聽(tīng)ACTION_DOWN、ACTION_MOVE、ACTION_UP這三個(gè)事件發(fā)生時(shí)的坐標(biāo),就可以根據(jù)坐標(biāo)判斷用戶(hù)滑動(dòng)的方向,并在不同的事件中進(jìn)行相應(yīng)的邏輯處理,這種方式的使用代碼如下所示。
mListView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 觸摸時(shí)操作
break;
case MotionEvent.ACTION_MOVE:
// 移動(dòng)時(shí)操作
break;
case MotionEvent.ACTION_UP:
// 離開(kāi)時(shí)操作
break;
}
return false;
}
});
OnScrollListener
OnScrollListener是AbsListView中的監(jiān)聽(tīng)事件,它封裝了很多ListView相關(guān)的信息,使用起來(lái)也更加靈活。首先來(lái)看一下OnScrollListener的一般使用方法,代碼如下所示。
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState) {
case SCROLL_STATE_IDLE:
// 滑動(dòng)停止時(shí)
Log.d("Test", "SCROLL_STATE_IDLE");
break;
case SCROLL_STATE_TOUCH_SCROLL:
// 正在滾動(dòng)
Log.d("Test", "SCROLL_STATE_TOUCH_SCROLL");
break;
case SCROLL_STATE_FLING:
// 手指拋動(dòng)時(shí),即手指用力滑動(dòng)
// 在離開(kāi)后ListView由于慣性繼續(xù)滑動(dòng)
Log.d("Test", "SCROLL_STATE_FLING");
break;
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// 滾動(dòng)時(shí)一直調(diào)用
Log.d("Test", "onScroll");
}
OnScrollListener中有兩個(gè)回調(diào)方法——onScrollStateChanged()和onScroll()。
先來(lái)看第一個(gè)方法onScrollStateChanged(),這個(gè)方法根據(jù)它的參數(shù)scrollState來(lái)決定其回調(diào)的次數(shù),scrollState有以下三種模式:
·SCROLL_STATE_IDLE:滾動(dòng)停止時(shí)。
·SCROLL_STATE_TOUCH_SCROLL:正在滾動(dòng)時(shí)。
·SCROLL_STATE_FLING:手指拋動(dòng)時(shí),即手指用力滑動(dòng),在離開(kāi)后ListView由于慣性繼續(xù)滑動(dòng)
當(dāng)用戶(hù)沒(méi)有做手指拋動(dòng)的狀態(tài)時(shí),這個(gè)方法只會(huì)回調(diào)2次,否則會(huì)回調(diào)三次,差別就是手指拋動(dòng)的這個(gè)狀態(tài)。通常情況下,我們會(huì)在這個(gè)方法中通過(guò)不同的狀態(tài)來(lái)設(shè)置一些標(biāo)志Flag,來(lái)區(qū)分不同的滑動(dòng)狀態(tài),供其他方法處理。
下面再來(lái)看看onScroll()這個(gè)回調(diào)方法,它在ListView滾動(dòng)時(shí)會(huì)一直回調(diào),而方法中的后三個(gè)int類(lèi)型的參數(shù),則非常精確地顯示了當(dāng)前ListView滾動(dòng)的狀態(tài),這三個(gè)參數(shù)如下所示。
·firstVisibleItem:當(dāng)前能看見(jiàn)的第一個(gè)Item的ID(從0開(kāi)始)
·visibleItemCount:當(dāng)前能看見(jiàn)的Item的總數(shù)。
·totalItemCount:整個(gè)ListView的Item總數(shù)。
這里需要注意的是,當(dāng)前能看見(jiàn)的Item數(shù),包括沒(méi)有顯示完整的Item,即顯示一小半的Item也包括在內(nèi)了。通過(guò)這幾個(gè)參數(shù),可以很方便地進(jìn)行一些判斷,比如判斷是否滾動(dòng)到最后一行,就可以使用如下代碼進(jìn)行判斷,當(dāng)前可視的另一個(gè)Item的ID加上當(dāng)前可視Item的和等于Item總數(shù)的時(shí)候,即滾動(dòng)到了最后一行。
if (firstVisibleItem + visibleItemCount == totalItemCount && totalItemCount > 0) {
Log.d("Test", "滾動(dòng)到最后一行");
}
再比如,可以通過(guò)如下代碼來(lái)判斷滾動(dòng)的方向,代碼如下所示。
if(firstVisibleItem > lastVisibleItem){
// 上滑
}else if(firstVisibleItem < lastVisibleItem){
// 下滑
}
lastVisibleItem = firstVisibleItem;
通過(guò)一個(gè)成員變量lastVisibleItem來(lái)記錄上次第一個(gè)可視的Item的ID并于當(dāng)前的可視Item的ID進(jìn)行比較,即可知道當(dāng)前滾動(dòng)的方向。
要理解整個(gè)OnScrollListener,最好的方法還是在代碼中添加Log,并打印出狀態(tài)信息來(lái)進(jìn)行分析學(xué)習(xí)。在以上代碼中,已經(jīng)添加了相應(yīng)的Log,對(duì)照Log進(jìn)行分析,會(huì)很快掌握OnScrollListener的用法。
當(dāng)然,ListView也給我們提供了一些封裝的方法來(lái)獲得當(dāng)前可視的Item的位置等信息。
// 獲取可視區(qū)域內(nèi)最后一個(gè)Item的id
mListView.getLastVisiblePosition();
// 獲取可視區(qū)域內(nèi)第一個(gè)Item的id
mListView.getFirstVisiblePosition();
項(xiàng)目地址→ListViewSkill
原文地址ListView常用優(yōu)化技巧(Android群英傳)
我的自媒體博客blankj小站(OJ、LeetCode、Android開(kāi)發(fā)),歡迎來(lái)逛逛。