现在的位置: 首页 > 综合 > 正文

Android listview 利用反射的自动绑定Adapter

2014年08月29日 ⁄ 综合 ⁄ 共 10625字 ⁄ 字号 评论关闭

获取源代码 git clone https://github.com/coderebot/ListViewAutoAdapter.git 或者使用svn co https://github.com/coderebot/ListViewAutoAdapter

网上有很多介绍ListView的用法,大多涉及了Adapter。Android提供的Adapter主要有ArrayAdapter, SimpleAdapter, SimpleCursorAdapter。当然,也介绍了如何自己从BaseAdapter继承的方法。

但是,这些文章介绍的方法距离实际使用还是很有距离的,基本处于练手的级别。对于很多开发者来说,实现一个功能复杂、效率高的ListView还是有一点难度。

我根据自己的编程经验,结合网上的介绍,利用java的反射原理,提供一种使用更加简便、效率更高、控制更加灵活的方法。

我的灵感来源自SimpleAdapter。使用过SimpleAdapter的朋友都知道,SimpleAdapter是一种使用简单而功能强大的Adapter,可以实现大多数我们已知的ListView类型。但是SimpleAdapter还是存在很多问题:

  1. 它使用Map对象保存一条数据,空间和时间效率都不高;
  2. SimpleAdapter使用的数据和原始数据是不能直接共享,必须生成一个新的List<Map>对象
  3. View和数据的映射是固定的,不能根据我们的需要做调整,比如,如果数据是一个星期类型,就不能直接提供给View来显示。

我通过实现一个AutoBinderAdapter来解决以上遇到的问题。

为了便于说明,请先看截图:

代码下载可以到:http://download.csdn.net/detail/doon/6819483

Activity的布局很简单,就是一个listview: (activity_main.xml)

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <ListView
        android:id="@+id/listView1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true" >

    </ListView>

</RelativeLayout>

listitem的布局也非常简单:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal" >

    <ImageView 
        android:id="@+id/img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="3dp"
        />
    <LinearLayout 
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        >

        <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:orientation="horizontal" >

		        <TextView
		            android:id="@+id/title"
		            android:layout_width="fill_parent"
		            android:layout_height="fill_parent"
		            android:layout_gravity="left"
		            android:layout_weight="1"
		            android:textSize="16sp" />
		        
		            <ImageView
		                android:id="@+id/img_constellation"
		                android:layout_width="32dp"
		                android:layout_height="32dp"
		                android:layout_gravity="right"
		                android:layout_margin="1dp" />
		        
		</LinearLayout>

        <TextView
            android:id="@+id/info"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:textSize="10sp" />

    </LinearLayout>

</LinearLayout>

下面看Activity的代码

package org.liview.listviewadpter;

import org.liview.listviewadpter.R;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.widget.ListView;
import java.util.List;
import java.util.ArrayList;
import android.view.View;
import android.widget.ImageView;

public class MainActivity extends Activity {

	private ListView mListView;
	//这里定义了数据结构
	public static class Beauty {
		public String name;
		public int    headIcon;
		public String info;
		public int    constellation; //星座
		
		public static final int Aquarius = 0; 
		public static final int Pisces = 1;
		public static final int Aries = 2;
		public static final int Taurus = 3;
		public static final int Gemini = 4;
		public static final int Cancer = 5;
		public static final int Leo = 6;
		public static final int Virgo = 7;
		public static final int Libra = 8;
		public static final int Scorpio = 9;
		public static final int Sagittarius = 10;
		public static final int Capricorn = 11;
		
		public Beauty(String name, int head, String info, int cons) {
			this.name = name;
			this.headIcon = head;
			this.info = info;
			this.constellation = cons;
		}
	}
	
	private List<Beauty> mBeauties;
	
