Android 适配器教程(四)

之前我们学习了什么是适配器,并且三种常用的安卓原生适配器也讲完了,接下来我们就要自定义适配器了,自定义的适配器能适应更多的情况,功能更加强大,当然也需要我们更加深入的学习才能应用自如。


终于到自己写一个适配器的时候了!


我准备了两个例子,一个简单一些,一个复杂一些,这次先看个简单的:

我还是继续在前三次的Demo项目上继续添加例子,最后一篇的时候把源码分享给大家~

让我们继续一步步写下去。


这个例子是在ListView上面添加按钮,具体来说是显示一个按钮和一个图片,两行字。这个小问题涉及到的知识挺多的。也许你会想:添加按钮首先要写一个有按钮的xml文件,然后用教程(三)的方法定义一个适配器,然后将数据映射到布局文件上。但是事实并非这样,因为按钮是无法映射的,即使你成功的用布局文件显示出了按钮也无法添加按钮的响应,这时就要研究一下ListView是如何现实的了,而且必须要重写一个类继承BaseAdapter



首先我们先了解一下自己写适配器的原理:


第一步要首先重写一个类继承BaseAdapter


先让我们看一下各个方法:

 

(1)首先是getCount()方法,这个方法要返回你要添加进ListView里的东西的总数,

也就是要告诉ListView,我添加进列表里的东西有多少,需要多长的列表。

这里的mArray就是一个简单的List<String>,

可能有人会问,为什么不直接返回mListView的长度呢?

原因就是我们的ListView可能会添加”头“和”尾“,来进行一些更新之类的交互,

就像微博之类的下拉刷新或者到底后加载,所以干脆直接用我们添加的内容的长度


@Override   

public int getCount() {  

return mArray == null ? 0 : mArray.size();

}  

 

 

(2)接下来就是getItem(int position)方法

ListView要加载内容,要获得内容才可以加载!

这个方法就是要让ListView可以通过一个position来获得我们要添加在相应位置的内容的

内容是什么?当然是刚才mArray里相应位置的东西啦!


@Override  

public Object getItem(int position) {  

    return mArray.get(position);  

}  

 

 

(3)然后是getItemId(int position)方法,这个方法应该是为了方便ListView进行管理的,

简单说,我们就按原来的position来让他管理,原本是几就是几,省事,直接返回position!


@Override  

public long getItemId(int position) {  

    return position;  

}  

 

最后,重头戏!getView()方法!

这里要实现的东西就比较多了

这个也很好理解,个人的理解就是ListView要方便的得到自己里面的每个View

不然人家怎么知道你的mArray里的数据,要怎么填入ListView里的每个View

 

@Override 

        public View getView(int position, View convertView, ViewGroup parent) {  

            return null;  

        }  


这里面一般我们会怎么做呢?

一般,我们添加到ListView里的每个View都是xml定义好的。

一开始需要构造过来一个Context!

通过LayoutInflater.from(context).inflate(R.layout.你定义的xml,null);

获得你要添加进去的View来赋给convertView

如果我们定义的xml里有一个TextView

那我们就TextView tTextView = (TextView)convertView.findViewById(R.id.你的textview);

这样就可以通过position,在mArray里找到我们相应位置的内容,让TextView显示出来

当然,最后要return convertView

把这个我们包装好的View给回ListView,让它在列表里显示。

下面是具体实现的过程:

项目开始:

也还是先在activity_main.xml里添加一个button,一会跳转的时候使用。

然后新建一个类MyAdapterDemo继承自Activity作为我们第四个个例子的Activity,@Override 我们的onCreate方法。

新建一个xml文件myadapterdemo.xml作为我们的布局文件,其中也是包含一个文本域和一个ListView:


myadapterdemo.xml:

代码如下:

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

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="这是myadapter的一个例子" >
    </TextView>

    <ListView
        android:id="@+id/myadapterlistview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" >
    </ListView>

</LinearLayout>

然后需要定义好一个用来显示每一个列内容的xml

Listitem2.xml 包含横向的图片与文字还有一个button,

Listitem2.xml:

代码如下:

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

    <ImageView
        android:id="@+id/imgview1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5px" />

    <TextView
        android:id="@+id/text1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5px"
        android:textColor="#000000"
        android:textSize="22px" />

    <TextView
        android:id="@+id/text2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5px"
        android:textColor="#000000"
        android:textSize="15px" />

    <Button
        android:id="@+id/view_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="more" />

