1. 程式人生 > >BaseAdapter——convertView回收機制與動態控制元件響應

BaseAdapter——convertView回收機制與動態控制元件響應

前言:對於listView的BaseAdapter的派生,難度比較大。最難理解的莫過於getView(int position, View convertView, ViewGroup parent)這個函式是如何產生每條記錄的,有些部落格中利用holderView,有些部落格卻沒有用,種種方法之間有什麼異同,今天我們就來揭開這個繪製ITEM機制的面紗。

本篇藉助《PullToRefresh使用詳解(二)---重寫BaseAdapter實現複雜XML下拉重新整理》的例子。所以本篇對於程式碼的講解就比較粗略,如果有讀者對於如何重寫BaseAdapter不太熟悉的話,請先移步看看這篇文章,然後再回來這裡,相信會有不一樣的收穫。

一、ConvertView回收機制

工作原理:

1、ListView 針對List中每個item,要求 adapter “給我一個檢視” (getView)。
2、一個新的檢視被返回並顯示

如果我們有上億個專案要顯示怎麼辦?為每個專案建立一個新檢視?NO!這不可能!
實際上Android為你快取了檢視。Android中有個叫做Recycler的構件,下圖是他的工作原理:

如果你有10億個專案(item),其中只有可見的專案存在記憶體中,其他的在Recycler中。
ListView先請求一個type1檢視(getView)然後請求其他可見的專案。convertView在getView中是空(null)的。
當item1滾出螢幕,並且一個新的專案從螢幕低端上來時,ListView再請求一個type1檢視。convertView此時不是空值了,它的值是item1。你只需設定新的資料然後返回convertView,不必重新建立一個檢視。   以上摘自《

ListView中getView的原理+如何在ListView中放置多個item

也就是說:

1、android的listView在初始化的時候,如上面這個列表,整個螢幕只能放下7個item,那麼listView在初始化時,就會只建立7個view,對於這些view也就是引數中的convertView。
2、那問題來了,當繼續網上滑動,item1消失了,而item8出來了。那系統還是為item8重新建立一個新的convertView嗎?另一個問題,item1的convertView去哪了?(銷燬回收資源,還是重新利用?)如果你是系統設計者,你會怎麼做?大家想想,如果為每個要顯示的item都建立新convertView是不是太浪費了,況且對於item1的convertView已經沒用了,我們何不把它拿來給item8用。對!系統就是這樣做的!這就是convertView的回收機制。就是將那些不再被用的ITEM的convertView重新給即將顯示的ITEM使用的機制!

二、例子

先給大家看一下單個ITEM的佈局圖片,對於具體佈局程式碼,看原始碼吧。

對於JAVA原始碼,我們先看這種方式寫的convertView的生成方法。

package com.example.try_pulltorefresh_map;
/**
 * 完成了從TXT文字中提取,並向下重新整理
 * blog:http://blog.csdn.net/harvic880925
 * @author harvic
 * @date  2014-5-8
 * 
 */
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import org.json.JSONArray;

import com.handmark.pulltorefresh.library.PullToRefreshBase;
import com.handmark.pulltorefresh.library.PullToRefreshListView;
import com.handmark.pulltorefresh.library.PullToRefreshBase.Mode;
import com.handmark.pulltorefresh.library.PullToRefreshBase.OnRefreshListener;

import android.os.Bundle;
import android.app.ListActivity;
import android.content.Context;
import android.graphics.Color;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;

public class MainActivity extends ListActivity {
	
	private ArrayList<HashMap<String, Object>> listItem = new ArrayList<HashMap<String, Object>>();
	private PullToRefreshListView mPullRefreshListView;
	MyAdapter adapter=null;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		mPullRefreshListView = (PullToRefreshListView) findViewById(R.id.pull_refresh_list);

		//設定下拉監聽函式
		mPullRefreshListView.setOnRefreshListener(new OnRefreshListener<ListView>() {
			@Override
			public void onRefresh(PullToRefreshBase<ListView> refreshView) {
				String label = DateUtils.formatDateTime(getApplicationContext(), System.currentTimeMillis(),
						DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_ALL);

				// Update the LastUpdatedLabel
				refreshView.getLoadingLayoutProxy().setLastUpdatedLabel(label);

				// Do work to refresh the list here.
			}
		});

		mPullRefreshListView.setMode(Mode.PULL_FROM_END);//設定底部下拉重新整理模式
		
		listItem=getData();//獲取LIST資料
		adapter = new MyAdapter(this);

