ListView与RatingBar

我们对ListView做了进一步的探讨,然而给出的例子list中的元素可以有多个widget,并可灵活设置他们的值,但是这些widget之间缺乏互动,而且getView()的调用,需要重刷给list的entry,我们希望能够在entry中触发变化。

本次,我们继续根据《Beginging Android 2》的学习,结合RatingBar,将程序稍微复杂一点。RatingBar看用于媒体库的平级,我们用RatingBar取代了之前例子的图标,当RatingBar设置为三星时,该entry后面的文本改为大写,如果低于三星将恢复原来的小写显示。

例子:自定义数据结构和内部widget的触发处理

1)Android XML文件: 用RatingBar替代之前例子的ImageView

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout  ……>
  <RatingBar android:id="@+id/c85_rating"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:numStars = "3" <!-- 设置三星平级方式-->
    android:stepSize = "0.5"  <!--step为0.5,也就是允许2.5的星级评比 -->
    android:rating = "2"/>  <!-- 缺省为2星-->
  <TextView android:id="@+id/c85_label"
    android:paddingLeft="2px"
    android:paddingRight="2px"
    android:paddingTop="10px"
    android:textSize="24sp"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" />  
</LinearLayout>

2)设置自定制数据结构来存储信息,并提供查询信息的方法

在之前的例子中,我们使用了ArrayList<String>来存放每个单元的数据信息,在这个例子中,作为更通用的方式,每个单元信息为我们自定的类RowModel。

class RowModel{
    String label;           //存储entry的当前文本显示内容,通过调用toString()给出,如果三星将提供大写显示。
    float rating = 2.0f; //存储entry的星级数据,对应RatingBar的星级显示
    
    RowModel(String label){
        this.label = label;
    }
    public String toString(){
        if(rating >= 3.0){
            return label.toUpperCase();
        }
        return label;
    }
}

在我们的主类中,根据自定义的数据结构设置我们的数据信息list,并导入list adapter中,同时我们增加一个方法,根据position(index)来从数据信息中获取该单元的数据。

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ArrayList<RowModel> list = new ArrayList<RowModel>(); //步骤1:list作为数据的存储
    for(String s: items){ //步骤2:将String[] items的信息导入list中,这种写法比较特别,我一般会老老实实for(int i =0; i <items.length; i++)的方式来写。 
        list.add(new RowModel(s));
    }
    setListAdapter(new RatingAdapter(list)); //步骤3:设置自定制的listadapter(具体在后面处理),并将信息数据list导入其中

//根据List的位置,获得具体的list元素,一般add,del,find的处理中,相当于find 
private RowModel getModel(int position){
    return ((RatingAdapter)getListAdapter()).getItem(position);
}

3)List单元的View和widget信息捆绑,实现快速定位widget

根据之前的学习,为了使程序运行得更有效率,我们会使用setTag的方式,将list单元的UI的View和存储单元UI中widget信息的类捆绑,以便可以快速定位widget。

步骤1:设置存储List单元View中widget的相关类。

其实,我们可以将这些widget信息和2)中的数据信息放在一起,在这个例子中程序会更借鉴,但是这样的处理很不好,我们尽可能把要将UI相关的信息和数据信息放在一起,否则UI修改或者进行尺寸适配时出现麻烦。

private class ViewWrapper{
    View base;
    RatingBar rate = null;
    TextView label = null;
    
    ViewWrapper(View base){
        this.base = base;
    }
    
    RatingBar getRatingBar(){
        if(rate == null)
            rate =(RatingBar) base.findViewById(R.id.c85_rating);
        return rate;
    }
    
    TextView getLabel(){
        if(label == null)
            label = (TextView)base.findViewById(R.id.c85_label);
        return label;
    }
}

步骤2:List单元View的呈现(getView),并且提供其中widget触发的处理

一个List单元的View对应两个内容,一个是存储的数据,可以通过getModel来获得,另一个是对应的单元UI的widget队形的存储,通过getTag()和setTag(),这个在上一次学习中已经学习了,我们还需要增加View中widget的触发,在这个例子中,当RatingBar的星级出现变化是,可能需要重写刷新后面文章的显示。我们具体看代码:

