閱讀781 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Android 適配器教程(四)

之前我們學習了什麼是適配器,並且三種常用的安卓原生適配器也講完了,接下來我們就要自定義適配器了,自定義的適配器能適應更多的情況,功能更加強大,當然也需要我們更加深入的學習才能應用自如。


終於到自己寫一個適配器的時候了!


我準備了兩個例子,一個簡單一些,一個複雜一些,這次先看個簡單的:

我還是繼續在前三次的Demo項目上繼續添加例子,最後一篇的時候把源碼分享給大家~

讓我們繼續一步步寫下去。


這個例子是在ListView上麵添加按鈕,具體來說是顯示一個按鈕和一個圖片,兩行字。這個小問題涉及到的知識挺多的。也許你會想:添加按鈕首先要寫一個有按鈕的xml文件,然後用教程(三)的方法定義一個適配器,然後將數據映射到布局文件上。但是事實並非這樣,因為按鈕是無法映射的,即使你成功的用布局文件顯示出了按鈕也無法添加按鈕的響應,這時就要研究一下ListView是如何現實的了,而且必須要重寫一個類繼承BaseAdapter



首先我們先了解一下自己寫適配器的原理:


第一步要首先重寫一個類繼承BaseAdapter


先讓我們看一下各個方法:

 

(1)首先是getCount()方法,這個方法要返回你要添加進ListView裏的東西的總數,

也就是要告訴ListView,我添加進列表裏的東西有多少,需要多長的列表。

這裏的mArray就是一個簡單的List<String>,

可能有人會問,為什麼不直接返回mListView的長度呢?

原因就是我們的ListView可能會添加”頭“和”尾“,來進行一些更新之類的交互,

就像微博之類的下拉刷新或者到底後加載,所以幹脆直接用我們添加的內容的長度


@Override   

public int getCount() {  

return mArray == null ? 0 : mArray.size();

}  

 

 

(2)接下來就是getItem(int position)方法

ListView要加載內容,要獲得內容才可以加載!

這個方法就是要讓ListView可以通過一個position來獲得我們要添加在相應位置的內容的

內容是什麼?當然是剛才mArray裏相應位置的東西啦!


@Override  

public Object getItem(int position) {  

    return mArray.get(position);  

}  

 

 

(3)然後是getItemId(int position)方法,這個方法應該是為了方便ListView進行管理的,

簡單說,我們就按原來的position來讓他管理,原本是幾就是幾,省事,直接返回position!


@Override  

public long getItemId(int position) {  

    return position;  

}  

 

最後,重頭戲!getView()方法!

這裏要實現的東西就比較多了

這個也很好理解,個人的理解就是ListView要方便的得到自己裏麵的每個View

不然人家怎麼知道你的mArray裏的數據,要怎麼填入ListView裏的每個View

 

@Override 

        public View getView(int position, View convertView, ViewGroup parent) {  

            return null;  

        }  


這裏麵一般我們會怎麼做呢?

一般,我們添加到ListView裏的每個View都是xml定義好的。

一開始需要構造過來一個Context!

通過LayoutInflater.from(context).inflate(R.layout.你定義的xml,null);

獲得你要添加進去的View來賦給convertView

如果我們定義的xml裏有一個TextView

那我們就TextView tTextView = (TextView)convertView.findViewById(R.id.你的textview);

這樣就可以通過position,在mArray裏找到我們相應位置的內容,讓TextView顯示出來

當然,最後要return convertView

把這個我們包裝好的View給回ListView,讓它在列表裏顯示。

下麵是具體實現的過程:

項目開始:

也還是先在activity_main.xml裏添加一個button,一會跳轉的時候使用。

然後新建一個類MyAdapterDemo繼承自Activity作為我們第四個個例子的Activity,@Override 我們的onCreate方法。

新建一個xml文件myadapterdemo.xml作為我們的布局文件,其中也是包含一個文本域和一個ListView:


myadapterdemo.xml:

代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:andro
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="這是myadapter的一個例子" >
    </TextView>

    <ListView
        android:
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" >
    </ListView>

</LinearLayout>

然後需要定義好一個用來顯示每一個列內容的xml

Listitem2.xml 包含橫向的圖片與文字還有一個button,

Listitem2.xml:

代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:andro
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal" >

    <ImageView
        android:
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5px" />

    <TextView
        android:
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5px"
        android:textColor="#000000"
        android:textSize="22px" />

    <TextView
        android:
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5px"
        android:textColor="#000000"
        android:textSize="15px" />

    <Button
        android:
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="more" />

