1. 程式人生 > >Android教你如何一步步打造通用介面卡

Android教你如何一步步打造通用介面卡

前言

在Android開發中ListView是最為常用的控制元件之一,基本每個應用都會涉及到它,要使用ListView列表展示,就不可避免地涉及到另外一個東西——Adapter,我們都知道,Adapter是連線資料和列表介面的一個橋樑,一般專案中一個listview就會有一個Adapter與之對應,然後就是一堆方法的重寫,包括getCount,getItem,getView等等,遇到自定義佈局時還需重寫getView方法,重寫getView的時候邏輯不復雜還好,遇到程式碼邏輯複雜的時候adapter簡直臃腫,並且還需要寫很多次重複的程式碼,比如判斷convertView是否為空,findViewById無數次停不下來睡覺



寫了這麼多,你是否想過,可否有一個公用的自定義Adapter基類,將這些經常重複的程式碼和邏輯封裝起來,方便我們呼叫,減少getView中的程式碼邏輯,下面就來一步步將其“包裝”起來成為我們想要的效果。


先走一遍我們之前寫ListView和Adapter的方式:

activity_main.xml:

<RelativeLayout 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"
    >

    <ListView 
        android:id="@+id/listview"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        
    </ListView>

</RelativeLayout>


MainActivity:

public class MainActivity extends Activity {
	
	private ListView listview;
	
	private MyAdapter adapter;
	
	private List<String> data;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		listview = (ListView)this.findViewById(R.id.listview);
		
		getData();
		
		adapter = new MyAdapter(this.getApplicationContext(), data);
		
		listview.setAdapter(adapter);
	}
	
	public void getData(){
		data = new ArrayList<String>();
		for(int i=0; i<20; i++){
			data.add("資料"+i);
		}
	}
}


自定義介面卡 MyAdapter:

public class MyAdapter extends BaseAdapter{
	
	private Context mContext;
	
	private List<String> list;
	
	public MyAdapter(Context context, List<String> list){
		this.mContext = context;
		this.list = list;
	}
	
	@Override
	public int getCount() {
		// TODO Auto-generated method stub
		return list.size();
	}
	
	@Override
	public Object getItem(int position) {
		// TODO Auto-generated method stub
		return list.get(position);
	}
	
	@Override
	public long getItemId(int position) {
		// TODO Auto-generated method stub
		return position;
	}
	
	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		// TODO Auto-generated method stub
		ViewHolder viewholder = null;
		if(convertView == null){
			convertView = View.inflate(mContext, R.layout.item_listview, null);
			viewholder = new ViewHolder();
			viewholder.titleTv = (TextView)convertView.findViewById(R.id.titleTv);
			convertView.setTag(viewholder);
		}
		else{
			viewholder = (ViewHolder)convertView.getTag();
		}
		viewholder.titleTv.setText(list.get(position));
		
		return convertView;
	}
	
	public static class ViewHolder{
		TextView titleTv;
	}

}


每個列表項的佈局檔案 item_listview.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    
    <TextView 
        android:id="@+id/titleTv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingTop="15dp"
        android:paddingBottom="15dp"
        android:paddingLeft="15dp"
        android:textSize="20sp"
        android:textColor="#000"
        />
    
</LinearLayout>


對於以上這部分程式碼不太明白可以參考我之前的文章ListView基礎篇ListView優化篇,通過以上程式碼就可以完成一個簡單的列表介面和資料展示:


接下來我們就在這個基礎上進行一步步封裝:

可以看到,我們每次呼叫getView時候,都會新建一個ViewHolder,然後判斷convertView是否為空,以及用

viewholder儲存我們每個列表項的子控制元件,再通過setTag和getTag來複用Viewholder,這一部分邏輯是每次getView

都會呼叫的,所以首先能夠想到將ViewHolder物件的邏輯給封裝起來,封裝ViewHolder首先要考慮以下幾點:

封裝ViewHolder

