Android学习笔记(十九):建立自己的ListView
在之前的例子中,我们通过设置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开发相关文章