Android:ListView的擴充與進階

iamxiarui_發表於2018-07-02

##寫在前面

很長時間沒有更新部落格了,因為最近一直在寫即時聊天的Demo。跟著教程完成了一個基於XMPP的IM,完成後又寫了一個基於環信的IM。感覺真心學了很多東西,都想分享出來,依次來吧。首先想說的就是關於ListView的一些知識。

ListView是Android中最常見也最難用的控制元件,因為ListView用處很多,幾乎所有的APP都會用到這樣一個控制元件。它基本的用法大家都知道,這裡我就不再贅述了。本文主要是ListView的一些擴充套件知識,有以下幾個方面:

  • 更改自帶ListView主題的樣式
  • 多樣式ListView
  • ViewHolder的兩種寫法
  • ListView的效率優化
  • 一些乾貨網站推薦

##更改自帶ListView主題的樣式

我們知道,在大部分APP開發環境中,ListView都是需要自定義的。這裡說的自定義意思是需要我們自定義一個介面卡繼承基本的Adapter類,比如BaseAdapter等。因為基本的Adapter不滿足一些複雜的列表效果展示。當我們自定義Adapter的時候,就可以複寫父類的方法,從而實現複雜的列表。比如這樣:

比較複雜的ListView

而小部分場景呢,我們只需要基本的Adapter就可以了,比如只需要展示字串,這個時候只需要繫結ArrayAdapter就能滿足需求。

簡單的ListView

下面就是上面這個ListView的具體程式碼,很簡單,不再解釋。

/**
 * @ClassName: MainActivity
 * @Description:ListView的擴充
 * @author: iamxiarui@foxmail.com
 * @date: 2016年5月30日 下午2:15:46
 */
public class MainActivity extends Activity {

	private ListView mainListView;
	private ArrayList<String> listData;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		initView();

		initData();

		initAdapter();
	}

	/**
	 * @Title: initView
	 * @Description:初始化View
	 * @return: void
	 */
	private void initView() {
		mainListView = (ListView) findViewById(R.id.lv_main);
	}

	/**
	 * @Title: initData
	 * @Description:初始化列表資料
	 * @return: void
	 */
	private void initData() {
		listData = new ArrayList<String>();
		for (int i = 0; i < 30; i++) {
			listData.add("這是第 " + i + " 條資料");
		}
	}

	/**
	 * @Title: initAdapter
	 * @Description:繫結介面卡
	 * @return: void
	 */
	private void initAdapter() {
		mainListView.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, listData));
	}

}
複製程式碼

可以看出,這裡ListView的樣式是android自帶的樣式android.R.layout.simple-list-item-1。當然說的重點不是這個,現在我們設想有這樣一個需求。我們只需要展示字串,沒有複雜的樣式,不需要自定義Adapter,但是我們需要在android自帶樣式的基礎上,將列表中文字的顏色改成其他顏色或者更換文字大小。那該怎麼做呢?

沒錯!就是複寫**getView()**方法。程式碼如下:

/**
 * @Title: initAdapter
 * @Description:繫結介面卡,並更改自帶主題樣式
 * @return: void
 */
private void initAdapter() {
	mainListView.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, listData) {
		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			// 這裡我們知道,item中只有一個TextView,可以直接寫
			TextView textView = (TextView) super.getView(position, convertView, parent);
			textView.setTextColor(Color.BLUE);
			textView.setTextSize(36);
			return textView;
		}
	});
}
複製程式碼

現在執行後可以看出,樣式已經發生了改變。

更改樣式後的ListView

所以在沒有自定義Adapter的前提下,我們仍然能對基本的Adapter做一些樣式上的更改。

##多樣式ListView

多樣式ListView也是比較常見的,意思就是一個ListView中的item有多種樣式。最常見的莫過於聊天記錄的ListView了,傳送者與接收者的訊息是不同的樣式。

聊天記錄ListView

那這樣是如何做的呢?毫無疑問,我們需要自定義Adapter,但是在這裡,我們複寫的方法除了常用的**getCount()、getItem()、getItemId()、getView()**四個方法外,我們還需要複寫下面兩個方法:

  • public int getViewTypeCount() :這個方法是指定ListView中樣式的數目,預設返回1
  • public int getItemViewType(int position) :根據引數positon指定當前item的樣式型別

現在我們試著寫出如下效果的ListView,條目依次不同,相當於一個ListView中存在兩種樣式。

兩種樣式的ListView

程式碼中的重點就是下面的自定義Adapter,重點中的重點就是上面說到的兩個方法的複寫,具體如下:

/**
 * @ClassName: MyListAdapter
 * @Description:自定義兩種樣式的介面卡
 * @author: iamxiarui@foxmail.com
 * @date: 2016年5月30日 下午2:51:00
 */
public class MyListAdapter extends BaseAdapter {

	private Context context;
	private ArrayList<String> list;

	public MyListAdapter(Context context, ArrayList<String> list) {
		this.context = context;
		this.list = list;
	}

	@Override
	public int getCount() {
		return list.size();
	}

	@Override
	public Object getItem(int position) {

		return list.get(position);
	}

	@Override
	public long getItemId(int position) {

		return position;
	}

	/**
	 * @Title: getViewTypeCount
	 * @Description:兩種樣式
	 * @return: 樣式的數目
	 */
	@Override
	public int getViewTypeCount() {
		return 2;
	}