		//設定介面卡
		ListView actualListView = mPullRefreshListView.getRefreshableView();
		actualListView.setAdapter(adapter);	
		
	}
	
	private ArrayList<HashMap<String, Object>> getData() {
		ArrayList<HashMap<String, Object>> list = new ArrayList<HashMap<String, Object>>();
		HashMap<String, Object> map = new HashMap<String, Object>();
		InputStream inputStream;
		try {
			inputStream=this.getAssets().open("my_home_friends.txt");
			String json=readTextFile(inputStream);
			JSONArray array = new JSONArray(json);
			for (int i = 0; i < array.length(); i++) {
				map = new HashMap<String, Object>();
				map.put("name", array.getJSONObject(i).getString("name"));
				map.put("info", array.getJSONObject(i).getString("info"));
				map.put("img",array.getJSONObject(i).getString("photo"));
				list.add(map);
			}
			return list;	
			
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
		
		
		return list;	
	}

	
	
	public final class ViewHolder{
		public TextView name;
		public TextView info;
		
		public TextView attentntion;
	}		
	
	public class MyAdapter extends BaseAdapter{

		private LayoutInflater mInflater;
		
		public MyAdapter(Context context){
			this.mInflater = LayoutInflater.from(context);
		}
		@Override
		public int getCount() {
			// TODO Auto-generated method stub
			return listItem.size();
		}

		@Override
		public Object getItem(int arg0) {
			// TODO Auto-generated method stub
			return null;
		}

		@Override
		public long getItemId(int arg0) {
			// TODO Auto-generated method stub
			return 0;
		}
		
		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			
			System.out.println("position:"+position+"   convertView:"+convertView);
			ViewHolder holder = null;
					
			holder=new ViewHolder();  		
			convertView = mInflater.inflate(R.layout.item, null);
			holder.name = (TextView)convertView.findViewById(R.id.name);
			holder.info = (TextView)convertView.findViewById(R.id.info);
			holder.attentntion=(TextView)convertView.findViewById(R.id.attention);
			
			
			holder.name.setText((String)listItem.get(position).get("name"));
			holder.info.setText((String)listItem.get(position).get("info"));
			
			final TextView attention=holder.attentntion;
			holder.attentntion.setOnClickListener(new View.OnClickListener() {				
				@Override
				public void onClick(View v) {
					// TODO Auto-generated method stub
					attention.setTextColor(Color.RED);
				}
			});
			
			convertView.setTag(holder);		

			return convertView;
		}
		
	}
	
	
	////工具類
	/**
	 * 
	 * @param inputStream
	 * @return
	 */
	public String readTextFile(InputStream inputStream) {
		String readedStr = "";
		BufferedReader br;
		try {
			br = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
			String tmp;
			while ((tmp = br.readLine()) != null) {
				readedStr += tmp;
			}
			br.close();
			inputStream.close();
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		return readedStr;
	}


}

以上程式碼凡是懂如何派生自BaseAdapter的應該都可以看懂,這裡就不再多講,只看核心程式碼,摘錄如下:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
	
	System.out.println("position:"+position+"   convertView:"+convertView);
	ViewHolder holder = null;
			
	holder=new ViewHolder();  		
	convertView = mInflater.inflate(R.layout.item, null);
	holder.name = (TextView)convertView.findViewById(R.id.name);
	holder.info = (TextView)convertView.findViewById(R.id.info);
	holder.attentntion=(TextView)convertView.findViewById(R.id.attention);
	
	holder.name.setText((String)listItem.get(position).get("name"));
	holder.info.setText((String)listItem.get(position).get("info"));
	
	final TextView attention=holder.attentntion;
	holder.attentntion.setOnClickListener(new View.OnClickListener() {				
		@Override
		public void onClick(View v) {
			// TODO Auto-generated method stub
			attention.setTextColor(Color.RED);
		}
	});	

	return convertView;
}

這裡利用system.out.println對convertView進行捕捉,執行如果如下:

手機初始化是這樣子的:

根據上面我們講的理論,在初始化時,整個螢幕能放下三個ITEM,所以會建立三個全新的convertView。當我往下拉一個ITEM,出現第四個ITEM的時候,就會回收第一個ITEM的convertView給第四個。捕捉結果如下:


清楚的看到,前四個convertView為NULL,當第五個ITEM出現時,此時由於第一個ITEM肯定已經滾出螢幕,所以將其重新傳給即將出現的item5使用。我們上面說的第四個ITEM出現的時候就應該不再建立新convertView了,我想android開發者在考慮多建立一個ITEM的目的在於更安全吧。
回到上面的程式碼,好像看著程式碼沒有任何問題,我在裡面寫了個clickListener,當點選“關注”的時候,字型會變紅,試一下。
            點選“關注”                     下拉後再拉回來

    

問題出現了:當拉回來的時候,“關注”不再紅了!!!!!!為什麼????
問題出在程式碼上:

holder=new ViewHolder();  		
convertView = mInflater.inflate(R.layout.item, null);
holder.name = (TextView)convertView.findViewById(R.id.name);
holder.info = (TextView)convertView.findViewById(R.id.info);
holder.attentntion=(TextView)convertView.findViewById(R.id.attention);