</LinearLayout>

自定義適配器:

 

新建一個類MyAdapter繼承自BaseAdapter,這時候Eclipse會提示你Override默認的方法,點擊之後就會出現上麵我所說的那些方法了。

之後創建一個ViewHolder,具體原因我會在下一次教程中仔細說明

ViewHolder:

public final class ViewHolder{
		public ImageView img;
		public TextView text3;
		public TextView text4;
		public Button viewBtn;
	}
把ViewHolder作為內部類放在最後等著,一會就有用了。


構造器

然後我們再研究下我們需要外部的什麼東西,也就是構造器需要被傳進什麼參數。

我們類比一下之前原生的適配器的構造方法,自然而然的想到他們都有一個Context,同時都需要傳參數!

所以我們的MyAdapter自然也不例外了~

在之前的說明中說過,一開始需要構造過來一個Context!

隻有這樣才能通過LayoutInflater.from(context).inflate(R.layout.你定義的xml,null);

獲得你要添加進去的View來賦給convertView。

所以構造器以及必要的對象就要這樣寫:

	private LayoutInflater mInflater;
	private List<Map<String, Object>> data;
//	構造器,接收數據
	public MyAdapter(Context context, List<Map<String, Object>> data){
		this.mInflater = LayoutInflater.from(context);
		this.data = data;
	}

注意這有個LayoutInflater我給大家解釋一下

在實際開發中LayoutInflater這個類還是非常有用的,它的作用類似於findViewById()。不同點是LayoutInflater是用來找res/layout/下的xml布局文件,並且實例化;而findViewById()是找xml布局文件下的具體widget控件( ButtonTextView)
具體作用:
1
、對於一個沒有被載入或者想要動態載入的界麵,都需要使用LayoutInflater.inflate()來載入;

2、對於一個已經載入的界麵,就可以使用Activiyt.findViewById()方法來獲得其中的界麵元素。

這樣我們從構造器中得到了數據data還有context(通過LayoutInflater得到布局文件


之後我們隻要把@Override的方法填寫完整就好了,希望這時候你還沒有忘記開始時我講過的基本知識。

不過沒關係,我會一點點講清楚的:


第一個方法getCount()

是得到長度,所以外部數據有所少就要有多長,所以返回的是data==null?0:data.size();

也就是如果外麵傳過來的數據為空,那麼長度為0,不是空,長度就是數據的數量,

注意:這裏非常有必要處理一下data==null這種情況!很多代碼這裏都沒做處理,這是很不好的習慣。

所以這個方法填好了:

@Override
public int getCount() {

return data == null? 0:data.size();
}


第二個方法getItem(int position)

之前也說過了,這個方法就是要讓ListView可以通過一個position來獲得我們要添加在相應位置的內容的

內容是什麼?當然是data裏相應位置的東西啦!

所以這個方法也填好了:

@Override
public Object getItem(int position) {

return data.get(position);
}


第三個方法getItemId(int position)

這個方法應該是為了方便ListView進行管理的,

沒有什麼特殊需求的話,我們就按原來的position來讓他管理,position原本是幾就是幾,直接返回position


@Override
public long getItemId(int position) {

return position;
}



這樣的話就隻剩下一個大頭了:

千唿萬喚始出來的

public View getView(int position, View convertView, ViewGroup parent)

寫到這我發現還是不得不先解釋一下ViewHolder了,我先粗略的盡量讓大家理解,因為下一講的主題就是它!

咱們一點點的分析,注意看返回值的類型,是一個View,不難想象這就是返回了List裏麵一個Item的View,Android中有個叫做Recycler(反複循環器)的構件,ListView的加載原理是這樣的:

有一個item出了屏幕,在空出來的部分是由出屏幕的那個item通過適配器的getView方法改變成了新的item加以填充的。所以
1.如果你有10億個項目(item),其中隻有可見的項目存在內存中,其他的在Recycler中
2.初始化的時候,ListView先請求一個type1視圖(getView),然後請求其他可見的項目。這時conVertView在getView中是null的
3.當item1滾出屏幕,並且一個新的項目從屏幕地段上來時,ListView再請求一個type1視圖。convertView此時不是空值了,它的值是item1.你隻需要設定新的數據返回convertView,不必重新創建一個視圖。這樣直接使用convertView從而減少了很不不必要view的創建
 而更快的方式是定義一個ViewHolder,將convertView的tag設置為ViewHolder,不為空是重新使用
 (Tip:View中的setTag(Onbect)表示給View添加一個格外的數據以後可以用getTag()將這個數據取出來。)

這個問題先說到這,下一講我們在仔細討論,這樣對getView有所理解了吧

我直接貼代碼,然後大家看注釋就好了:

@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		ViewHolder holder = null;			//首先定義一個ViewHolder對象
		if (convertView == null) {			//當初始化的時候 convertView是空的
			
			holder=new ViewHolder();		//創建一個ViewHolder
			
			//通過LayoutInflater用來找res/layout/下的xml布局文件,並且實例化,這樣convertView的界麵就有了。
			convertView = mInflater.inflate(R.layout.listitem2, null);
			//使用Activiyt.findViewById()方法來獲得其中的界麵元素。
			holder.img = (ImageView)convertView.findViewById(R.id.imgview2);
			holder.text3 = (TextView)convertView.findViewById(R.id.text3);
			holder.text4 = (TextView)convertView.findViewById(R.id.text4);
			holder.viewBtn = (Button)convertView.findViewById(R.id.view_btn);
			//將holder對象作為標簽添加到View上
			convertView.setTag(holder);
			
		}else {
			//不為空的時候直接重新使用convertView從而減少了很多不必要的View的創建
			holder = (ViewHolder)convertView.getTag();
		}
		
			//然後加載數據
		holder.img.setBackgroundResource((Integer)data.get(position).get("imgview2"));
		holder.text3.setText((String)data.get(position).get("text3"));
		holder.text4.setText((String)data.get(position).get("text4"));
		
			//為按鈕加監聽
		holder.viewBtn.setOnClickListener(new View.OnClickListener() {
			
			@Override
			public void onClick(View v) {
				showInfo();					
			}
		});
		
		
		return convertView;
	}