1.首先這個封裝來肯定要有一個ViewHolder的構造方法,另外,從上面可以看出每次getView都需要初始化convertView,那麼我們可以將convertView的初始化搬到ViewHold的構造方法中來進行,既然convertView要在ViewHolder的構造方法中初始化,那麼必定還需要inflate所需要的引數,以及每一個Item的下標,即context、layoutId、ViewGroup、position:

public class CommonViewHolder {
	
	public View mConvertView;

	public CommonViewHolder(Context context, int position, int layoutId, ViewGroup parent){
		mConvertView = View.inflate(context, layoutId, null);
		mConvertView.setTag(this);
	}
}


2.注意到以前的方式每次都需要判斷convertView是否為null,是則new一個新的convertView和ViewHolder例項並且setTag,否則採用getTag重用之前的ViewHolder:

public static CommonViewHolder get(Context context, View convertView, int position, int layoutId, ViewGroup parent){
	if(convertView == null){
		return new CommonViewHolder(context, position, layoutId, parent);
	}
	else{
		return (CommonViewHolder)convertView.getTag();
	}
}


3.不同場景下列表項的元素是不確定的,數量和型別都不一致,既然是打造通用Adapter,那肯定要相容多種情況,數量上我們可以想到使用Map來儲存我們的子控制元件,型別上可以使用Java的泛型來構造,如下:

private HashMap<Integer, View> map = new HashMap<Integer, View>();

public <T extends View> T getView(int viewId){
	View view = map.get(viewId);
	//如果view為空,則findId找到,並放進map中
	if(view == null){
		view = mConvertView.findViewById(viewId);
		map.put(viewId, view);
	}
	//如果view不會空,則直接返回
	return (T)view;
}


4.當然,以前我們getView方法最後返回的是一個convertView,所以還可以提供一個getConvertView的方法返回每一行對應的convertView:

public View getConvertView(){
	return mConvertView;
}


至此,完整的通用ViewHolder已打造完畢,完整程式碼如下:

public class CommonViewHolder {
	
	public HashMap<Integer, View> map;
	
	public View mConvertView;
	
	public CommonViewHolder(Context context, int position, int layoutId, ViewGroup parent){
		map = new HashMap<Integer, View>();
		mConvertView = View.inflate(context, layoutId, null);
		mConvertView.setTag(this);
	}
	
	public static CommonViewHolder get(Context context, View convertView, int position, int layoutId, ViewGroup parent){
		if(convertView == null){
			return new CommonViewHolder(context, position, layoutId, parent);
		}
		else{
			return (CommonViewHolder)convertView.getTag();
		}
	}
	
	public <T extends View> T getView(int viewId){
		View view = map.get(viewId);
		if(view == null){
			view = mConvertView.findViewById(viewId);
			map.put(viewId, view);
		}
		return (T)view;
	}

	public View getConvertView(){
		return mConvertView;
	}
}


如今,我們Adapter中的getView也就變成了這個樣子:

public View getView(int position, View convertView, ViewGroup parent) {
	// TODO Auto-generated method stub
	CommonViewHolder holder = CommonViewHolder.get(mContext, convertView, position, R.layout.item_listview, parent);
	TextView titleTv = holder.getView(R.id.titleTv);
	titleTv.setText(list.get(position));
	return holder.getConvertView();
}


它的程式碼執行流程如下:
1.首先進入get方法獲取一個ViewHolder例項,如果convertView為空,則進入到構造方法,new一個用來存放這一行的map集合,inflate一個新的View,並且給它setTag,如果convertView不為空,則直接通過getTag獲得ViewHolder例項

2.接著呼叫holder.getView,傳入控制元件ID,如果在該map中還未有過,則通過findViewById找到控制元件,並存放進該行的View集合中,如果已經存在,則可以進行View的複用,即直接map.get(viewId);

3.最後呼叫getConvertView,獲得我們已經處理好的convertView例項

封裝Adapter