	private List<Beauty> getData() {
		if(mBeauties != null)
			return mBeauties;
		
		mBeauties = new ArrayList<Beauty>();
		mBeauties.add(new Beauty("貂蝉", R.drawable.mv1, "东汉帝国小姐冠军", Beauty.Aquarius));
		mBeauties.add(new Beauty("王昭君", R.drawable.mv2, "和亲大使", Beauty.Capricorn));
		mBeauties.add(new Beauty("西施", R.drawable.mv3, "越国美女007", Beauty.Leo));
		mBeauties.add(new Beauty("杨贵妃", R.drawable.mv4, "最不担心体重的美丽女人", Beauty.Gemini));
		mBeauties.add(new Beauty("苍井空", R.drawable.mv5, "屌丝女神", Beauty.Aries));
		mBeauties.add(new Beauty("赫本", R.drawable.mv6, "最有公主气质的女人", Beauty.Sagittarius));
		
		return mBeauties;
	}
	
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		mListView = (ListView)findViewById(R.id.listView1);
		
<span style="white-space:pre">		</span>//这里是关键,实现adpater的地方
		try{
			mListView.setAdapter(
					new AutoBindAdapter<Beauty>(
							this,
							R.layout.listitem,
							getData(),
							new AutoBindAdapter.AutoBinder()
								.add(R.id.img, Beauty.class.getField("headIcon"))
								.add(R.id.title, Beauty.class.getField("name"))
								.add(R.id.info, Beauty.class.getField("info"))
								.add(R.id.img_constellation, Beauty.class.getField("constellation"),
										new AutoBindAdapter.Binder() {
											public void setView(View view, Object obj) {
												ImageView img = (ImageView)(view);
												int resId = 0;
												int constellation = (Integer)obj;
												switch(constellation){
												case Beauty.Aquarius:    resId = R.drawable.aquarius; break;
												case Beauty.Pisces:      resId = R.drawable.pisces; break;
												case Beauty.Aries:       resId = R.drawable.aries; break;
												case Beauty.Taurus:      resId = R.drawable.taurus; break;
												case Beauty.Gemini:      resId = R.drawable.gemini; break;
												case Beauty.Cancer:      resId = R.drawable.cancer; break;
												case Beauty.Leo:         resId = R.drawable.leo; break;
												case Beauty.Virgo:       resId = R.drawable.virgo; break;
												case Beauty.Libra:       resId = R.drawable.libra; break;
												case Beauty.Scorpio:     resId = R.drawable.scorpio; break;
												case Beauty.Sagittarius: resId = R.drawable.sagittarius; break;
												case Beauty.Capricorn:   resId = R.drawable.capricorn; break;
												default: return;
												}
												img.setImageResource(resId);
											}
										}
									)
								
					)
			);
		}catch(Exception e)
		{
			e.printStackTrace();
		}
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// Inflate the menu; this adds items to the action bar if it is present.
		getMenuInflater().inflate(R.menu.main, menu);
		return true;
	}

}

首先,我定义了一个内部类Beauty,表示美女数据,包括name, headIcon, info, constellation四个信息。这些信息都将要显示在ListView里面。

请大家注意,这里我通过定义class Beauty来表示数据,而不是像SimpleAdapter那样,必须定义一个包含'name","headIcon","info"和"constellation" 的HashMap对象。因为在通常情况下,开发者为了效率和方便访问数据,都会将数据声明为一个class类型。

对于ListView来说,Beauty就是一个原始类型。