這樣大家就比較理解了吧,至於ViewHolder和Tag,下次會好好解釋的~
 
下麵是MyAdapter的完整代碼
package com.example.adapterdemo;

import java.util.List;
import java.util.Map;

import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;

public class MyAdapter extends BaseAdapter{
	
	private LayoutInflater mInflater;
	private List<Map<String, Object>> data;
	private Context c;
//	構造器,接收數據
	public MyAdapter(Context context, List<Map<String, Object>> data){
		this.c = context;
		this.mInflater = LayoutInflater.from(context);
		this.data = data;
	}

	@Override
	public int getCount() {
		
		return data == null? 0:data.size();
	}

	@Override
	public Object getItem(int position) {
		
		return data.get(position);
	}

	@Override
	public long getItemId(int position) {
		
		return position;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		ViewHolder holder = null;			//首先定義一個ViewHolder對象
		if (convertView == null) {			//當初始化的時候 convertView是空的
			
			holder=new ViewHolder();		//創建一個ViewHolder
			
			//通過LayoutInflater用來找res/layout/下的xml布局文件,並且實例化,這樣convertView的界麵就有了。
			convertView = mInflater.inflate(R.layout.listitem2, null);
			//使用Activiyt.findViewById()方法來獲得其中的界麵元素。
			holder.img = (ImageView)convertView.findViewById(R.id.imgview2);
			holder.text3 = (TextView)convertView.findViewById(R.id.text3);
			holder.text4 = (TextView)convertView.findViewById(R.id.text4);
			holder.viewBtn = (Button)convertView.findViewById(R.id.view_btn);
			//將holder對象作為標簽添加到View上
			convertView.setTag(holder);
			
		}else {
			//不為空的時候直接重新使用convertView從而減少了很多不必要的View的創建
			holder = (ViewHolder)convertView.getTag();
		}
		
			//然後加載數據
		holder.img.setBackgroundResource((Integer)data.get(position).get("img"));
		holder.text3.setText((String)data.get(position).get("text3"));
		holder.text4.setText((String)data.get(position).get("text4"));
		
			//為按鈕加監聽
		holder.viewBtn.setOnClickListener(new View.OnClickListener() {
			
			@Override
			public void onClick(View v) {
				showInfo();					
			}
		});
		
		
		return convertView;
	}
	
	
	public final class ViewHolder{
		public ImageView img;
		public TextView text3;
		public TextView text4;
		public Button viewBtn;
	}
	/**
	 * listview中點擊按鍵彈出對話框
	 */
	public void showInfo(){
		new AlertDialog.Builder(c)
		.setTitle("我的listview")
		.setMessage("介紹...")
		.setPositiveButton("確定", new DialogInterface.OnClickListener() {
			@Override
			public void onClick(DialogInterface dialog, int which) {
			}
		})
		.show();
		
	}
	
}