上面我們對ViewHolder進行了封裝,讓adapter的getView方法大大簡化,接下來開始封裝我們的Adapter
封裝Adapter成為公共類,我們需要注意以下問題:
平時我們寫Adapter的時候資料型別總是不一樣的,比如一會兒是一個User列表,一會兒是一個Car列表,傳進來的資料來源的型別一般是不一樣的,那如何做到不管傳進來什麼型別都能使用呢?是的沒錯,又是通過泛型來解決:

public abstract class CommonAdapter<T> extends BaseAdapter{
	
	public Context mContext;
	
	public List<T> list;
	
	public CommonAdapter(Context context, List<T> list){
		this.mContext = context;
		this.list = list;
	}
	
	@Override
	public int getCount() {
		// TODO Auto-generated method stub
		return list.size();
	}
	
	@Override
	public Object getItem(int position) {
		// TODO Auto-generated method stub
		return list.get(position);
	}
	
	@Override
	public long getItemId(int position) {
		// TODO Auto-generated method stub
		return position;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		// TODO Auto-generated method stub
		CommonViewHolder holder = CommonViewHolder.get(mContext, convertView, position, R.layout.item_listview, parent);
		TextView titleTv = holder.getView(R.id.titleTv);
		titleTv.setText(list.get(position));
		return holder.getConvertView();
	}	
	
}


可以看到,我們將Adapter的資料型別代替為泛型的形式,並且我們定義的CommonAdapter為一個抽象類,這樣做的原因是在基類先將getCount、getItem等方法給實現了,然後以後的具體Adapter類就只需要繼承該CommonAdapter,但是其實開發中我們的getView是每次的操作都是各有所異的,不可能定死,而且仔細看你會發現getView中的生成holder例項的程式碼和返回convertView例項的程式碼是千篇一律,區別只在於傳進來的item的佈局id以及控制元件的生成不一樣罷了,所以可以將這部分不一樣的提取出來放在一個抽象方法中,留給子類去實現,將

MainActivity中:CommonAdapter修改如下:

public abstract class CommonAdapter<T> extends BaseAdapter{
	
	public Context mContext;
	
	public List<T> list;
	
	public int layoutId;
	
	public CommonAdapter(Context context, List<T> list, int layoutId){
		this.mContext = context;
		this.list = list;
		this.layoutId = layoutId;
	}
	
	@Override
	public int getCount() {
		// TODO Auto-generated method stub
		return list.size();
	}
	
	@Override
	public Object getItem(int position) {
		// TODO Auto-generated method stub
		return list.get(position);
	}
	
	@Override
	public long getItemId(int position) {
		// TODO Auto-generated method stub
		return position;
	}
	
	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		// TODO Auto-generated method stub
		CommonViewHolder holder = CommonViewHolder.get(mContext, convertView, position, layoutId, parent);
		convert(holder, list.get(position), position);
		return holder.getConvertView();
	}
	//這個就是留給具體Adapter實現的方法
	public abstract void convert(CommonViewHolder viewHolder, T data, int position);
	
}


至此,我們的通用Adapter打造完畢,接下來我們來看看實踐效果:

例子1

先定義一個用於純文字顯示的ListView的Adapter類:

public class TextListViewAdapter extends CommonAdapter<String>{

	public TextListViewAdapter(Context context, List<String> list) {
		super(context, list);
		// TODO Auto-generated constructor stub
	}
	
	public View getView(int position, View convertView, ViewGroup parent) {
		// TODO Auto-generated method stub
		CommonViewHolder holder = CommonViewHolder.get(mContext, convertView, position, R.layout.item_listview, parent);
		TextView titleTv = holder.getView(R.id.titleTv);
		titleTv.setText(list.get(position));
		return holder.getConvertView();
	}	
	
}


MainActivity中:

public class MainActivity extends Activity {
	
	private ListView listview;
	private CommonAdapter adapter;
	private List<String> data;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		listview = (ListView)this.findViewById(R.id.listview);
		initData();
		adapter = new TextListViewAdapter(this.getApplicationContext(), data);
		listview.setAdapter(adapter);
	}
	
	public void initData(){
		data = new ArrayList<String>();
		for(int i=0; i<20; i++){
			data.add("資料"+i);
		}
	}
}


