2009年6月2日 星期二

ListView跟ListActivity小考究

因為Goolge有隻範例程式NotePad,所以目前的書籍很多都會用這個當例子
裡面會用到ListActivity這東西,今天我想探討其相關議題,包括如後自訂List的格式
他會使用到BaseAdapter這東西

說到ListActivity呢,就要先談及Activiry,Activity是Android四大支柱之一
他是代表一個顯示在手機上的"畫面",每個程式都至少有一個Activity。
而ListActivity是從Actitivy延伸出去的,用來顯示列表。
但是已經有個ListView可以顯示列表了,為何還有個ListActivity,他跟ListView又有什麼不同
ListActivity簡單來說是一個包含ListView的Activity,他將ListView的功能從新包裝,扮演一個wrapper的角色,讓ListView更容易控制

ListActivity的部分原始碼

public class ListActivity extends Activity {

protected ListView mList;

private Handler mHandler = new Handler();
private boolean mFinishedStart = false;
.....

}


下面一個簡單的ListView範例


public class Note extends Activity {
String[] data=new String[]{"A","B","C"};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.main);
ListView list=(ListView)findViewById(R.id.ListView01);
ListAdapter ad=new ArrayAdapter (this,android.R.layout.simple_list_item_1,data);
list.setAdapter(ad);
list.setOnItemClickListener(new AdapterView.OnItemClickListener(){

@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,long arg3) {

TextView txt=null;
txt=(TextView)arg0.getChildAt((int)arg3);
}});
}

在上面程式中可以用findViewById找出我在main.xml設定的ListView,好處是名子沒有被綁死,如果用ListActivity的話,佈置(layout)檔的ListView名子會被鎖定,上面設定Row用了佈局android.R.layout.simple_list_item_1,他跟R.layout不一樣,他是android.R.layout開頭,是Android提供的預設排版,懶惰的時候可以直接套Android提供的資源,除了layout之外他還有提供各種圖示,如果想寫一些通訊錄之類的,可以使用他提供的android.R.drawable.sym_call_incoming的圖示。
ListView想處裡按下事件的話是使用 AdapterView.OnItemClickListener,他提供的函式並不容易處裡閱讀,所以ListActivity提供了更好的界面

ListActivity一點簡單的程式

public class Note extends ListActivity {
/** Called when the activity is first created. */
String[] data=new String[]{"A","B","C"};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.list);
getListView().setEmptyView(findViewById(R.id.empty));

Button b=(Button)findViewById(R.id.Button01);
b.setOnClickListener(new Button.OnClickListener(){

@Override
public void onClick(View v) {
fillData();
}});

}


private void fillData(){
ListAdapter ad=new ArrayAdapter(this,R.layout.notes_row,data);
setListAdapter(ad);
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
// TODO Auto-generated method stub
super.onListItemClick(l, v, position, id);
TextView txt=null;
txt=(TextView)l.getChildAt(position);
}

}

ListActivity的佈局檔,在此我取list,在ListActivity的佈局檔裡面最少要有一個ListView,而且名子必須是android:list,這是被綁死的,而且必須指定row的佈局,在此我用R.layout.notes_row,裡面只有一個TextView


<Listview android:id="@+id/android:list">

佈局檔

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>

<ListView android:id="@+id/android:list"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
></ListView>
<TextView
android:id="@+id/empty"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="No Data"
/>

<Button android:text="Get" android:id="@+id/Button01" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button>
</LinearLayout>




我們可以看一下ListActivity原始碼設局的部分

public void onContentChanged() {
super.onContentChanged();
View emptyView = findViewById(com.android.internal.R.id.empty);
mList = (ListView)findViewById(com.android.internal.R.id.list);
if (mList == null) {
throw new RuntimeException(
"Your content must have a ListView whose id attribute is " +
"'android.R.id.list'");
}
.....
}

如果ListActivity用的ListView名子不為android:list就會丟出RuntimeException的例外

ListActivity對於按下事件監聽是用onListItemClick(ListView l, View v, int position, long id) ,這1比ListView提供的更好使用且更清晰,裡面幾個主要的參數ListView l,得到ListView主體。position是代表被按下item的編號,ListView預設是只有TextView,所以ListView看起來像是個TextView陣列,如果想自定義ListView的View必須自己寫一個類別繼承BaseAdapter
而最後一個參數id可以跟SQLite配合,他會回傳SQLite裡面的_id的值