</LinearLayout>

自定义适配器:

 

新建一个类MyAdapter继承自BaseAdapter,这时候Eclipse会提示你Override默认的方法,点击之后就会出现上面我所说的那些方法了。

之后创建一个ViewHolder,具体原因我会在下一次教程中仔细说明

ViewHolder:

public final class ViewHolder{
		public ImageView img;
		public TextView text3;
		public TextView text4;
		public Button viewBtn;
	}
把ViewHolder作为内部类放在最后等着,一会就有用了。


构造器

然后我们再研究下我们需要外部的什么东西,也就是构造器需要被传进什么参数。

我们类比一下之前原生的适配器的构造方法,自然而然的想到他们都有一个Context,同时都需要传参数!

所以我们的MyAdapter自然也不例外了~

在之前的说明中说过,一开始需要构造过来一个Context!

只有这样才能通过LayoutInflater.from(context).inflate(R.layout.你定义的xml,null);

获得你要添加进去的View来赋给convertView。

所以构造器以及必要的对象就要这样写:

	private LayoutInflater mInflater;
	private List<Map<String, Object>> data;
//	构造器,接收数据
	public MyAdapter(Context context, List<Map<String, Object>> data){
		this.mInflater = LayoutInflater.from(context);
		this.data = data;
	}

注意这有个LayoutInflater我给大家解释一下

在实际开发中LayoutInflater这个类还是非常有用的,它的作用类似于findViewById()。不同点是LayoutInflater是用来找res/layout/下的xml布局文件,并且实例化;而findViewById()是找xml布局文件下的具体widget控件( ButtonTextView)
具体作用:
1
、对于一个没有被载入或者想要动态载入的界面,都需要使用LayoutInflater.inflate()来载入;

2、对于一个已经载入的界面,就可以使用Activiyt.findViewById()方法来获得其中的界面元素。

这样我们从构造器中得到了数据data还有context(通过LayoutInflater得到布局文件


之后我们只要把@Override的方法填写完整就好了,希望这时候你还没有忘记开始时我讲过的基本知识。

不过没关系,我会一点点讲清楚的:


第一个方法getCount()

是得到长度,所以外部数据有所少就要有多长,所以返回的是data==null?0:data.size();

也就是如果外面传过来的数据为空,那么长度为0,不是空,长度就是数据的数量,

注意:这里非常有必要处理一下data==null这种情况!很多代码这里都没做处理,这是很不好的习惯。

所以这个方法填好了:

@Override
public int getCount() {

return data == null? 0:data.size();
}


第二个方法getItem(int position)

之前也说过了,这个方法就是要让ListView可以通过一个position来获得我们要添加在相应位置的内容的

内容是什么?当然是data里相应位置的东西啦!

所以这个方法也填好了:

@Override
public Object getItem(int position) {

return data.get(position);
}


第三个方法getItemId(int position)

这个方法应该是为了方便ListView进行管理的,

没有什么特殊需求的话,我们就按原来的position来让他管理,position原本是几就是几,直接返回position


@Override
public long getItemId(int position) {

return position;
}



这样的话就只剩下一个大头了:

千呼万唤始出来的

public View getView(int position, View convertView, ViewGroup parent)

写到这我发现还是不得不先解释一下ViewHolder了,我先粗略的尽量让大家理解,因为下一讲的主题就是它!

咱们一点点的分析,注意看返回值的类型,是一个View,不难想象这就是返回了List里面一个Item的View,Android中有个叫做Recycler(反复循环器)的构件,ListView的加载原理是这样的:

有一个item出了屏幕,在空出来的部分是由出屏幕的那个item通过适配器的getView方法改变成了新的item加以填充的。所以
1.如果你有10亿个项目(item),其中只有可见的项目存在内存中,其他的在Recycler中
2.初始化的时候,ListView先请求一个type1视图(getView),然后请求其他可见的项目。这时conVertView在getView中是null的
3.当item1滚出屏幕,并且一个新的项目从屏幕地段上来时,ListView再请求一个type1视图。convertView此时不是空值了,它的值是item1.你只需要设定新的数据返回convertView,不必重新创建一个视图。这样直接使用convertView从而减少了很不不必要view的创建
 而更快的方式是定义一个ViewHolder,将convertView的tag设置为ViewHolder,不为空是重新使用
 (Tip:View中的setTag(Onbect)表示给View添加一个格外的数据以后可以用getTag()将这个数据取出来。)

这个问题先说到这,下一讲我们在仔细讨论,这样对getView有所理解了吧

我直接贴代码,然后大家看注释就好了:

@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		ViewHolder holder = null;			//首先定义一个ViewHolder对象
		if (convertView == null) {			//当初始化的时候 convertView是空的
			
			holder=new ViewHolder();		//创建一个ViewHolder
			
			//通过LayoutInflater用来找res/layout/下的xml布局文件,并且实例化,这样convertView的界面就有了。
			convertView = mInflater.inflate(R.layout.listitem2, null);
			//使用Activiyt.findViewById()方法来获得其中的界面元素。
			holder.img = (ImageView)convertView.findViewById(R.id.imgview2);
			holder.text3 = (TextView)convertView.findViewById(R.id.text3);
			holder.text4 = (TextView)convertView.findViewById(R.id.text4);
			holder.viewBtn = (Button)convertView.findViewById(R.id.view_btn);
			//将holder对象作为标签添加到View上
			convertView.setTag(holder);
			
		}else {
			//不为空的时候直接重新使用convertView从而减少了很多不必要的View的创建
			holder = (ViewHolder)convertView.getTag();
		}
		
			//然后加载数据
		holder.img.setBackgroundResource((Integer)data.get(position).get("imgview2"));
		holder.text3.setText((String)data.get(position).get("text3"));
		holder.text4.setText((String)data.get(position).get("text4"));
		
			//为按钮加监听
		holder.viewBtn.setOnClickListener(new View.OnClickListener() {
			
			@Override
			public void onClick(View v) {
				showInfo();					
			}
		});
		
		
		return convertView;
	}

