Android学习笔记(十九):建立自己的ListView

来源:互联网 发布:hp1020网络打印机驱动 编辑:程序博客网 时间:2024/05/22 15:56

在之前的例子中,我们通过设置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开发相关文章

原创粉丝点击