每次執行getView獲取當前ITEM時,都會重新new 一個viewHolder與R.layout.item繫結,也就是說,每次都會產生一個新佈局賦值給convertView讓其顯示。而我們上面講了,android會將回收過來的convertView返回給即將顯示的getView使用,以節約資源。而我們這裡卻沒有領情,每次都重新建立一個佈局賦給convertView,由於每次都建立一個新佈局,所以當ITEM1被重新拉回來顯示的時候,由於是重新建立的佈局,當然是初始狀態。“關注”當然也就是黑色的了。
改進:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
	
	System.out.println("position:"+position+"   convertView:"+convertView);
	ViewHolder holder = null;
	
	if (convertView == null) {
		
	holder=new ViewHolder();  	
	convertView = mInflater.inflate(R.layout.item, null);
	holder.name = (TextView)convertView.findViewById(R.id.name);
	holder.info = (TextView)convertView.findViewById(R.id.info);
	holder.attentntion=(TextView)convertView.findViewById(R.id.attention);
	convertView.setTag(holder);		
	}else {
	
	holder = (ViewHolder)convertView.getTag();
	}
	
	holder.name.setText((String)listItem.get(position).get("name"));
	holder.info.setText((String)listItem.get(position).get("info"));
	
	final TextView attention=holder.attentntion;
	holder.attentntion.setOnClickListener(new View.OnClickListener() {				
		@Override
		public void onClick(View v) {
			// TODO Auto-generated method stub
			attention.setTextColor(Color.RED);
		}
	});
	return convertView;
}	

不同的部分在這:

ViewHolder holder = null;

if (convertView == null) {
	
holder=new ViewHolder();  	
convertView = mInflater.inflate(R.layout.item, null);
holder.name = (TextView)convertView.findViewById(R.id.name);
holder.info = (TextView)convertView.findViewById(R.id.info);
holder.attentntion=(TextView)convertView.findViewById(R.id.attention);
convertView.setTag(holder);		
}else {

holder = (ViewHolder)convertView.getTag();
}

當convertView為空,即初始化建立時,我們就將生成的佈局利用setTag()儲存在convertView中,當convertView利用回收機制回收過來讓我們再次使用時,我們通過getTag()將儲存的佈局取出來,重新將佈局裡的各個控制元件重新賦值就可以了。這裡就利用了android-listView的回收機制。
再看"關注"的點選事件執行的怎樣:
           點選                                         拉下去再拉回來                                    再往下拉

     

看第二張圖,當拉下去再拉回來的時候,一切正常,但當我們再往下拉(第三張圖),問題又出現了,明明沒有點P-5,為什麼關注反而是紅色的!!!!!!

這是因為,P-5用的是P-1回收來的convertView!!!而P-1的convertView的佈局裡“關注”是紅色的。所以只要回收機制在,我們就沒有辦法改變從P-1回收來的convertView裡的圖片佈局,除非人為的將其重置!

理解了這個問題以後,我們想想解決辦法。
首先申請一個arrayList  attentionArr變數,儲存使用者點選“關注”的ITEM的position,然後在繪製當前ITEM時,根據這個position是否在attentionArr裡來判斷是不是將“關注”重新變紅。

程式碼如下:

@Override
public View getView(final int position, View convertView, ViewGroup parent) {
	
	System.out.println("position:"+position+"   convertView:"+convertView);
	ViewHolder holder = null;
	
	if (convertView == null) {
		
	holder=new ViewHolder();  	
	convertView = mInflater.inflate(R.layout.item, null);
	holder.name = (TextView)convertView.findViewById(R.id.name);
	holder.info = (TextView)convertView.findViewById(R.id.info);
	holder.attentntion=(TextView)convertView.findViewById(R.id.attention);
	convertView.setTag(holder);		
	}else {
	
	holder = (ViewHolder)convertView.getTag();
	}
	
	holder.name.setText((String)listItem.get(position).get("name"));
	holder.info.setText((String)listItem.get(position).get("info"));
	final TextView attention=holder.attentntion;
	
	//根據當前position判斷,重新制做樣式
	if (attentionArr.contains(position)) {
		attention.setTextColor(Color.RED);
	}else {
		attention.setTextColor(Color.BLACK);
	}

	holder.attentntion.setOnClickListener(new View.OnClickListener() {				
		@Override
		public void onClick(View v) {
			// TODO Auto-generated method stub
			attention.setTextColor(Color.RED);
			attentionArr.add(position);//在點選時將position加入其中
		}
	});
	return convertView;
}
理解程式碼難度不大,首先在OnClickListener時,將position加入到attentionArr陣列中,然後在getView裡,判斷當前position是不是使用者點選過的,即是否包含在attentionArr陣列中,如果是,則將“關注”置為紅色,否則置為初始色,黑色。

原始碼說明:根據本文的順序,分為三個原始碼,第一個即全部重新建立convertView版,第二個是利於回收機制,將holderView保存於convertView中,然後再取出來的那版,最後一個,是基於第二個的基礎上修改的,也是本篇的最終版。

請大家尊重原創者版權,轉載請標明出處:http://blog.csdn.net/harvic880925/article/details/25335957 不勝感激!