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控件(如 Button、TextView等)。 具体作用: 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的加载原理是这样的:
@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,下次会好好解释的~
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向之前那样增加适配器,添加数据就好了,在按钮的处理上也比较简单,我偷下懒,先贴代码后解释~
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了。
到此为止,我们的学习之旅又进了一大步,自定义适配器这一部分需要好好的进行理解,只有真正理解的比较透彻,写起来才会比较顺手,学会和精通是不一样的,仅仅是学会就只能实现一些简单的功能,而学精才能推陈出新,创造出更有影响力的项目。