这样大家就比较理解了吧,至于ViewHolder和Tag,下次会好好解释的~
 
下面是MyAdapter的完整代码
package com.example.adapterdemo;

import java.util.List;
import java.util.Map;

import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;

public class MyAdapter extends BaseAdapter{
	
	private LayoutInflater mInflater;
	private List<Map<String, Object>> data;
	private Context c;
//	构造器,接收数据
	public MyAdapter(Context context, List<Map<String, Object>> data){
		this.c = context;
		this.mInflater = LayoutInflater.from(context);
		this.data = data;
	}

	@Override
	public int getCount() {
		
		return data == null? 0:data.size();
	}

	@Override
	public Object getItem(int position) {
		
		return data.get(position);
	}

	@Override
	public long getItemId(int position) {
		
		return position;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		ViewHolder holder = null;			//首先定义一个ViewHolder对象
		if (convertView == null) {			//当初始化的时候 convertView是空的
			
			holder=new ViewHolder();		//创建一个ViewHolder
			
			//通过LayoutInflater用来找res/layout/下的xml布局文件,并且实例化,这样convertView的界面就有了。
			convertView = mInflater.inflate(R.layout.listitem2, null);
			//使用Activiyt.findViewById()方法来获得其中的界面元素。
			holder.img = (ImageView)convertView.findViewById(R.id.imgview2);
			holder.text3 = (TextView)convertView.findViewById(R.id.text3);
			holder.text4 = (TextView)convertView.findViewById(R.id.text4);
			holder.viewBtn = (Button)convertView.findViewById(R.id.view_btn);
			//将holder对象作为标签添加到View上
			convertView.setTag(holder);
			
		}else {
			//不为空的时候直接重新使用convertView从而减少了很多不必要的View的创建
			holder = (ViewHolder)convertView.getTag();
		}
		
			//然后加载数据
		holder.img.setBackgroundResource((Integer)data.get(position).get("img"));
		holder.text3.setText((String)data.get(position).get("text3"));
		holder.text4.setText((String)data.get(position).get("text4"));
		
			//为按钮加监听
		holder.viewBtn.setOnClickListener(new View.OnClickListener() {
			
			@Override
			public void onClick(View v) {
				showInfo();					
			}
		});
		
		
		return convertView;
	}
	
	
	public final class ViewHolder{
		public ImageView img;
		public TextView text3;
		public TextView text4;
		public Button viewBtn;
	}
	/**
	 * listview中点击按键弹出对话框
	 */
	public void showInfo(){
		new AlertDialog.Builder(c)
		.setTitle("我的listview")
		.setMessage("介绍...")
		.setPositiveButton("确定", new DialogInterface.OnClickListener() {
			@Override
			public void onClick(DialogInterface dialog, int which) {
			}
		})
		.show();
		
	}
	
}