之後我們再回到MyAdapterDemo向之前那樣增加適配器,添加數據就好了,在按鈕的處理上也比較簡單,我偷下懶,先貼代碼後解釋~

MyAdapterDemo的完整代碼如下:

package com.example.adapterdemo;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;


public class MyAdapterDemo extends Activity {
	private ListView lv;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.myadapterdemo);
		lv = (ListView) findViewById(R.id.myadapterlistview);
		MyAdapter myadapter = new MyAdapter(this,getData());
		lv.setAdapter(myadapter);
		
	}
	
	private List<Map<String, Object>> getData() {
		List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();

		Map<String, Object> map = new HashMap<String, Object>();
		map.put("text3", "Image 1");
		map.put("text4", "info 1");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);

		map = new HashMap<String, Object>();
		map.put("text3", "Image 2");
		map.put("text4", "info 2");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);

		map = new HashMap<String, Object>();
		map.put("text3", "Image 3");
		map.put("text4", "info 3");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image 4");
		map.put("text4", "info 4");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image 5");
		map.put("text4", "info 5");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image 6");
		map.put("text4", "info 6");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image 7");
		map.put("text4", "info 7");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image 8");
		map.put("text4", "info 8");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image 9");
		map.put("text4", "info 9");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image10");
		map.put("text4", "info10");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image11");
		map.put("text4", "info11");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image12");
		map.put("text4", "info12");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image13");
		map.put("text4", "info13");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image14");
		map.put("text4", "info14");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		return list;
	}
	
}

讓我們再總結一下工作原理listView在開始繪製的時候,係統首先調用getCount()函數,根據他的返回值得到listView的長度,然後根據這個長度,調用getView()逐一繪製每一行。如果你的getCount()返回值是0的話,列表將不顯示同樣return1,就隻顯示一行。

  係統顯示列表時,首先實例化一個適配器(這裏將實例化自定義的適配器)。當手動完成適配時,必須手動映射數據,這需要重寫getView()方法。係統在繪製列表的每一行的時候將調用此方法。getView()有三個參數,position表示將顯示的是第幾行,covertView是從布局文件中inflate來的布局。我們用LayoutInflater的方法將定義好的listitem.xml文件提取成View實例用來顯示。然後將xml文件中的各個組件實例化(簡單的findViewById()方法)。這樣便可以將數據對應到各個組件上了。但是按鈕為了響應點擊事件,需要為它添加點擊監聽器,這樣就能捕獲點擊事件。至此一個自定義的listView就完成了。

現在讓我們回過頭從新審視這個過程。係統要繪製ListView了,他首先獲得要繪製的這個列表的長度,然後開始繪製第一行,怎麼繪製呢?調用getView()函數。在這個函數裏麵首先獲得一個View(實際上是一個ViewGroup),然後再實例並設置各個組件,顯示之。好了,繪製完這一行了。那 再繪製下一行,直到繪完為止。

如果需要ListView也加入監聽,在實際的運行過程中會發現listView的每一行沒有焦點了,這是因為Button搶奪了listView的焦點,隻要布局文件中將Button設置為沒有焦點應該就OK了。

最後我們看一下實現效果圖:

到此為止,我們的學習之旅又進了一大步,自定義適配器這一部分需要好好的進行理解,隻有真正理解的比較透徹,寫起來才會比較順手,學會和精通是不一樣的,僅僅是學會就隻能實現一些簡單的功能,而學精才能推陳出新,創造出更有影響力的項目。

下一講我會對這一講留下的幾個問題進行詳細的分析,比如Holder,tag的詳解,爭取讓大家理解的更加透徹一些,請繼續關注~

源代碼我會在最後一講的最後附上鏈接,因為我也是邊寫博客邊碼代碼,我覺得這樣思路比較清楚一些。
我也還是個學生,水平有限,還請大家多多指教~


最後更新:2017-04-03 05:39:34

  上一篇:go 關於“卷積”的通俗解釋
  下一篇:go 網頁搜索之後的APP搜索