SQLite的部分,請去參閱手冊,在Android裡面SQLite資料表配合CursorAdapter一定得有個名叫"_id"的主鍵,Android幾個有關Cursor的Adapter
  1. CursorAdapter
  2. ResourceCursorAdapter
  3. SimpleCursorAdapter

其中SimpleCursorAdapter繼承ResourceCursorAdapter,ResourceCursorAdapter又去繼承CursorAdapter。

而如果想要自定ListView的型態的話,必須寫個類別繼承
比方說帶著圖片的ListView


首先,先看一下一般書上都有的範例,或者到官方的網站也可以看到
http://developer.android.com/guide/samples/NotePad/index.html

private void fillData(){
Cursor c=mdb.fetchAll();
startManagingCursor(c);
c.moveToFirst();
String[] from=new String[]{mdb.KEY_NOTE};
int[] to =new int[]{R.id.text};
SimpleCursorAdapter ad=new SimpleCursorAdapter(this,R.layout.notes_row,c,from,to);
setListAdapter(ad);
}

fillData是把資料庫的東西填到ListView,一般都會用SimpleCursorAdapter去轉接,但是這不能滿足我們,所以我照自己的方式修改了一下

private void fillData(){
Cursor c=mdb.fetchAll();
startManagingCursor(c);
IconAdapter icad=new IconAdapter(this,c);
setListAdapter(icad);
}

寫一個帶著圖片的轉接器,來看一下IconAdapter 的程式碼

static class IconAdapter extends BaseAdapter{

private Context _ctx;
private ArrayList notes;
private LayoutInflater mlin;
private Bitmap icon;
private Cursor _c;
public IconAdapter(Context ctx,Cursor c){...}

,@Override
public int getCount() {...}

@Override
public Object getItem(int arg0) {...}

@Override
public long getItemId(int arg0) {...}

@Override
public View getView(int pos, View view, ViewGroup arg2) {...}

static class Holder{
TextView text;
ImageView icon;
}
}

BaseAdapter有四個函式要複寫getCount、getItem、getItemId、getView,而且不能亂寫
因為我是要從資料庫取資料,所以我用了Cursor當參數,用ArrayList存他,如果有買『旗標:Android程式設計與應用』的話,他裡面的例子是用靜態陣列當資料,但是那不能滿足我們需求

一步一步解析

建構式

public IconAdapter(Context ctx,Cursor c){
_ctx=ctx;
_c=c;
mlin=LayoutInflater.from(ctx);
notes=new ArrayList();
int index=0;

for(int i=0;i<c.getColumnCount();i++)
{
if(c.getColumnName(i).equals(NoteDbAdapter.KEY_NOTE))
{
index=i;
}
}

c.moveToFirst();
for(int i=0;i<c.getCount();i++)
{
notes.add(c.getString(index));
c.moveToNext();
}

icon=BitmapFactory.decodeResource(
_ctx.getResources(),android.R.drawable.sym_call_incoming);
}

裡面只是一般的設值,Cursor的操作請參考上一篇http://hatsukiakio.blogspot.com/2009/06/cursorindexoutofboundsexception.html
而函式LayoutInflater.from(this),這是要把佈局檔從xml file轉成Layout物件,這之後會用到
而要將資源轉成Bitmap則必須使用BitmapFactory.decodeResource(res,resID);
第一個參數可以用Context.getResources()取得,第二個是圖片ID,在此我們使用系統的來電圖示

getCount()

public int getCount() {

return notes.size();
}

要複寫函式之一,主要是這個ListView共有幾筆資料,我直接回傳ArrayList的size

getItem(int arg0)