之后我们再回到MyAdapterDemo向之前那样增加适配器,添加数据就好了,在按钮的处理上也比较简单,我偷下懒,先贴代码后解释~

MyAdapterDemo的完整代码如下:

package com.example.adapterdemo;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;


public class MyAdapterDemo extends Activity {
	private ListView lv;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.myadapterdemo);
		lv = (ListView) findViewById(R.id.myadapterlistview);
		MyAdapter myadapter = new MyAdapter(this,getData());
		lv.setAdapter(myadapter);
		
	}
	
	private List<Map<String, Object>> getData() {
		List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();

		Map<String, Object> map = new HashMap<String, Object>();
		map.put("text3", "Image 1");
		map.put("text4", "info 1");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);

		map = new HashMap<String, Object>();
		map.put("text3", "Image 2");
		map.put("text4", "info 2");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);

		map = new HashMap<String, Object>();
		map.put("text3", "Image 3");
		map.put("text4", "info 3");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image 4");
		map.put("text4", "info 4");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image 5");
		map.put("text4", "info 5");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image 6");
		map.put("text4", "info 6");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image 7");
		map.put("text4", "info 7");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image 8");
		map.put("text4", "info 8");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image 9");
		map.put("text4", "info 9");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image10");
		map.put("text4", "info10");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image11");
		map.put("text4", "info11");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image12");
		map.put("text4", "info12");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image13");
		map.put("text4", "info13");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		map = new HashMap<String, Object>();
		map.put("text3", "Image14");
		map.put("text4", "info14");
		map.put("img", R.drawable.ic_launcher);
		list.add(map);
		
		return list;
	}
	
}

让我们再总结一下工作原理listView在开始绘制的时候,系统首先调用getCount()函数,根据他的返回值得到listView的长度,然后根据这个长度,调用getView()逐一绘制每一行。如果你的getCount()返回值是0的话,列表将不显示同样return1,就只显示一行。

  系统显示列表时,首先实例化一个适配器(这里将实例化自定义的适配器)。当手动完成适配时,必须手动映射数据,这需要重写getView()方法。系统在绘制列表的每一行的时候将调用此方法。getView()有三个参数,position表示将显示的是第几行,covertView是从布局文件中inflate来的布局。我们用LayoutInflater的方法将定义好的listitem.xml文件提取成View实例用来显示。然后将xml文件中的各个组件实例化(简单的findViewById()方法)。这样便可以将数据对应到各个组件上了。但是按钮为了响应点击事件,需要为它添加点击监听器,这样就能捕获点击事件。至此一个自定义的listView就完成了。

现在让我们回过头从新审视这个过程。系统要绘制ListView了,他首先获得要绘制的这个列表的长度,然后开始绘制第一行,怎么绘制呢?调用getView()函数。在这个函数里面首先获得一个View(实际上是一个ViewGroup),然后再实例并设置各个组件,显示之。好了,绘制完这一行了。那 再绘制下一行,直到绘完为止。

如果需要ListView也加入监听,在实际的运行过程中会发现listView的每一行没有焦点了,这是因为Button抢夺了listView的焦点,只要布局文件中将Button设置为没有焦点应该就OK了。

最后我们看一下实现效果图:Android 适配器教程(四)

到此为止,我们的学习之旅又进了一大步,自定义适配器这一部分需要好好的进行理解,只有真正理解的比较透彻,写起来才会比较顺手,学会和精通是不一样的,仅仅是学会就只能实现一些简单的功能,而学精才能推陈出新,创造出更有影响力的项目。

下一讲我会对这一讲留下的几个问题进行详细的分析,比如Holder,tag的详解,争取让大家理解的更加透彻一些,请继续关注~

源代码我会在最后一讲的最后附上链接,因为我也是边写博客边码代码,我觉得这样思路比较清楚一些。
我也还是个学生,水平有限,还请大家多多指教~


上一篇:一些实用的安卓UI设计工具


下一篇:Android 适配器教程 (六)