private class RatingAdapter extends ArrayAdapter<RowModel>{
        //步骤2.1:设置构造函数,将数据信息放入ArrayAdapter中,这样可以通过getItem() 获取数据信息,同时也设置layout格式
        RatingAdapter(ArrayList<RowModel> list){
            super(Chapter8Test5.this,R.layout.entry,list);
        }

        //步骤2.2: 编写ListView中每个单元的呈现
        public View getView (int position, View convertView, ViewGroup parent) {
            View row = convertView;
            ViewWrapper wrapper;
            RatingBar ratebar = null;
 
            //步骤2.3:如果没有创建View,根据layout创建之,并将widget的存储类的对象与之捆绑为tag 
            if(row == null){ 
                LayoutInflater inflater=getLayoutInflater();
                row = inflater.inflate(R.layout.entry, parent,false);
                wrapper = new ViewWrapper(row);
                row.setTag(wrapper);
                //步骤2.4:在生成View的时候,添加将widget的触发处理 
                ratebar = wrapper.getRatingBar();
                ratebar.setOnRatingBarChangeListener (new RatingBar.OnRatingBarChangeListener () {
                    public void onRatingChanged (RatingBar ratingBar, float rating, boolean fromUser) {
                        //步骤2.4.1:存储变化的数据 
                        Integer index = (Integer)ratingBar.getTag();
                        RowModel model = getModel(index);
                        model.rating = rating;
                        //步骤2.4.2:设置变化 
                        LinearLayout parent = (LinearLayout)ratingBar.getParent();
                        TextView label = (TextView)parent.findViewById(R.id.c85_label);
                        label.setText(model.toString());
                    }
                });
            }else{ //步骤2.4:利用已有的View,获得相应的widget 
                wrapper = (ViewWrapper) row.getTag();
                ratebar = wrapper.getRatingBar();
            }
            //步骤2.5:设置显示的内容,同时设置ratingbar捆绑tag为list的位置,因为setTag()是View的方法,因此我们不能降至加在ViewWrapper,所以需要加载ViewWrapper中的widget中,这里选择了ratebar进行捆绑。
            RowModel model= getModel(position);
            wrapper.getLabel().setText(model.toString());
            ratebar.setTag (new Integer(position));
            ratebar.setRating(model.rating);
            return row;
        }        
    }

我们在这里例子中进行了一个实验,考察什么时候convertView可以为null,一屏可以显示0-8个row,这些list的元素都是null,需要通过程序来创建,然而当我混动屏幕的时候,我想象中,后面的元素第一次也应该为0,但是出乎我的意外,只有position=14的出现row=null。对于通过scroll屏幕的情况,下一屏Android可能根据第一屏对UI的处理情况进行了处理。 因此Android对UI的智能处理情况我们不太能把握,因此任何与数据有关,不是纯粹的UI问题的初始赋值的问题,不要只放置在if(row==null)中进行初始处理,否则会引起不可预测的意外 。例如我们将步骤2.5中的ratebar.setTag(new Integer(position))此句放在if(row==null)会得到不正常的结果,因为不是所有的list元素中的该widget都在初始的情况下成功进行了捆绑,所以我们将它放置在外面或者通知方式在if和else的判断中,保证所有情况都覆盖。

ListAdapter:CursorAdapter

一般来讲,我们可以使用ArrayAdapter来适用很多情况,还有其他的Adapter,使用方式类似,但是CursorAdapter有些不一样,通过newView()和bindView(),如果没有创建,使用newView(),然后调用bindView(),如果已经创建,使用bindView()。



在之前的例子中,我们通过设置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 Chapter8RatingView(Context context){
        super(context);
    }
    public Chapter8RatingView(Context context,AttributeSet attrs){
        super(context,attrs);
    }
    public Chapter8RatingView (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,ListAdapter delegate ){
            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并不复杂,所以我们在实际上并无需如此担心。

你可能感兴趣的:(android,ListView,list,layout,null,Integer)