public Object getItem(int arg0) {
return getView(arg0, null, null);

要複寫函式之二,回傳第arg0筆資料的物件,在此我交給getView去做

getItemId(int arg0)

@Override
public long getItemId(int arg0) {

if(_c!=null)
{
//重要
_c.moveToPosition(arg0);
return _c.getLong(0);
}
else{
return 0;
}
}

要複寫函式之三,這個函式很重要,如果要希望ListView能夠執行SQLite的執行刪除等動作,這個函式一定要寫對,因為onListItemClick(ListView l, View v, int position, long id)第四個參數id就是從這個函式來的,參考我們上面說的,想要對資料庫作動作須仰賴這個屬姓
之前因為沒寫好這個函式,導致程式雖然顯示正常,但是資料庫動作都錯掉了
一開始我們用一個變數Cursor _c儲存了傳進來的資料庫指標,先判斷他是不是null,不是的話用moveToPosition()移到指定位置把該row的"_id"欄位抓出來回傳,在此我的"_id"欄是在第0個位置(getItem跟getItemId傳進來的參數都是被點選row的position)

getView(int pos, View view, ViewGroup arg2)

@Override
public View getView(int pos, View view, ViewGroup arg2) {

Holder holder=null;
if(view==null)
{
view=mlin.inflate(R.layout.icon, null);
holder=new Holder();
holder.icon=(ImageView)view.findViewById(R.id.icon);
holder.text=(TextView)view.findViewById(R.id.icontext);
view.setTag(holder);

}
else{
holder=(Holder)view.getTag();
}

holder.text.setText(notes.get(pos));
holder.icon.setImageBitmap(icon);
return view;
}

要複寫函式之四,整個BaseAdapter最核心的函式,沒有他連顯示都辦不到
他會先傳近三個參數(第pos個row,第pos個row的View物件,Viewgroup)
我們真正會用到的只有前兩個
在看程式碼之前先看看R.layout.icon的佈局檔,裡面有一個ImageView、一個TextView

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<ImageView
android:id="@+id/icon"
android:layout_width="35sp"
android:layout_height="35sp"
></ImageView>
<TextView
android:id="@+id/icontext"
android:gravity="center_vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
></TextView>
</LinearLayout>

而為了儲存這ImageView跟TextView,我宣告了一個資料結構

static class Holder{
TextView text;
ImageView icon;
}

一開始先宣告變數holder,然後看看現在這個位置的View是否產生過
如果沒有(view==null),先讓View產生一個Layout佈局的物件,透過inflate函式可以把xml file的佈局檔轉成View物件

view=mlin.inflate(R.layout.icon, null);

之後就可以用findViewById取得佈局檔裡面的ImageView跟TextView,之後用setTag把Holder這資料結構儲存,以方便之後會再用到

holder=new Holder();
holder.icon=(ImageView)view.findViewById(R.id.icon);
holder.text=(TextView)view.findViewById(R.id.icontext);
view.setTag(holder);

如果View已經產生過了,就可以用getTag把之前儲存的Holder取出來

holder=(Holder)view.getTag();

之後把內容設定好後就可以回傳了

holder.text.setText(notes.get(pos));
holder.icon.setImageBitmap(icon);
return view;

-----------------------
這邊在提一下取用的方法

SimpleCursorAdapter ad=new SimpleCursorAdapter(this,R.layout.notes_row,c,from,to);
setListAdapter(ad);
...
protected void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
TextView txt=(TextView)l.getChildAt(position);
...
}
}

在使用預設的SimpleCursorAdapter 時候,ListView的佈局只是一個TextView,可以直接用getChildAt(position)把內容抓出來

但是用了我們自己的IconAdapter的時候,要依據我們的佈局去抓,像是我用一個LinearLayout去包一個TextView,一個ImageView,所以用getChildAt(position)抓出來的是一個LinearLayout物件,要再去裡面抓想要的資料


IconAdapter icad=new IconAdapter(this,c);
setListAdapter(icad);
.........
protected void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
LinearLayout lay=(LinearLayout)l.getChildAt(position);
TextView txt=(TextView)lay.findViewById(R.id.icontext);
.........
}



感謝2F的意見,下面是修改後的程式碼
上面錯誤的部分我就留者對照比較了

IconAdapter icad=new IconAdapter(this,c);
setListAdapter(icad);
.........
protected void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
LinearLayout lay=(LinearLayout)l.getChildAt(position-l.getFirstVisiblePosition());
TextView txt=(TextView)lay.findViewById(R.id.icontext);
.........
}

2 則留言:

匿名 提到...

謝謝你的文章,對我幫助很大

匿名 提到...

關於文章最後面提到的取用的方法

我測試的時候有發生一些問題,當list的項目超過一個畫面的時候(也就是需要捲動),此時捲到底點下去很可能出錯。

LinearLayout lay = (LinearLayout) l.getChildAt(position-l.getFirstVisiblePosition());

建議將取位置的程式碼改成上面的