我们对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并不复杂,所以我们在实际上并无需如此担心。