在之前的例子中,我們通過設定adapter的getView()來編寫我們所希望的UI,然而在面向對編程中,我們希望能夠建立自己的ListView,例如類的名字為com.wei.android.learning.RatingView,只要在XML中用我們自己的RatingView對ListView來替代,就可以實現我們的風格,並前在原始碼中向使用ListView一樣簡單調用就可以了。
實現的目標
在Android XML檔案中,可以如下調用我們的RatingView:
<com.wei.android.learning.RatingView
<!--原來為ListView,現在指向我們自訂的ListView -->
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
在JAVA原始碼中,可以如同基礎的ListView一樣載入我們的RatingView
protected void onCreate(Bundle savedInstanceState) {
... ...
setContentView(R.layout......);
setListAdapter(new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,items));
}
而我們自己的RatingView,我們在每個List單元中的View前面會增設三星的RaingBar,後面可以普通的View,採用了TextView,和我們的上一次學習比較相似。為此,我們需要實現繼承ListView的類RatingView。下面的過程比之前的例子稍微複雜一點點,但是這種方式是我們所需的,可能重複利用我們自己的代碼,並將UI設計和程式的邏輯處理分離。
步驟1:構建我們的ListView,並指向我們自訂adapter
這個步驟將我們的ratingView的adapter(相關的UI定義)指向我們自訂的adapter
public class RatingView extends ListView{
//步驟1.1 重寫建構函式,我們不作特殊的處理,直接調用super的建構函式
public RatingView(Context context){
super(context);
}
public RatingView(Context context,AttributeSet attrs){
super(context,attrs);
}
public RatingView (Context context, AttributeSet attrs, int defStyle){
super(context ,attrs,defStyle);
}
//步驟1.2:通過設定adapter,綁帶我們自訂的adapter:RatenableWrapper,我們將通過該apdater來描繪List的UI結構
public void setAdapter(ListAdapter adapter){
super.setAdapter(new RatenableWrapper(getContext(),adapter));
}
}
步驟2:實現自訂的ListAdapter介面
我們先設定一個類用來儲存每個List元素的widget。每個List元素由兩個組成,一個是三星RatingBar,一個是我們通過layout Id傳遞過來的View
class ViewWrapper{
ViewGroup base;
View guts = null; //我們通過layout Id傳遞過來的View
RatingBar rate = null; //三星RatingBar
/* 建構函式,儲存ViewGroup*/
ViewWrapper(ViewGroup base){
this.base = base;
}
/*擷取View和設定View*/
RatingBar getRatingBar(){
if(rate == null)
rate = (RatingBar) base.getChildAt(0);
return rate;
}
void setRatingBar(RatingBar rate){
this.rate = rate;
}
/*擷取三星ratingbar和設定三星ratingbar*/
View getGuts(){
if(guts == null)
guts=base.getChildAt(1);
return guts;
}
void setGuts(View guts){
this.guts=guts;
}
}
我們去翻閱之前的例子,在程式中通過setListAdapter中將ListView綁定到某個adapter,將會調用到步驟1中的setAdapter(ListAdapter adapter),我們通過RatenableWrapper類具體實現ListAdapter介面。這是我們建立我們自己ListView的關鍵。
//步驟2:實現ListAdapter介面
private class RatenableWrapper implements ListAdapter {
//步驟2.1:看看setListAdapter(裡面的參數也是實現ListAdapter)以及setAdapter()的參數,我們需要儲存這個參數。
//Context:傳遞所顯示的Activity,這常會傳遞,當然也可以直接通過getContext()來獲得
//rates[]:記錄個三星RatingBar的每個的星數,針對我們這個例子設定
ListAdapter delegate = null;
Context context = null;
float[] rates = null;
//步驟2.2:實現建構函式,記錄相關的參數,並設定rates[]的初始值。
public RatenableWrapper(Context context,ListAdapterdelegate){
this.delegate = delegate;
this.context = context;
this.rates = new float[delegate.getCount()];
for(int i = 0; i < delegate.getCount(); i ++){
this.rates[i] = 2.0f;
}
}
//步驟2.3:實現ListAdapter的介面,如下,直接利用傳遞的參數delegate,這個參數也是ListAdapter的實作類別,我們將重點處理getView(),其他都直接調用delegate的處理。
public int getCount() {
return delegate.getCount();
}
public Object getItem(int position) {
return delegate.getItem(position);
}
public long getItemId(int position) {
return delegate.getItemId(position);
}
public int getItemViewType(int position) {
return delegate.getItemViewType(position);
}
public int getViewTypeCount() {
return delegate.getViewTypeCount();
}
public boolean hasStableIds() {
return delegate.hasStableIds();
}
public boolean isEmpty() {
return delegate.isEmpty();
}
public void registerDataSetObserver(DataSetObserver observer) {
delegate.registerDataSetObserver(observer);
}
public void unregisterDataSetObserver(DataSetObserver observer) {
delegate.unregisterDataSetObserver(observer);
}
public boolean areAllItemsEnabled() {
return delegate.areAllItemsEnabled();
}
public boolean isEnabled(int position) {
return delegate.isEnabled(position);
}
//步驟2.4:重點實現getView
public View getView(int position,View convertView,ViewGroup parent){
ViewWrapper wrap = null;
//ViewWrapper用於保留每個List元素的widget,我們在後面給出。
View row = convertView;
//步驟2.4.1:如果沒有建立過這個List單元的View,建立之。這個View分為左右兩部分,左邊只三星RatingBar,右邊是傳遞過來的View
if(convertView == null){
//步驟2.4.1.1:設定View,是水平擺放的LinearLayout,後面將row = layout;
LinearLayout layout = new LinearLayout(context);
layout.setOrientation(LinearLayout.HORIZONTAL);
//(1)第一部分是三星RatingBar,設定相關的屬性,
RatingBar rate = new RatingBar(context);
rate.setNumStars(3);
rate.setStepSize(1.0f);
rate.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.FILL_PARENT));
//(2)第二部分是傳遞過來的View,設定相關的屬性,
View guts = delegate.getView(position,null,parent);
guts.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.FILL_PARENT));
//(3)放置在LinearLayout上
layout.addView(rate);
layout.addView(guts);
//步驟2.4.1.2:設定三星RaingBar的觸發處理,在這個例子中,我們只是將點擊的星級存放在rates[]中,意思意思一下。
這需要將RatingBar這個widget和Index,也就是position捆綁,所以我們需要將ratingbar進行setTag。
RatingBar.OnRatingBarChangeListener l =
new RatingBar.OnRatingBarChangeListener() {
public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser) {
rates[(Integer)ratingBar.getTag()] = rating;
}
};
rate.setOnRatingBarChangeListener(l);
//步驟2.4.1.3:設定ListView的UI元素wrap,實現捆綁。
wrap = new ViewWrapper(layout);
wrap.setGuts(guts);
wrap.setRaingBar(rate);
layout.setTag(wrap);
//步驟2.4.1.4:回應步驟2.4.1.2,將ratingbar進行setTag()
rate.setTag(new Integer(position));
rate.setRating(rates[position]);
//步驟2.4.1.5,回應步驟2.4.1.1,對於row進行賦值
row = layout;
}else{ //步驟2.4.2:如果已經建立過這個List單元的View。如果我們增加Log.d進行跟蹤,我們會發現第一屏的8個list元素,都是需要建立的,但是如果scroll螢幕,後面的大多數的list元素,進入這個else分支。不清楚Android如何具體處理,它可以智能地根據原有的情況處理後面的list元素的UI,暫時想象為智能地處理了UI的布局,產生相應的widget,但是從程式的角度看,這些widget是沒有經過第一步的資料賦值,因此涉及非UI部分,安全地應當在此分支上進行再次賦值。這點需要注意。
wrap = (ViewWrapper)convertView.getTag();
//步驟2.4.2.1:傳遞了一個View,這個View也可能根據滾屏出現更新,我們同樣要對之進行處理
wrap.setGuts(delegate.getView(position,wrap.getGuts(),parent));
//步驟2.4.2.2:將Ratingbar和postiion進行捆綁(setTag),對Raingbar根據儲存在rates[]中的值設定星級,都需要重新設定
wrap.getRatingBar().setTag(new Integer(position));
wrap.getRatingBar().setRating(rates[position]);
}
return row;
}
}
步驟3:實驗一下
我們Android學習筆記(十七):再談ListView例子中的XML檔案的ListView修改為com.wei.android.learning.RatingView,如有圖所示。
討論問題1:如果觸發ListItemClick
在上面的main的程式,增加一個點擊出發機制,這在List中是非常常見的。如下:
getListView().setOnItemClickListener (new OnItemClickListener(){
public void onItemClick(AdapterView<?> parent, View view, int position, long id){
Toast.makeText(getApplicationContext(), items[position], Toast.LENGTH_SHORT).show();
}
});
我們嘗試驗擊,發現無法出發ItemList的點擊操作。ItemList是一個layout,裡面有一個widget和一個傳遞的View,widget和View都是可以出發點擊的動作,並且具有更好的優先順序別,所以無須。為瞭解決這個問題,我們在getView()中增加下面的處理:
layout.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
或者對layout中的每個view進行說明
guts.setFocusable(false);
rate.setFocusable(false);
由於我們對View的設定,採用的layout_width=wrap_content,這時我們發現點擊list item的空白是有效,但是點擊widget是無效的,可強制禁止widget監聽Click的事件來處理
guts.setClickable(false);
這樣整個View都是有效ListItemClick的監聽地區
討論問題2:如何同時處理內部widget觸發-擷取widget
舉個例子,我們在main activity中setListAdapter(new ArrayAdapter<String>(this,android.R.layout.simple_list_item_checked,items));item中也有checked。為了有更好的UI體驗,在getView中,我們設定guts的屬性layout_width是fill_parent。我們希望在按下ListItem的時候,該Item的Checked的狀態會改變。
在onItemClick()中參數View view實際是曾個ListItem,在這個例子中,即是getView中的layout/row。我們可以在RatingView(ListView)中增加一個函數,用於返回傳遞的View(即layout右邊的View),如下:
public View getMyView(View v){
ViewWrapper wrap = (ViewWrapper)v.getTag();
return wrap.getGuts();
}
對於android.R.layout.simple_list_item_checked,這個View的類型是CheckedTextView,可以使用setChecked()進行設定。看起來一起都沒有問題,但是我們發現點擊的時靈時不靈,而且其他的Item的check狀態莫名其妙會改變。引入下一個討論。
討論問題3:getView()的重新整理,需要注意什麼
我們在getView()中加入跟蹤的log,發現當我們點擊Item的時候,會觸發當前屏的getView進行重新整理。為了確保重新整理時不會改變,如同三星ratingbar,需要將item的check狀態保留,並重新設定,如同ratingbar。例如((CheckedTextView)wrap.getGuts()).setChecked(checks[position]);其中checkes[]我們用來儲存check的狀態。這樣整個顯示就正常了。我們在getView()對於具有狀態可能變更的widget,都需要進行重新整理。
等等,這種做法需要修改我們自訂的類,我們只知道要加三星ratingbar,我們並不能預置那個傳遞的View是什麼。這和我們的最初目標是偏離的。我們可以在對這個傳遞的View進行類型檢測getViewType,如果是CheckedTextView,則進行相關的操作。
回想一下啊Android的UI風格,其實手持終端的UI並不複雜,所以我們在實際上並無需如此擔心。
相關連結:我的Andriod開發相關文章