	/**
	 * @Title: getItemViewType
	 * @Description:每個item的樣式
	 * @param position:item位置
	 * @return: 樣式
	 */
	@Override
	public int getItemViewType(int position) {
		// 為了方便起見,我們根據奇偶項來區別樣式,注意從0開始數
		if (position % 2 != 0) {
			// 奇數項返回0
			return 0;
		} else {
			// 偶數項返回0
			return 1;
		}
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		View view;
		if (convertView == null) {
			convertView = View.inflate(context, R.layout.item_list, null);
		}
		view = convertView;
		// 如果是奇數項
		if (getItemViewType(position) == 0) {
			TextView itemText = (TextView) view.findViewById(R.id.tv_item);
			itemText.setText(list.get(position));
			itemText.setTextColor(Color.BLUE);
		}
		// 如果是偶數項
		else if (getItemViewType(position) == 1) {
			TextView itemText = (TextView) view.findViewById(R.id.tv_item);
			itemText.setText(list.get(position));
			itemText.setTextColor(Color.GRAY);
		}
		return view;
	}

}
複製程式碼

這樣我們就完成了ListView中存在多樣式的實現,有了這個簡單例子,當然可以實現聊天記錄列表或者其他更加複雜的多樣式。

##ViewHolder的兩種寫法

ViewHolder是官方提出的一種優化ListView的工具,主要是減少了一些重複的操作,極大的提升了ListView的一些效能,當然這個大家也都非常熟悉了。

用我們之前的例子來看,它的一般寫法是這樣的:

/**
 * @ClassName: ViewHolder
 * @Description:定義ViewHolder類
 * @author: iamxiarui@foxmail.com
 * @date: 2016年5月30日 下午3:28:05
 */
private static class ViewHolder {
	TextView itemText;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
	ViewHolder viewHolder;
	if (convertView == null) {
		// 不存在的時候,建立ViewHolder
		viewHolder = new ViewHolder();
		convertView = View.inflate(context, R.layout.item_list, null);

		viewHolder.itemText = (TextView) convertView.findViewById(R.id.tv_item);

		viewHolder.itemText.setText(list.get(position));

		// 儲存ViewHolder
		convertView.setTag(viewHolder);
	} else {
		// 存在的話直接複用
		viewHolder = (ViewHolder) convertView.getTag();
	}

	return convertView;
}
複製程式碼

效果圖跟第一幅圖一樣,這裡就不貼了。這種寫法也是谷歌官方的寫法,一般來說大家都這樣寫。

但是在看Github上一個開源庫的時候,看到一個大神是這樣寫的:

/**
 * @ClassName: ViewHolder
 * @Description:定義ViewHolder類
 * @author: iamxiarui@foxmail.com
 * @date: 2016年5月30日 下午3:28:05
 */
private static class ViewHolder {
	TextView itemText;

	// 建構函式中就初始化View
	public ViewHolder(View convertView) {
		itemText = (TextView) convertView.findViewById(R.id.tv_item);
	}

	// 得到一個ViewHolder
	public static ViewHolder getViewHolder(View convertView) {
		ViewHolder viewHolder = (ViewHolder) convertView.getTag();
		if (viewHolder == null) {
			viewHolder = new ViewHolder(convertView);
			convertView.setTag(viewHolder);
		}
		return viewHolder;
	}
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
	ViewHolder viewHolder;
	if (convertView == null) {
		convertView = View.inflate(context, R.layout.item_list, null);
	}
	// 得到一個ViewHolder
	viewHolder = ViewHolder.getViewHolder(convertView);
	viewHolder.itemText.setText(list.get(position));

	return convertView;
}
複製程式碼

對比來看,在定義ViewHolder類的時候,需要實現建構函式,並封裝得到ViewHolder的方法,看似是比原來的複雜很多,但是仔細推敲,這樣做的好處有以下幾點:

  • 將ViewHolder的實現方法完全封裝在其本身的類中,功能更強大
  • 其方法與Adapter的getView()方法隔離,條理更加清晰
  • 當getView()中需要實現多種樣式時,不需要寫重複程式碼

所以在開發過程中,我一般選取第二種方式,雖然看似複雜很多,但是更加符合邏輯。

##ListView的效率優化

關於ListView的優化是很多開發者感到困擾的問題,比如ListView的卡頓、圖片載入異常等問題十分棘手。其實提高ListView的流暢度只要記著兩點就行了:

  • 複用View中的item
  • 非同步處理耗時任務

這個問題在《Android開發藝術探索》中有專門的介紹,具體大概有四種方法:

###1、使用ViewHoler

不論是getView()方法中的convertView還是使用ViewHolder,其實基本原理都是複用已經用過的item。也就是在滑動過程中不需要一直new出新的View。而關於ViewHolder的具體使用,上面已經說過了,這裡就不在贅述了。

###2、不在getView()中執行耗時任務

其實這個跟網路請求要在子執行緒中執行一樣,不在UI執行緒中執行耗時任務,這樣會造成主執行緒異常的卡頓。同樣的在ListView中也不能在getView()方法中執行耗時任務,比如從網路上請求一些圖片之類的。

關於載入圖片我們可以使用ImageLoader實現,也就是非同步的方式處理。

###3、控制非同步任務的執行頻率

其實這句話的意思就是,滑動的時候不執行非同步載入,等停止滑動的時候再執行非同步任務。這樣就避免了滑動過程中的卡頓問題。

具體實現方式可以在**onScrollStateChange()**方法中定義一個滑動標誌,然後在getView()中判斷這個標誌,如果已經停止滑動,那就啟動載入任務。

###4、開啟硬體加速

一般來說,複用View與非同步載入方式已經能夠解決ListView卡頓的情況,如果你還是不滿意的話,可以嘗試開啟硬體加速,也可以解決一些卡頓問題。開啟方式是在AndroidManifest.xml中對應的Activity新增下面這句:

android:hardwareAccelerated = "true"

專案原始碼

Github-IamXiaRui-Android_ListViewDemo

Github-IamXiaRui-RecyclerView


個人部落格:www.iamxiarui.com 原文連結:http://www.iamxiarui.com/?p=674

相關文章