Android學習筆記(十九):建立自己的ListView

來源:互聯網
上載者:User

在之前的例子中,我們通過設定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開發相關文章

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.