可以看到,Adapter的程式碼比以前省去了好多,執行後效果:


例子2

我們再試試多控制元件的情況,將item_listview佈局檔案更改如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:layout_marginTop="15dp"
    android:layout_marginBottom="15dp"
    android:orientation="horizontal"
    >
    
    <ImageView 
        android:id="@+id/item_iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:src="@drawable/ic_launcher"/>
    
    <LinearLayout 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:orientation="vertical">
        
        <TextView 
	        android:id="@+id/titleTv"
	        android:layout_width="match_parent"
	        android:layout_height="0dp"
	        android:layout_weight="2"
	        android:gravity="bottom"
	        android:paddingTop="10dp"
	        android:paddingBottom="0dp"
	        android:paddingLeft="15dp"
	        android:textSize="15sp"
	        android:textColor="#000"
	        android:text="標題"
	        />
        <TextView 
	        android:id="@+id/detailTv"
	        android:layout_width="wrap_content"
	        android:layout_height="0dp"
	        android:layout_weight="2"
	        android:gravity="top"
	        android:paddingTop="0dp"
	        android:paddingBottom="10dp"
	        android:paddingLeft="15dp"
	        android:textSize="12sp"
	        android:textColor="#676767"
	        android:text="詳細內容"
	        />
    </LinearLayout>
    
</LinearLayout>


MainActivity中:

public class MainActivity extends Activity {
	
	private ListView listview;
	
	private CommonAdapter adapter;
	
	private List<ItemBean> data;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		listview = (ListView)this.findViewById(R.id.listview);
		initData();
		adapter = new TextImgAdapter(this.getApplicationContext(), data, R.layout.item_listview);
		listview.setAdapter(adapter);
	}
	
	public void initData(){
		data = new ArrayList<ItemBean>();
		for(int i=0; i<20; i++){
			ItemBean bean = new ItemBean(R.drawable.ic_launcher, "標題"+i, "詳細內容"+i);
			data.add(bean);
		}
	}
}

可以看到,做了些更改,資料型別更改為了我們自定義的bean,bean中有三個屬性,分別每個ListViewItem中的頭像、標題、內容

ItemBean類:

public class ItemBean {
	
	private int imgid;
	private String title;
	private String detail;
	
	public ItemBean() {
		super();
		// TODO Auto-generated constructor stub
	}
	public ItemBean(int imgid, String title, String detail) {
		super();
		this.imgid = imgid;
		this.title = title;
		this.detail = detail;
	}
	public int getImgid() {
		return imgid;
	}
	public void setImgid(int imgid) {
		this.imgid = imgid;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getDetail() {
		return detail;
	}
	public void setDetail(String detail) {
		this.detail = detail;
	}

}


最後再看看我們的Adapter,由於現在的item佈局多了一個ImageView以及一個TextView,所以我們的Adapter相應得變成如下:

public class TextImgAdapter extends CommonAdapter<ItemBean>{
	
	public TextImgAdapter(Context context, List<ItemBean> list, int layoutId) {
		super(context, list, layoutId);
		// TODO Auto-generated constructor stub
	}

	@Override
	public void convert(CommonViewHolder viewHolder, ItemBean data, int position) {
		// TODO Auto-generated method stub
		ImageView item_iv = viewHolder.getView(R.id.item_iv);
		TextView titleTv = viewHolder.getView(R.id.titleTv);
		TextView detailTv = viewHolder.getView(R.id.detailTv);
		item_iv.setBackgroundResource(R.drawable.ic_launcher);
		titleTv.setText(data.getTitle());
		detailTv.setText(data.getDetail());
	}

}


只是多了幾行控制元件的生成以及設定值,清晰了很多有木有~~以後有再多的元素,依然只需先生成對應的例項,然後set值,一目瞭然。

執行結果:


成功實現我們的效果,媽媽再也不用擔心我寫Adapter寫到廢寢忘食.........