下面,我们通过AutoBinderAdapter来让ListView直接使用,请看下面的分解代码:

			mListView.setAdapter(
					new AutoBindAdapter<Beauty>(
							this,
							R.layout.listitem,
							getData(),
							new AutoBindAdapter.AutoBinder()....
								
					)

AutoBindAdapter类的构造函数接受4个参数:context, 资源id,List数据和一个Binder。 其他三个同ArrayBinder类,第四个Binder对象是负责将数据和ListItem的View结合在一起的。我们先看一下:

new AutoBindAdapter.AutoBinder()
	.add(R.id.img, Beauty.class.getField("headIcon"))
        ....

AutoBindAdapter.AutoBinder类有一个add方法,其返回值是它自己,因此我们可以连续调用,就像火车头带车厢一样,可以带N多个add调用。

add方法有两个重载,上面的例子是一个简单的方法:接受一个id(ListItem中的子id),和一个Field对象,通过Beauty.class.getField的反射方法直接获取。

它的工作原理是:1. 通过id,找到对应子view;2. 通过Field的get方法,从Beauty对象中取得对应的属性值;3. 把获取的属性值设置给对应的View。

在默认情况下,我们根据View的类型和Field的类型,自动匹配数据和View,但是,AutoBindAdapter也允许开发者提供自定义的设置类型:

.add(R.id.img_constellation, Beauty.class.getField("constellation"),
<span style="white-space:pre">	</span>new AutoBindAdapter.Binder() {
	<span style="white-space:pre">	</span>public void setView(View view, Object obj) {
			ImageView img = (ImageView)(view);
			int resId = 0;
			int constellation = (Integer)obj;
			switch(constellation){
			case Beauty.Aquarius:    resId = R.drawable.aquarius; break;
			case Beauty.Pisces:      resId = R.drawable.pisces; break;
			case Beauty.Aries:       resId = R.drawable.aries; break;
			case Beauty.Taurus:      resId = R.drawable.taurus; break;
			case Beauty.Gemini:      resId = R.drawable.gemini; break;
			case Beauty.Cancer:      resId = R.drawable.cancer; break;
			case Beauty.Leo:         resId = R.drawable.leo; break;
			case Beauty.Virgo:       resId = R.drawable.virgo; break;
			case Beauty.Libra:       resId = R.drawable.libra; break;
			case Beauty.Scorpio:     resId = R.drawable.scorpio; break;
			case Beauty.Sagittarius: resId = R.drawable.sagittarius; break;
			case Beauty.Capricorn:   resId = R.drawable.capricorn; break;
			default: return;
			}
			img.setImageResource(resId);
		}
	}
)

我们创建了一个匿名类,继承自AutoBindAdapter.Binder,实现了方法setView。setView有两个参数:view表示被设置的view对象;obj是数据。

在这个例子中,我们获取的是星座的信息,它是0~11的数字,表示12个星座;view是一个ImageView,要显示对应的星座的图片。该匿名类就是实现这个转换,并设置给imageview的。

AutoBindAdapter的使用已经够简单了吧,重点是AutoBindAdapter的实现。

AutoBindAdapter继承自BaseAdapter:

public class AutoBindAdapter<E> extends BaseAdapter {

	private List<E> mList;
	private final Context mContext;
	private final LayoutInflater mInflater;

然后,我们定义一个binder接口:

	public static abstract class Binder {
		private static Binder defaultBinder = new Binder() {
			public void setView(View view, Object obj){
				..... //由于篇幅限制,省略,有兴趣者可以看代码
		};
		
		public abstract void setView(View view, Object obj);
		public static Binder getDefault() {
			return defaultBinder;
		}
	}

核心是setView方法,需要开发者实现。当然,我为一般情况提供了一个默认的Binder。

实现一个自动映射的binder类 (看里面的注释吧,不单独写了)

	public static class AutoBinder extends Binder{
		public static class BindInfo {
			public int viewId;
			public AccessibleObject access;
			public Binder bind;   //这里又使用了一个Binder。AutoBinder是给Adapter用的,这个bind是给listitem的子view使用的,因为它们的接口都一样,所以就借用了Adapter的Binder接口了。 
			public BindInfo(int id, AccessibleObject access, Binder bind)
			{
				viewId = id;
				this.access = access;
				this.bind = bind;
			}
                        //这里是获取子数据的地方
			public Object getValue(Object obj) {
				try{
					if(access instanceof Field) //按照Field调用
					{
						return ((Field)access).get(obj);
					}
					else {
						return ((Method)access).invoke(obj, (Object[])null); //按照Method调用,我假定是一个get方法
					}
				}
				catch(Exception e) {
					e.printStackTrace();
				}
				return null;
			}
		}
		private List<BindInfo> mBindList; //这是包含每个具体数据和view的映射关系的列表
		
		public AutoBinder()
		{
			mBindList = new ArrayList<BindInfo>();
		}
		//要求用户提供binder的add方法
		public AutoBinder add(int viewId, AccessibleObject access, Binder bind) {
			mBindList.add(new BindInfo(viewId, access, bind));
			return this; //返回自身,可以实现连续调用
		}
                //直接使用默认binder的add方法
		public AutoBinder add(int viewId, AccessibleObject access)
		{
			return add(viewId, access, Binder.getDefault());
		}
		//这个方法被Adapter的getView调用,用于绑定数据和子view
		public void setView(View view, Object obj)
		{
			int size = mBindList.size(); //获得总共的子数据项和子view项
			if(view == null || obj == null || size <= 0)
				return ;
				
			View[] holders = (View[]) view.getTag(); //这是ViewHolder的优化方法,显然不需要定义一个专门的类来表示ViewHolder,只需要一个固定长度的View数组即可
			if(holders == null) //第一次调用,没有缓存
			{
				holders = new View[size]; //创建ViewHolder
				for(int i = 0; i < size; i ++) //依次处理每个子项
				{
					BindInfo bind = mBindList.get(i); //获得对应位置的binder信息
					holders[i] = view.findViewById(bind.viewId); 
					holders[i].setTag(bind); //这又是一个加速方法,不用每次调用mBindList.get方法
					bind.bind.setView(holders[i], bind.getValue(obj)); //调用真正setView
				}
                                view.setTag(holders); //注意我上传的源代码这里忘记添加了,是个bug
			}
			else
			{         //利用缓存
				for(int i = 0; i < size; i++)
				{
					BindInfo bind = (BindInfo)holders[i].getTag(); //从缓存中取得binder
					bind.bind.setView(holders[i], bind.getValue(obj)); //直接进行绑定
				}
			}
			
		}
	}

注:上传的资源中的:view.setTag(holders) 一句忘了写了,会造成bug,请大家使用时自行添加注意!

下面是Adapter的重点部分:

	@Override
	public View getView(int postion, View convert, ViewGroup parent) {
		View view;
		if(convert == null)
			view = mInflater.inflate(mViewId, parent, false);
		else
			view = convert;
		
		bindView(view, mList.get(postion));
		
		return view;
	}
	
	private void bindView(View view, Object obj)
	{
		if(mBinder == null)
		{
			Binder.getDefault().setView(view, obj);
		}
		else
		{
			mBinder.setView(view, obj);
		}
	}

mBinder就是构造时传递过来的。 很简单,不说了。

AutoBinderAdapter用起来是不是很简单呢?

我已经将一些常用的优化技术集中在AutoBinderAdapter中了,我相信还会有一些优化技术用在其中。

而且,AutoBinderAdapter完全不依赖于用户的具体数据类型,这样,不同的listview完全可以使用同一套代码,可维护性大大的提高。

PS: Binder这个名字实在不好,和Android的Binder冲突了,但是我一时半会想不到更好的名字,如果哪位有什么好的建议,请告诉我哦!

抱歉!评论已关闭.