较早的网文说:Android是完全遵循MVC模式设计的框架,Activity是Controller;Activity对应的/res/layout/xml文件是View;数据即Model。而Adapter是Model和Controller之间的桥梁。
适配器将数据整合应用到List类型的可视控件上,列表控件的每行条目一般都是同样的结构,适配器就是重复操作条目的模板使之一一绑定数据。下面是适配器继承关系:
常用适配器:
l ArrayAdapter:数组型适配器,针对数组数据。它可以通过泛型指定数组的元素,主要适用于TextView控件。使用时同一时刻只能针对单一的控件。
l SimpleAdapter:简约型,简约不简单。其操作List集合数据,而List的每个元素又是Map集合,Map中可以对应多个控件存储多个k-v数据。控件类型可以是单纯的TextView,也可以是ImageView等。
l CursorAdapter:Cursor数据类型适配器。Cursor-游标,和SQLite数据库相关的数据封装对象,是查询数据库后返回的一行数据的封装,其中包含了众多的列的内容。本章节不展示CursorAdapter使用,因为和数据库的关联,将在后文SQLite时讲解。
l BaseAdapter:最底层基础型适配器。如果列表控件的条目不再是简单的显示一个文本标签,或者一个图片配上一个文本;而是LinearLayout、RelativeLayout、ImageView、TextView、Button等等大杂烩,那么BaseAdapter是必须的。
先看一下和适配器相关的控件,注意这张图展示的是AdapterView的继承关系:
自动补齐文本框。继承自EditText,继而上层是TextView,再上层是View。所以它不属于AdapterView,但却能和Adapter结合使用。
自动补齐的效果如图搜索引擎的输入框,当敲入一个字符时,会弹出一个列表展示以此字符开头的单词或短语。
首先在视图的布局xml里注册一个自补齐文本框。
<AutoCompleteTextView
android:id="@+id/autoComp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
要使用适配器的控件,总是须要一个条目布局文件。在/res/layout/里创建一个item_auto.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" >
<!-- 这个布局可以稍微复杂些,如多显示一张图片。但最终采用的还是TextView的文本内容 -->
<TextView
android:id="@+id/textviewInItem"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
也正是这个条目布局的复杂程度,决定了使用哪一种适配器。实际本例中因为AutoCompleteTextView只和文本相关,所以即使item布局中有其它诸如ImageView等控件也不影响使用,详见代码。
然后下文使用多个适配器的例子。
当前条目布局中只有一个TextView,使用ArrayAdapter最合适了。
// 1.定义数据。ArrayAdapter用到的数组数据
String[] strArr = {"a1","a2","aaa","d1","e1"};
// 这些数据一般配置在 /res/values/arr.xml 里(手动创建,名称限于小写字母数字下划线,常用于配置固定的内容如公司部门)
strArr = getResources().getStringArray(R.array.department);
// 或者从网络获取(最多的情况,涉及网络访问和异步请求,后文详述)
// 2.定义适配器
// 使用系统提供的条目布局时,只有3个参数(上下文,布局,数据)
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
MainActivity.this,
android.R.layout.simple_dropdown_item_1line, // 系统提供的布局
strArr);
// 使用自定义的条目布局时,则须要4个参数(上下文,布局,布局中文本标签,数据)
adapter = new ArrayAdapter<String>(
MainActivity.this,
R.layout.item_auto, // 自定义布局
R.id.textviewInItem, // 布局中对应的TextView。其它ImageView等不相干。
strArr);
// 3.实例化自补齐输入框
AutoCompleteTextView mAutoComp = (AutoCompleteTextView) findViewById(R.id.autoComp);
// 至少输入1个字符才出现提示。默认2个
mAutoComp.setThreshold(1);
// 指定适配器
mAutoComp.setAdapter(adapter);
// 输入状态监听
mAutoComp.addTextChangedListener(new TextWatcher() {
//输入前回调进入
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
Log.e("ard","beforeTextChanged:" + s);
}
/**
* 有输入时系统调用的方法
* s:本次输入结束后整行内容
* start:始于0的位置,刚刚输入(或删除)的字符位置。一个汉字算1个位置
* before:删除的字符数量。一般每次都是1个,除非先选择一段文字。一个汉字算1个
* count:本次输入的字符数量。一个汉字算1个,一个英语单词计算其全部字符,一行中文计算全部汉字
*/
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
Log.e("ard","s->"+s);
Log.e("ard","start->"+start);
Log.e("ard","count->"+count);
Log.e("ard","before->"+before);
}
//输入后回调进入
@Override
public void afterTextChanged(Editable s) {
Log.e("ard","afterTextChanged: " + s);
}
});
说一下上面arr.xml,这个文件名arr是自定义的,没有限制。关键是其内部的<resources>标签,及<string-array name="department">子标签。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="department">
<item>IT</item>
<item>财务</item>
<item>销售</item>
<item>设计</item>
<item>人力</item>
</string-array>
</resources>
另外AutoCompleteTextView不区分大小写。
ArrayAdapter还有一种使用形式,直接使用自身add方法添加数据,在测试性的开发中常用:
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.item);
adapter.add("a"); // 同Array的方法
adapter.add("b");
adapter.add("c");
mAuto.setAdapter(adapter);
实质是适配器内定义了一个ArrayList类型的变量,当适配器add时,调用这个变量的add方法存储数据。
简约型适配器-简单适配器,常用于显示单个ImageView+单个TextView这种很规整的条目布局。SimpleAdapter不是AutoCompleteTextView常用的适配器,它在Spinner、ListView等直接展示的控件里中常用。
下面是Item的布局:
<?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/imageViewInItem"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher" />
<TextView
android:id="@+id/textviewInItem"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="默认文本" />
</LinearLayout>
Item布局里有一个ImageView、一个TextView。
既然有2个须要动态控制的控件,那么数据中也至少有2个对应的k-v条目来匹配。现在使用虚拟数据演示:
// 1.数据准备
List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
for (int i = 0; i < 10; i++) {
Map<String, Object> map = new HashMap<String, Object>();
// AutoCompleteTextView 要求转为 String
map.put("img", String.valueOf(R.drawable.ic_launcher)); // 对应条目的图片展示控件
map.put("txt", "value " + i); // 对应条目的文本标签控件
// 如果还有其它需要动态控制的控件,继续put
list.add(map);
}
代码中List集合的元素类型为Map,而Map要求key是String类型value是Object类型,所以value可以是任何一种引用数据类型。但本例中可能由于AutoCompleteTextView的原因,现在value是为String。
当定义SimpleAdapter的时候,注意数据key和控件id的一一对应。
String[] from = new String[] { "img", "txt" };
int[] to = new int[] { R.id.imageViewInItem, R.id.textviewInItem };
// 2.定义适配器
SimpleAdapter adapter = new SimpleAdapter(
MainActivity.this, // 上下文
list, // 数据
R.layout.item_auto, // item布局
from, // 数据中的key
to); // 与数据key对应的控件id
最后要做的就是关联适配器:
// 3.实例化控件
AutoCompleteTextView mAutoComp = (AutoCompleteTextView) findViewById(R.id.autoComp);
mAutoComp.setThreshold(1);
mAutoComp.setAdapter(adapter);
终于轮到高大上的基础型适配器。在自补齐输入框使用BaseAdapter实在大材小用。
首先看一个复杂的item布局:
<?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:gravity="center_vertical"
android:orientation="horizontal" >
<ImageView
android:id="@+id/imageAvatar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<LinearLayout
android:id="@+id/flag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical" >
<TextView
android:id="@+id/textviewName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="默认文本" />
<TextView
android:id="@+id/textviewAge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="默认文本" />
</LinearLayout>
<TextView
android:id="@+id/textviewDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/flag"
android:textSize="40sp"
android:text="默认说明"/>
</RelativeLayout>
</LinearLayout>
再整点虚拟数据。
在BaseAdapter中已经很少直接使用数组、集合做数据的封装,因为其布局-数据过于复杂,通常会使用JavaBean来封装。JavaBean是一种模式,属于JavaEE开发中Model一个层面,这里对其概念进行引用。可以这样理解:JavaBean是属性私有并为属性提供了set、get方法的一般java类。
JavaBean的定义规范是属性名使用驼峰命名法,首字母小写,然后根据其意义某个字符大写。如姓名:name,昵称:nickName。姓名的set方法就是:setName(),而get方法就是:getName();昵称的set方法:setNickName(),get方法:getNickName()。当然set方法须要参数,get方法须要返回值。Boolean类型属性个get方法比较特殊:isSex()。
先整理一下项目的包机构,将Activity节目java文件放到一个独立目录;新建一个JavaBean类,放在一个独立目录:
在JavaBean里定义和控件对应的属性。很多时候因为业务的须要,属性数量比控件数量或多或少。为了省事都定义为String类型。
package com.cuiweiyou.newproject.bean;
public class ItemBean {
String avatar; // 头像
String name; // 姓名
String age; // 年龄
String description; // 说明,描述
}
然后 alt+shift+s,r,或者右键,Source,Generate Getters and Setters... 让IDE为我们创建属性的set、get方法。
一般为了使用方便,还会为JavaBean提供2个构造方法:
回到Activity中,
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
List<ItemBean> list = new ArrayList<ItemBean>(); // 虚拟数据
list.add(new ItemBean(String.valueOf(R.drawable.ic_launcher), "a小白", "35", "没有说明是最好的说明"));
list.add(new ItemBean(String.valueOf(R.drawable.ic_launcher), "a小黑", "25", "总得说点什么"));
list.add(new ItemBean(String.valueOf(R.drawable.ic_launcher), "a小红", "15", "没的说"));
list.add(new ItemBean(String.valueOf(R.drawable.ic_launcher), "b小白", "40", "1234567890"));
list.add(new ItemBean(String.valueOf(R.drawable.ic_launcher), "c小白", "33", "asdfghjk"));
list.add(new ItemBean(String.valueOf(R.drawable.ic_launcher), "d小白", "26", "pokijhytfds"));
最关键的部分来了,创建适配器。可以直接创建匿名适配器对象,也可以创建内部类适配器,还有最标准的做法-单独创建一个实现BaseAdapter的自定义适配器-一个java文件。
匿名适配器对象和内部类适配器都在Activity中,可以直接使用Activity的变量比较方便。而自定义适配器可以通过构造方法传入用到的数据。
/** 适配器 **/
public class AutoAdapter extends BaseAdapter{
/** 上下文对象 **/
private Context context;
/** 数据 **/
private List<ItemBean> list;
/**
* 适配器
* @param context 上下文
* @param list 数据
*/
public AutoAdapter(Context context, List<ItemBean> list) {
this.context = context;
this.list = list;
}
// 当调用适配器时,系统会首先执行此方法获取List中的元素数量,即可用创建的条目数量
@Override
public int getCount() {
// 如果数据无效,就返回0
return list==null || list.size()<1 ? 0 : list.size();
}
// 如果上面的getCount返回大于0的数值,如5,就调用5次此getItem方法创建5个条目。
// position:条目索引,始于0,系统传入
@Override
public Object getItem(int position) {
return list.get(position);
}
// 每调用一次getItem方法,即调用一次此方法。
// position:条目索引,始于0,系统传入
@Override
public long getItemId(int position) {
return position;
}
// 每调用一次getItem方法,即调用一次此方法
/**
* 此方法是创建条目的方法。
* position:当前条目的索引,系统传入
* convertView:条目的复用体,系统传入。用于优化
* parent:包裹此条目的父容器,系统传入
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 1.加载一个xml文件转为一个View对象
View mView = View.inflate(context, R.layout.item_auto, null);
// 2.从中得到控件实例
ImageView mAvatar = (ImageView) mView.findViewById(R.id.imageAvatar);
TextView mName = (TextView) mView.findViewById(R.id.textviewName);
TextView mAge = (TextView) mView.findViewById(R.id.textviewAge);
TextView mDescription = (TextView) mView.findViewById(R.id.textviewDescription);
// 3.使用数据更新控件
mAvatar.setImageResource(Integer.valueOf(list.get(position).getAvatar()));
mName.setText(list.get(position).getName());
mAge.setText(list.get(position).getAge());
mDescription.setText(list.get(position).getDescription());
// 4.返回View给系统
return mView;
}
}
代码中getView方法的convertView对象并没有使用到,将在后面的ListView章节细讲。
适配器还没有准备好。AutoCompleteTextView的适配器除了继承BaseAdapter外,还要实现Filterable接口。Filterable接口中有个getFilter方法,用于获取过滤器。一般过滤器实现过滤操作是异步进行的,但在此AutoCompleteTextView中却是直接调用performFiltering方法,所以不要进行耗时操作(网络、数据库),防止ANR:Application Not Responding。
来看一下过滤器完整代码:
// 自定义过滤器,根据输入的字符提取对应数据
class AutoFilter extends Filter {
// 查询传入的数据,过滤。返回结果到publishResults方法
// constraint即每次输入后整行文本内容。和mAutocomp.setThreshold(1);相关
// 按照默认时,达到2个字符才返回。so,如果输入后整行文本只有1个字符,constraint是null
@Override
protected FilterResults performFiltering(CharSequence constraint) {
if(constraint == null)
return null;
// 新建的属于适配器的数据集合副本
list = listTmp;
List<ItemBean> tmp = new ArrayList<ItemBean>(); // 属于过滤器的数据集合
// 查询结果保存到FilterResults对象里
FilterResults results = new FilterResults();
// 根据输入的文本,和传入的List集合中的数据匹配。本例匹配的是name
for (int i = 0; i < list.size(); i++) {
if(constraint.toString().equals(list.get(i).getName().substring(0,constraint.length()))){
tmp.add(list.get(i));
}
}
results.values = tmp; // 保存更新后的集合
results.count = tmp.size();
return results;
}
// 接收performFiltering发来的过滤后的数据
// constraint:同上,每次输入后的整行文本
// results:performFiltering返回的结果。FilterResults是Filter的内部静态类,其values变量类型是Object
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
if(constraint == null || results == null || results.count < 1)
return;
list = (List<ItemBean>) results.values;
// BaseAdapter刷新
notifyDataSetChanged();
}
}
现在整个适配器是这样的:
看一下最终效果:
本例先到此。还没有处理的问题:单击条目,将匹配的数据(name)显示在输入框里,要实现这一功能只需为条目添加点击事件做出处理。AutoCompleteTextView.setOnItemClickListener(new AdapterView.OnItemClickListener(){}),限于篇幅,后文的ListView章节中讲解。
下拉框也是常用的列表型展示控件。常常和上面讲到的arr.xml做一些固定数据的展示。
首先是 /res/values/arr.xml :
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="department">
<item>IT</item>
<item>财务</item>
<item>销售</item>
<item>设计</item>
<item>人力</item>
</string-array>
</resources>
然后是 /res/layout/item.xml 条目布局:
最后是java代码:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 从 /res/values/arr.xml 文件读取数据为数组
String[] department = getResources().getStringArray(R.array.department);
// 使用自定义条目布局创建适配器
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
this,
R.layout.item,
R.id.textviewName,
department);
Spinner mSpinner = (Spinner) findViewById(R.id.spinner);
mSpinner.setAdapter(adapter);
}
}
效果:
简单来说,这样就完了。但有时我们可能须要根据选择的条目-项目做某种相应,这里用到了setOnItemSelectedListener()为其添加选择监听。
mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener(){
// 选择了某一个条目
// position:对应适配器中的条目索引
// id:对应弹出的列表中的位置。一般和position相同
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
Log.d("ard", "position:" + position + ", id:" + id + ", 内容:" + mSpinner.getSelectedItem().toString());
}
// 打开又关闭,没做出选择
@Override
public void onNothingSelected(AdapterView<?> parent) {
Log.d("ard", "选择恐惧症?");
}});
又终于轮到高大上的列表展示控件ListView了。可以说这是app开发中使用频率最高、技术性最强的大批量数据展示控件。围绕其进行的网络请求、数据解析、线程通讯等技术更使之低奢内。
要用ListView基本必须配套BaseAdapter,本章节也是如此,同时解决前文留下的getView方法中优化问题。
准备一个有int-flag-国旗、string-country-国家、string-description-说明3个String类型属性的JavaBean,以及和JavaBean属性对应的条目布局,
再准备模拟数据,/res/values/arr.xml,以如下形式在每个二级节点力一一对应添加合适数量的item:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="country">
<item>澳大利亚 Australia</item>
<item>奥地利 Austria</item>
<item>比利时 Belgium</item>
<item>加拿大 Canada</item>
<item>中国 China</item>
</string-array>
<string-array name="flag">
<item>@drawable/australia</item>
<item>@drawable/austria</item>
<item>@drawable/belgium</item>
<item>@drawable/canada</item>
<item>@drawable/china</item>
</string-array>
<string-array name="description">
<item>全称为澳大利亚联邦。其领土面积7,686,850万平...个大陆的国家。</item>
<item>奥地利共和国,是一个位于欧洲中部的议会制共和制制国家</item>
<item>比利时王国,位于欧洲西部沿海,东与德国接壤,北...国隔海相望。</item>
<item>北美洲最北的国家,东北部和丹麦领地格陵兰岛相望...斯加州为邻。</item>
<item>世界四大文明古国之一,有着悠久的历史,距今约...变和朝代更迭。</item>
</string-array>
</resources>
先看一下Activity里的代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 1.准备数据
String[] country = getResources().getStringArray(R.array.country);
/* <integer-array name="flag">
<item>@drawable/australia</item>
这种形式使用getIntArray获取到的是0,除非item里是纯数字。使用getStringArray获取的是图片全路径字串
int[] flag = getResources().getIntArray(R.array.flag); */
/* 下面是将字符串型的图片ID数组转为可用的id数组
<string-array name="flag">
<item>@drawable/australia</item>
首先读取字符数组为TypedArray对象,其内部定义多个int数组保存资源的ID */
TypedArray arr = getResources().obtainTypedArray(R.array.flag);
int[] flag = new int[arr.length()];
// 然后遍历typedarray的元素
for (int i = 0; i < arr.length(); i++){
// 提取资源文件的ID,没有提取到有效id时给0
flag[i] = arr.getResourceId(i, 0);
}
arr.recycle(); // 回收那个啥,使arr可以重用
String[] description = getResources().getStringArray(R.array.description);
List<ItemBean> list = new ArrayList<ItemBean>();
for (int i = 0; i < country.length; i++) {
list.add(new ItemBean(flag[i], country[i], description[i]));
}
// 2.定义适配器
MyListAdapter adapter = new MyListAdapter(MainActivity.this, list);
// 3.为ListView指定适配器
((ListView)findViewById(R.id.listView)).setAdapter(adapter);
}
其中涉及arr.xml的部分先了解即可。
自定义适配器的结构:
getView方法的重写在AutoCompleteTextView里接触过了:使用View的静态方法inflate方法加载一个xml布局文件为View实例,进行数据设置后返回。这里要着重讲解其优化操作。
优化方式1:
之前已经说过,方法的第2个参数convertView是系统传入的一个复用体。可以使用它重复调用1个条目模板,尽可能减少View.inflate操作。如此,可以优化代码如下:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 创建第一个条目时,保存重用体为模板
if(convertView == null){
convertView = View.inflate(context, R.layout.item, null);
}
// 使用重用对象获取条目中的具体某个控件
ImageView mFlag = (ImageView) convertView.findViewById(R.id.flag);
TextView mCountry = (TextView) convertView.findViewById(R.id.country);
TextView mDescription = (TextView) convertView.findViewById(R.id.description);
// 条目对应的数据封装对象
ItemBean bean = list.get(position);
// 更新控件
mFlag.setImageResource(bean.getFlag());
mCountry.setText(bean.getCountry());
mDescription.setText(bean.getDescription());
// 向系统返回复用对象,即一个条目
return convertView;
}
优化方式2:
看起来好多了,无论创建多少条目,只需第一次View.inflate建好模板之后重复使用即可。但是每次创建条目,仍然需要重新从模板中查找具体的控件,因此还需要优化。此时引入一个控制器-Holder。Holder是把模板的每个控件进行封装,成为一个简单Java类。当创建新条目时,不再重新findViewById而是直接从Holder里拿出来复用。
业务比较简单时,Holder总是以内部类的形式创建于适配器内,其属性即条目中要动态控制的控件类型。
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 先声明控制器
ItemHolder holder;
if(convertView == null){
convertView = View.inflate(context, R.layout.item, null);
// 随复用体初始化
holder = new ItemHolder();
holder.mIVFlag = (ImageView) convertView.findViewById(R.id.flag);
holder.mTVName = (TextView) convertView.findViewById(R.id.country);
holder.mTVDesc = (TextView) convertView.findViewById(R.id.description);
// 将控制器指定为复用体的tag
convertView.setTag(holder);
} else {
// 从第2个条目开始,重用控制器
holder = (ItemHolder) convertView.getTag();
}
// 条目对应的数据封装对象
ItemBean bean = list.get(position);
// 使用控制器更新控件
holder.mIVFlag.setImageResource(bean.getFlag());
holder.mTVName.setText(bean.getCountry());
holder.mTVDesc.setText(bean.getDescription());
// 向系统返回复用对象,即一个条目
return convertView;
}
// 控制器,Holder类
class ItemHolder{
ImageView mIVFlag;// 国旗
TextView mTVName;// 国家名称
TextView mTVDesc;// 描述
}
ListView的事件
单击和长按:
列表控件有条目单击事件ListView.setOnItemClickListener,AdapterView.OnItemClickListener。同时比较常用的还有长按事件ListView.setOnItemLongClickListener,AdapterView.OnItemLongClickListener。
两个事件结合使用的例子:当点击时判断一个开关是否打开,打开了做出响应;而这个开关只有长按条目后才打开。
既然在2个监听中用到同一个开关,因此这个开关是个全局属性,来看一下Activity的结构:
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// 这里访问外部类的成员变量,此变量须初始化
if(clickAble){
Log.d("ard", "单击了 position:" + position + ", id:" + id);
}
}
});
mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
/**
* parent mListView
* view 条目实例
* position 对应数据集合中的索引,始于0
* id 对应列表中的位置,始于0
*/
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
clickAble = true;
Log.d("ard", "开关打开了 ");
return true; // 返回true表示在条目内完成了处理,click事件不再下发引起其它事件
}
});
滚动:
如果条目数量很多ListView即可滚动显示超出屏幕的内容,这些条目的信息可以通过setOnScrollListener,AbsListView.OnScrollListener得到。
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
/**
* 滚动过程中持续调用,监听滚动状态改变的方法
* view:mListView
* scrollState:状态码,0-停止;1-正在滚动;2-手指离开屏幕
*/
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
Log.d("ard", "当前滚动状态:" + scrollState);
}
/**
* 滚动过程中持续调用,监听数据的方法
* view:mListView
* firstVisible:当前屏幕上可以看到的最顶部第一个条目的索引。始于0
* visibleItemCount:当前屏幕上可以看到的全部条目数量
* totalItemCount:mListView内拥有的全部条目数量。
* /// 一般第一个条目的索引是0,如果mListView添加了header条目,header的索引是0,普通条目类推
* /// totalItemCount一般和适配器使用的数据集合list的size()相同。如果mLV添加了header、footer,也计算在内
*/
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
Log.d("ard", "当前可见最顶条目索引:" + firstVisibleItem + ", 可见总条目数量:" + visibleItemCount + ",全部条目数量:" + totalItemCount);
/**
* 注意,如果添加了 setOnScrollListener(new AbsListView.OnScrollListener的同时,
* 又添加了 setOnItemClickListener(new AdapterView.OnItemClickListener
* 那么 点击mListView,会首先执行 onScroll ,然后才执行 onItemClick。若手指在屏幕上滑动,不会触发单击
*/
}
});
代码的注释也提到了,同时添加条目单击和列表滚动监听时的并发情况。没有哪种业务要求在单击事件、滚动事件的onScroll里同时做相同的处理,所以这种并发也不会带来什么悲剧。
Item内控件的事件
ListView确实很复杂呃~~~~~~~~~~~
每个条目是一个自定义的布局,其中又有各种子控件,TextView、ImageView、Button、... ,大杂烩。每个控件都可以单独添加事件处理。
要控制Item中的子控件,首先要得到控件实例,继而要首先得到Item的位置。如果是单单指定控制某位置Item的子控件,在Activity里,使用mListView的适配器getItem得到条目实例,再findViewById获取控件实例即可。如果要为每一个条目中的指定控件添加事件处理,则需要在适配器内部进行。getView方法中即得到每个Item位置,又创建Item实例,首选。
注意:如果子控件添加事件处理,会屏蔽Item的事件。
接上面的代码:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final ItemHolder holder;
if(convertView == null){
convertView = View.inflate(context, R.layout.item, null);
holder = new ItemHolder();
holder.mIVFlag = (ImageView) convertView.findViewById(R.id.flag);
holder.mTVName = (TextView) convertView.findViewById(R.id.country);
holder.mTVDesc = (TextView) convertView.findViewById(R.id.description);
convertView.setTag(holder);
} else {
holder = (ItemHolder) convertView.getTag();
}
ItemBean bean = list.get(position);
holder.mIVFlag.setImageResource(bean.getFlag());
holder.mTVName.setText(bean.getCountry());
holder.mTVDesc.setText(bean.getDescription());
/**
* 新插入代码:
* 给ImageView控件添加单击事件
* 单击图片,改变文本标签
**/
holder.mIVFlag.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
holder.mTVDesc.setText(holder.mTVName.getText().toString());
}
});
return convertView;
}
本例中,无论哪个Item,只要点击上面的ImageView,就会触发相应处理;同时不会引发整个Item添加的单击事件和点击事件;在ImageView上长按触发的仍旧是单击事件。
可扩展列表控件。实际继承自ListView,看起来也一样,仅默认多了一个下拉箭头。但它的每个条目是一个分组,可以点开显示下一级子列表。常用于聊天好友分组、通讯录分组、部门级别等。
级别关系也说明了所属关系:第2级列表属于第1级列表的Item。数据封装时,外层Item的FatherBean会多出一个属性存储第2即列表。List<ChildBean> childList这样,ChildBean即第2级列表的Item封装。
模拟数据的准备:
private void initData() {
// Activity中全局的数据集合
list = new ArrayList<ItemGroup>();
for (int i = 0; i < 20; i++) {
List<ItemChild> listchild = new ArrayList<ItemChild>();
for (int j = 0; j < 10; j++) {
listchild.add(new ItemChild("姓名" + j, "介绍" + j));
}
list.add(new ItemGroup("组名" + i, "描述" + i, listchild));
}
}
既然是2级列表,那么也就有对应不同级别条目的不同布局。新建2个xml文件:item_group.xml、item_child.xml,注意只能用小写字母和下划线。布局如何全靠个人审美,没什么要注意的。
然后先看一下扩展列表的准备:
最重要的适配器部分。MyListAdapter不再是继承自BaseAdapter,而是前文没有讲过的BaseExpandableListAdapter。
BaseExpandableListAdapter和BaseAdapter没有任何关系,但它的使用雷同,结构非常相像。
public class MyListAdapter extends BaseExpandableListAdapter{
private Context context;
private List<ItemGroup> list;
public MyListAdapter(Context context, List<ItemGroup> list){
this.context = context;
this.list = list;
}
@Override
public int getGroupCount() { // 外层父条目总数
return list==null || list.size()<1 ? 0 : list.size();
}
@Override
public int getChildrenCount(int groupPosition) { // 内层子条目总数
List<ItemChild> listChild = list.get(groupPosition).getListChild();
return listChild==null || listChild.size()<1 ? 0 : listChild.size();
}
@Override
public Object getGroup(int groupPosition) { // 外层父条目bean
return list.get(groupPosition);
}
@Override
public Object getChild(int groupPosition, int childPosition) { // 内层子条目bean
return list.get(groupPosition).getListChild().get(childPosition);
}
@Override
public long getGroupId(int groupPosition) { // 外层父条目id
return groupPosition;
}
@Override
public long getChildId(int groupPosition, int childPosition) { // 内层子条目id
return childPosition;
}
@Override
public boolean hasStableIds() { // 显示时复用已有对象
return true;
}
@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
// TODO
return null;
}
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
// TODO
return null;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) { // 内层子条目是否接收点击事件
return false;
}
}
下面看getGroupView、getChildView方法,它们分别创建Item的布局实例,系统分别给它们提供了convertView复用体,因此和ListView一样可以做出优化。getGroupView中系统提供了一个isExpanded属性,保存组条目的展开状态,只要复用了,滚出滚入屏幕会都是之前的展开状态。getChildView里isLastChild用于指定子条目在本组中是否是最后一条。
/**
* 外层条目
* groupPosition:条目索引
* isExpanded:是否展开,默认false
* convertView:复用体
*/
@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
/** TODO Holder优化 **/
if(convertView == null){
convertView = View.inflate(context, R.layout.item_group, null);
}
TextView mGroup = (TextView) convertView.findViewById(R.id.group); // 组名
TextView mDescription = (TextView) convertView.findViewById(R.id.description); // 描述
mGroup.setText(list.get(groupPosition).getGroup());
mDescription.setText(list.get(groupPosition).getDescription());
return convertView;
}
/**
* 内层条目
* @param groupPosition 所在组的索引
* @param childPosition 在本组中的位置索引
* @param isLastChild 是否是本组最后一个条目
* @param convertView 复用体
*/
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
/** TODO Holder优化 **/
if(convertView == null){
convertView = View.inflate(context, R.layout.item_child, null);
}
TextView mName = (TextView) convertView.findViewById(R.id.name);
TextView mIntroduction = (TextView) convertView.findViewById(R.id.introduction);
mName.setText(list.get(groupPosition).getListChild().get(childPosition).getName());
mIntroduction.setText(list.get(groupPosition).getListChild().get(childPosition).getIntroduction());
return convertView;
}
代码中没有使用Holder优化。
再说说isChildSelectable方法,字面意思说是否子条目可以被选择。这是相对于扩展列表整体而言的,对应ExpandableListView的setOnChildClickListener方法,参数为ExpandableListView.OnChildClickListener。
// 子条目的点击事件
mListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
// 适配器的isChildSelectable方法返回false时点击子条目不能触发此方法,返回true时触发。不影响父条目开关
// parent:mListView
// v:被点击子条目布局实例
// groupPosition:被点击子条目所在的组索引
// childPosition:被点击子条目在本组中的索引
// id:被点击的子条目在所属列表中的id,一般和childPosition相同
@Override
public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) {
Log.d("ard", "子条目接收到点击事件");
return true;
}
});
关于ExpandableListView使用控制器Holder优化参考ListView即可。
还有一点是,自带一个箭头表示组的展开与否,每个版本中样式不同但都很丑,尤其是位置可能不合适。
解决方法是在xml里操作groupIndicator属性将箭头隐藏:
<ExpandableListView
android:id="@+id/ev"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:groupIndicator="@null" >
</ExpandableListView>
然后就可以在group条目的布局里加入一个ImageView配合一个好看的图片,在适配器的getGroupView里通过isExpanded参数进行操作:
if(isExpanded){
mImageView.setImageResource(R.drawable.open);// 展开状态图片
}else{
mImageView.setImageResource(R.drawable.close);// 关闭状态图片
}
网格展示控件。说到它总会想到TableLayout表格布局,都是按照格子的形式分配空间。实际上GridView让可视元素的可控性更高,用的更多。
GridView默认先水平添加Item,宽度用满时再向垂直方向添加。所以和ListView一样,如果Item数量很多,GridView是可以垂直滚动的。
和ListView一样使用:在Activity的xml里注册,定义一个Item布局,定义适配器,定义数据,指定适配器并绑定数据。
xml里配置用到的属性:
numColumns,列的数量,取值有系统提供的auto_fit,根据columnWidth指定的列宽和屏幕宽度自动设置数值。或者直接指定一个数值。
columnWidth,每列的宽度,如果指定,当numColumns为auto_fit时有效;如果不指定,会根据numColumns指定的列数平均分配。
stretchMode,余留宽度的分配模式。columnWidth-将宽度平均分配给每列使用;SpacingWidth-将宽度平分给列与列之间的间隔。
verticalSpacing,行间距。
horizontalSpacing,列间。
Item布局宽指定为match_parent和指定为具体数值,配合上面的属性使用时的效果是不一样的,具体就看业务需求了,这里不再配图。Item布局的高如果指定为match_parent无意义,被当做wrap_content处理。常用的情景是,将Item的宽设为match_parent,然后指定网格控件的numColumns为具体数字,再结合horizontalSpacing、verticalSpacing使用。
GridView中用到的技术在ListView中都用到过了,下面是一个简单的实例代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final String[] box = new String[]{"盒子", "Box", "Cube", "箱子", "柜子", "橱子", "笼子", "屋子", "楼房", "立方体", "豆腐块"};
((GridView) findViewById(R.id.gridView)).setAdapter(new BaseAdapter() {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View mView = View.inflate(MainActivity.this, R.layout.item, null);
((TextView)mView.findViewById(R.id.box)).setText(box[position]);;
return mView;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public Object getItem(int position) {
return box[position];
}
@Override
public int getCount() {
return box.length;
}
});
}
视图页控件,出自android support v4扩展库,它不是AdapterView的子类而是extends ViewGroup。是一个容器控件,可以左右切换多个View对象。
直接干吧,先看看Activity的xml里怎么整:
<LinearLayout 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:orientation="vertical" >
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<!-- 子元素 -->
</android.support.v4.view.ViewPager>
</LinearLayout>
ViewPager里可容纳的页面不限于一个,每个页面都可以使用自己风格的布局。本例图省事,都用一种,复制为4份,分别改下图片和文字:
然后准备点虚拟数据,这里直接将每个页面实例封为数据:
View view1 = View.inflate(this, R.layout.view_viewpager1, null);
View view2 = View.inflate(this, R.layout.view_viewpager2, null);
View view3 = View.inflate(this, R.layout.view_viewpager3, null);
View view4 = View.inflate(this, R.layout.view_viewpager4, null);
List<View> list = new ArrayList<View>();
list.add(view1);
list.add(view2);
list.add(view3);
list.add(view4);
适配器仍是必不可少的部分,
public class MyPagerAdapter extends PagerAdapter {
private List<View> list;
public MyPagerAdapter(List<View> list) {
this.list = list;
}
/**
* 页面总数
*/
@Override
public int getCount() {
return list.size();
}
/**
* 1. 初次显示页面0时,执行2次
* 当前显示页索引为0时没有上一页。经系统传入,创建页面0,然后又执行一次出入1,创建页面1.
* instantiateItem. container:ViewPager, position:0 当前页
* instantiateItem. container:ViewPager, position:1 下一页
* 如果初始setCurrentItem页不是第1页(索引0),那么这个方法执行3次
*/
@Override
public Object instantiateItem(ViewGroup container, int position) {
// 1.在mViewPager里添加页面
container.addView(list.get(position));
// 将页面的索引赋给页面做tag,以备不时之需
list.get(position).setTag(position);
// 2.返回这个页面
return list.get(position);
}
/**
* 2. 确定哪个页面View和instantiateitem(ViewGroup,int)返回的指定key对象object关联
* @param view 数据源中加载的页面
* @param object 执行instantiateItem时缓存的页面
* @return 当加载的页面和已经缓存的为同一个时,直接使用缓存的
*/
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
/**
* 3.清理过多加重的页面,清理内存
* container:mViewPager
* position:移出屏幕2个位置,须要移除的页面索引
* object:被移除的页面View
*/
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView(list.get(position));
}
}
最后给ViewPager指定适配器:
// 定义适配器
MyPagerAdapter mpa = new MyPagerAdapter(list);
// 实例化视图页控件
ViewPager mViewPager = (ViewPager) findViewById(R.id.viewPager);
mViewPager.setAdapter(mpa); // 指定适配器
ViewPager还有getCurrentItem()方法获取当前页索引;setCurrentItem(0, true/false)随时设置当前显示的是哪一页,true和false指的的是否使用动画平滑切换。
ViewPager还有个ViewPager.OnPageChangeListener用来监听页面切换事件,来瞧瞧:
// setOnPageChangeListener deprecated // 添加页面切换监听,更新为下面的fangf
mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
/**
* index:被选择的页面索引,即切换后 显示的页面
* 如果页面只是滚了一下但没有切换,不会调用此方法
*/
@Override
public void onPageSelected(int index) {
Log.d("ard", "切换后的页面: " + index);
}
/**
* index:当前页面(发生切换前的)索引,scale:当前页面偏移的百分比,pixel:当前页面偏移的像素位置
*/
@Override
public void onPageScrolled(int index, float scale, int pixel) {
Log.d("ard", "当前页面: " + index);
}
/**
* state:0-停止滚动,1-随手指滚动,2-页面自己滚动,或者复位或者滚动到下一个页面
*/
@Override
public void onPageScrollStateChanged(int state) {
Log.d("ard", "切换状态: " + state);
}
});
默认页面切换方式是一个由快到慢的过程,平凡而踏实。ViewPager有setPageTransformer方法设置,共2个参数,第一个Boolean类型的参数reverseDrawingOrder指定方向,其实正反无所谓好看就行;第二个参数指定自定义方式。
第2个参数,须是ViewPager.PageTransformer接口的实现类。逗逼的是,官方没有在api里提供实现类,而是提供了2种实现代码,DepthPageTransformer、ZoomOutPageTransformer。瞧瞧:
纵深提升方式DepthPageTransformer:
public class DepthPageTransformer implements android.support.v4.view.ViewPager.PageTransformer {
private static float MIN_SCALE = 0.75f;
public void transformPage(View view, float position) {
int pageWidth = view.getWidth();
if (position < -1) { // [-Infinity,-1)
// This page is way off-screen to the left.
view.setAlpha(0);
} else if (position <= 0) { // [-1,0]
// Use the default slide transition when
// moving to the left page
view.setAlpha(1);
view.setTranslationX(0);
view.setScaleX(1);
view.setScaleY(1);
} else if (position <= 1) { // (0,1]
// Fade the page out.
view.setAlpha(1 - position);
// Counteract the default slide transition
view.setTranslationX(pageWidth * -position);
// Scale the page down (between MIN_SCALE and 1)
float scaleFactor = MIN_SCALE + (1 - MIN_SCALE) * (1 - Math.abs(position));
view.setScaleX(scaleFactor);
view.setScaleY(scaleFactor);
} else { // (1,+Infinity]
// This page is way off-screen to the right.
view.setAlpha(0);
}
}
}
然后指定视图页面的切换方式:
mViewPager.setPageTransformer(true, new DepthPageTransformer()); // 页面切换动画
缩放出入方式ZoomOutPageTransformer:
public class ZoomOutPageTransformer implements android.support.v4.view.ViewPager.PageTransformer {
private static float MIN_SCALE = 0.85f;
private static float MIN_ALPHA = 0.5f;
@Override
public void transformPage(View view, float position) {
int pageWidth = view.getWidth();
int pageHeight = view.getHeight();
if (position < -1) { // [-Infinity,-1)
// This page is way off-screen to the left.
view.setAlpha(0);
} else if (position <= 1) { // [-1,1]
// Modify the default slide transition to
// shrink the page as well
float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));
float vertMargin = pageHeight * (1 - scaleFactor) / 2;
float horzMargin = pageWidth * (1 - scaleFactor) / 2;
if (position < 0) {
view.setTranslationX(horzMargin - vertMargin / 2);
} else {
view.setTranslationX(-horzMargin + vertMargin / 2);
}
// Scale the page down (between MIN_SCALE and 1)
view.setScaleX(scaleFactor);
view.setScaleY(scaleFactor);
// Fade the page relative to its size.
view.setAlpha(MIN_ALPHA + (scaleFactor - MIN_SCALE) / (1 - MIN_SCALE) * (1 - MIN_ALPHA));
} else { // (1,+Infinity]
// This page is way off-screen to the right.
view.setAlpha(0);
}
}
}
看得出来,要自定义切换方式,直接实现PageTransformer的transformPage方法,第1个参数是页面实例,第2个参数不是页面索引。上面代码中View的动画使用的是属性动画,是3.0推出,所以3.0以下不支持,以目前的市场占有率还是不要考虑顾及了。
应用启动初始,进行页面切换变动,如果通过Log打印transformPage的2个参数会持续打印2个页面的信息;而一旦当前页面不是索引为0的,Log打印的会是3个页面的信息。以下是通过设置tag打印的信息,其中注明了分析:
----------------- 共4个页面,索引0到3 -------
----------------- 初始,从最左索引0页面切换到索引1页面 ------
D/ard(1904): view0, position:0.0 当前显示页面左移隐藏,从0到-1
D/ard(1904): view1, position:1.0 右侧隐藏页面左移显示,从1到0
D/ard(1904): view0, position:-0.184375
D/ard(1904): view1, position:0.459375
D/ard(1904): view0, position:-1.0
D/ard(1904): view1, position:0.0
--------------- 当前页面从索引1切换到最左索引0 ------
D/ard(21471): view1, position:0.0 当前显示页面右移隐藏,从0到1
D/ard(21471): view0, position:-1.0 左侧隐藏页面右移显示,从-1到0
D/ard(21471): view2, position:1.0 右隐藏页面继续右移还是隐藏,从1到2
D/ard(21471): view1, position:0.71574074
D/ard(21471): view0, position:-0.28425926
D/ard(21471): view2, position:1.7157408
D/ard(21471): view1, position:1.0
D/ard(21471): view0, position:0.0
D/ard(21471): view2, position:2.0
----------------- 当前页面从索引2切换到最右索引3 ------
D/ard(2301): view1, position:-1.0 左侧隐藏页面继续左移,从-1到-2
D/ard(2301): view2, position:0.0 当前显示页面左移隐藏,从0到-1
D/ard(2301): view3, position:1.0 右侧隐藏页面左移显示,从1到0
D/ard(2301): view1, position:-1.671875
D/ard(2301): view2, position:-0.671875
D/ard(2301): view3, position:0.328125
D/ard(2301): view1, position:-2.0
D/ard(2301): view2, position:-1.0
D/ard(2301): view3, position:0.0
---------------- 从最右索引3切换到索引2 ------
D/ard(2301): view2, position:-1.0 左侧隐藏页面右移显示,从-1到0
D/ard(2301): view3, position:0.0 当前显示页面右移隐藏,从0到1
D/ard(2301): view2, position:-0.621875
D/ard(2301): view3, position:0.378125
D/ard(2301): view2, position:0.0
D/ard(2301): view3, position:1.0
======================================================================
当前显示的总是0
左边隐藏的-1,继续向左-2
右边隐藏的1,继续向右2
可以看出,如果position从1奔着2去或者从-1奔着-2去的,都是不显示的;从0到1或者从0到-1的都从显示转为隐藏的;从-1到0或者从1到0都是从隐藏转为显示的。这个position值是持续变化的,进行相应的设置就好了。
这是个简单实现的摇摆切换:
mViewPager.setPageTransformer(
true,
new ViewPager.PageTransformer() {
@Override
public void transformPage(View view, float position) {
// 当前的向右隐藏,右侧隐藏的向左显示
if(position < 1 && position > 0){
view.setRotation((int) (20 * position));
}
// 当前正好静止显示的,看不见的都正常
else if(position == 0 || position >= 1 || position <= -1){
view.setRotation(0);
}
// 显示的向左隐藏,隐藏的向右显示
else if(position < 0 && position > -1){
view.setRotation((int) ( 20 * position));
}
}
}
); // 页面切换动画
关于ViewPager的切换方式在github有个开源项目,集成多种很炫的方式,使用方法就问搜索引擎吧,https://github.com/jfeinstein10/JazzyViewPager 。
ViewPager还有两个子控件:PagerTitleStrip、PagerTabStrip,PagerTitleStrip类直接继承自ViewGroup类,PagerTabStrip类继承子PagerTitleStrip,这两个类也是容器类。如果使用须注意,在定义XML的layout的时候,这两个控件必须包裹在ViewPager内。其它注意点也在下面的代码中注明:
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<!--
带下划线又带文字的标签型头,对应页面的位置显示。可响应点击进行页面切换
须在ViewPager的包裹内
layout_gravity指定显示位置,默认头部top顶部,可选bottom底部 -->
<android.support.v4.view.PagerTabStrip
android:id="@+id/pagerTabStrip"
android:layout_width="wrap_content"
android:layout_height="30dip"
android:layout_gravity="top"
android:gravity="center" >
</android.support.v4.view.PagerTabStrip>
<!-- 只有文字的文本型头,拥挤显示不一定对应页面位置。默认不能点击 -->
<android.support.v4.view.PagerTitleStrip
android:id="@+id/pagerTitleStrip"
android:layout_width="wrap_content"
android:layout_height="30dip"
android:layout_gravity="bottom"
android:gravity="center" />
</android.support.v4.view.ViewPager>
PagerTabStrip或PagerTitleStrip不要同时使用,因为不能同时为它们设置数据。要对这2种页面标题进行动态的控制,可以在java里使用类似代码:
PagerTabStrip mPagerTabStrip = (PagerTabStrip)findViewById(R.id.pagerTabStrip);
mPagerTabStrip.setBackgroundColor(Color.RED); // 背景色
mPagerTabStrip.setTabIndicatorColor(Color.WHITE); // 标题下划线颜色
mPagerTabStrip.setTextColor(Color.WHITE); // 标题文本颜色
//mPagerTabStrip.setClickable(false); // 是否响应点击事件
要设置标题,须要适配器里多重写一个方法来显示自定义的标题。下面代码里titles是一个String类型的数组,通过适配器的构造方法传入。
// 非必须的。配合PagerTabStrip使用设置页面标题。titles是自定义的String数组保存多个标题字符串
@Override
public CharSequence getPageTitle(int position) {
return titles[position];
}
还有个问题是,应用启动后标题并不显示,当手动切换页面后才显示出来。遗留给后人解决吧。
话说回来,这个标题实在是长的很励志,有没有办法整整容啊,答案是肯定的。来学习一个新的类:
Html里的<span>标签一定都熟悉,可以设置一段文字的颜色大小字体等,Android里的Spannable接口也封装了如此功能,有实现子类SpannableString、SpannableStringBuilder。 SpannableString如同String,构造对象时传入一个String,之后再无法更改String的内容,也无法拼接多个 SpannableString;而SpannableStringBuilder则如图StringBuilder,还实现了Editable接口,它可以通过其append()方法来拼接多个String。
在Adapter的为页面头设置标题的getPageTitleChar方法要求返回值是Sequence,无比机智的Spannable正是其子类,所以在这里认定SpannableStringBuilder:
然后,1.定义一个SpannableStringBuilder ssb,ssb.append(titles[position]),2.设置复合文本,3.return ssb。搞定。
这里提供以下代码参考,注意标题的大小要合适,否则调试半年也只会越整越残。
// 创建一个 SpannableString对象
SpannableStringBuilder ssb = new SpannableStringBuilder("使用SSB创作多美多姿的文本");
/* 模式:
* Spanned.SPAN_EXCLUSIVE_EXCLUSIVE(前后都不包括)、
* Spanned.SPAN_INCLUSIVE_EXCLUSIVE(前面包括,后面不包括)、
* Spanned.SPAN_EXCLUSIVE_INCLUSIVE(前面不包括,后面包括)、
* Spanned.SPAN_INCLUSIVE_INCLUSIVE(前后都包括)。
*/
// 设置一段文本(属性类型,起始位置,结束位置,模式)
// TypefaceSpan 字体(default,default-bold,monospace,serif,sans-serif)
ssb.setSpan(new TypefaceSpan("monospace"), 0, 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
ssb.setSpan(new TypefaceSpan("serif"), 1, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
// AbsoluteSizeSpan 文字绝对值大小,默认像素
ssb.setSpan(new AbsoluteSizeSpan(20), 0, 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
// 第二个参数 true:字体单位是dp/dip,false:像素
ssb.setSpan(new AbsoluteSizeSpan(20, true), 0, 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
// RelativeSizeSpan 文字相对值大小像素(默认字体大小的多少倍)0.5f表示默认字体大小的一半
ssb.setSpan(new RelativeSizeSpan(0.5f), 1, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
ssb.setSpan(new RelativeSizeSpan(2.0f), 1, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
// ScaleXSpan 字体相对值宽,像素( 默认字体宽度的多少倍)2.0f-两倍,X轴方向放大两倍,高度不变。没有ScaleYSapn
ssb.setSpan(new ScaleXSpan(2.0f), 1, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
// ForegroundColorSpan 前景色-字体颜色
ssb.setSpan(new ForegroundColorSpan(Color.MAGENTA), 1, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
// BackgroundColorSpan 背景色
ssb.setSpan(new BackgroundColorSpan(Color.CYAN), 1, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
// 字体样式 正常,粗体,斜体,粗斜体
ssb.setSpan(new StyleSpan(android.graphics.Typeface.NORMAL), 0, 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
ssb.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 1, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
ssb.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 2, 3, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
ssb.setSpan(new StyleSpan(android.graphics.Typeface.BOLD_ITALIC), 3, 4, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
// UnderlineSpan 下划线
ssb.setSpan(new UnderlineSpan(), 0, 3, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
// StrikethroughSpan 删除线
ssb.setSpan(new StrikethroughSpan(), 2, 3, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
// SubscriptSpan 下标
ssb.setSpan(new SubscriptSpan(), 4, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
// SuperscriptSpan 上标
ssb.setSpan(new SuperscriptSpan(), 5, 6, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
// URLSpan 超级链接(需要添加控件的setMovementMethod方法附加响应)
// tel:电话,mailto:邮件,http:网络,sms、smsto:短信,mms、mmsto:彩信,geo:地图(x,y坐标)
ssb.setSpan(new URLSpan("tel:4155551212"), 0, 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
ssb.setSpan(new URLSpan("mailto:webmaster@google.com"), 1, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
ssb.setSpan(new URLSpan("http://www.baidu.com"), 2, 3, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
ssb.setSpan(new URLSpan("sms:4155551212"), 3, 4, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
ssb.setSpan(new URLSpan("mms:4155551212"), 4, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
ssb.setSpan(new URLSpan("geo:38.899533,-77.036476"), 5, 6, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
// BulletSpan 项目符号(宽模式,颜色)
ssb.setSpan(new BulletSpan(BulletSpan.STANDARD_GAP_WIDTH, Color.GREEN), 1, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
// 设置图片
ssb.setSpan(new ImageSpan(this, R.drawable.ic_launcher), 0, 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
mTextView.setText(ssb); // 设置TextView的文本为SSB
复用展示控件,ListView、GridView的替代者。来自于Android Support v7扩展库,它不是AdapterView的子类而是extends ViewGroup,为了考虑Android2.1(API level 7) 及更高版本而设计的,v7依赖v4包,如果要使用,两个包得同时被引用。v4包是默认被IDE导入的。v7包可以从SDK中找到:%sdk%\extras\android\support\v7\recyclerview\libs\android-support-v7-recyclerview.jar,复制进项目的libs目录中。
很遗憾的是,这样不一定能正常发布应用,会出现
java.lang.NoClassDefFoundError: android.support.v7.recyclerview.R$styleable
另外一种引入的方式:File,Import,选择Android分组下的Existing Android Code Into Workspace,Next进入下一个界面,点击Brows按钮,定位到%sdk%\extras\android\support\v7\recyclerview目录,确定。记得勾选“Copy projects into workspace”。这样项目面板多了一个“recyclerview”的项目,右键,Properties,左侧Android,右侧勾选“Is Library”,Apply,OK。再右键当前开发的项目,Properties,左侧Android,右侧Add...,选中刚刚的recyclerview项目。
在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.support.v7.widget.RecyclerView
android:id="@+id/recycleView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
RecyclerView同样须要为每个Item创建布局,现在用个简单的图片展示控件,配点简单的数据:
来看RecyclerView中3要素:
l RecyclerView.Adapter
适配器是AdapterView比不可少的内容,它的结构比ListView使用的BaseAdapter简单多了,仅须重写3个方法。
其中的复用体,在BaseAdapter中是currentView,并且需手动优化为自定义的JavaBean型Holder类。而这里,强制要求是系统提供的RecyclerView.ViewHolder类型的对象,这个对象内部进行Item布局的封装。
l LayoutManager
布局管理器,指定RecyclerView的滚动方向。
l ItemAnimator
DefaultItemAnimator
RecycleView.setOnClickListener+View.OnClickListener没有反应;RecycleView.setOnScrollListener+RecyclerView.OnScrollListener被deprecation;并且没有提供setOnItemClick方法。这也是为什么它代码量少了很多单还不能替代ListView的原因。
但有一种比较简单的解决方法,RecyclerView.ViewHolder中有个内置的getPosition()方法可以得到当前Item的位置。索引如果为每个复用体添加点击事件就可以了。注意要添加到具体的控件上,直接在Item的View实例上似乎不能触发。
来看一下到这一步的完整代码:
public class MainActivity extends Activity {
private List list;
private RecyclerView mRecycleView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
initView();
}
private void initView() {
// 布局管理器,指定RecyclerView的滚动方向
LinearLayoutManager lm = new LinearLayoutManager(this);
lm.setOrientation(LinearLayoutManager.HORIZONTAL); // 水平
// 定义适配器
MyAdapter adapter = new MyAdapter(this, list);
// 实例化控件
mRecycleView = (RecyclerView) findViewById(R.id.recycleView);
// 指定滚动方法
mRecycleView.setLayoutManager(lm);
// 添加item动画,可无
mRecycleView.setItemAnimator(new DefaultItemAnimator());
// 当内容改变时保持尺寸不变
mRecycleView.setHasFixedSize(true);
// 关联适配器
mRecycleView.setAdapter(adapter);
}
private void initData() {
list = new ArrayList();
list.add(R.drawable.eminem);
list.add(R.drawable.eminem);
list.add(R.drawable.eminem);
}
}
/**
* RecyclerView专用适配器
* @author Administrator
*/
class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
// 必不可少的数据
private List list;
private Context context;
public MyAdapter(Context context, List list) {
this.context = context;
this.list = list;
}
/**
* 1.根据数据确定item数量
**/
@Override
public int getItemCount() {
return list.size();
}
/**
* 2.创建Item的控制器-复用体 把Item的布局文件加载为可用的View实例并付给复用体 viewGroup: index:
* 返回值是自定义的RecyclerView.ViewHolder类型的复用体
*/
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int index) {
return new ViewHolder(View.inflate(context, R.layout.item, null));
}
/**
* 3.绑定Item的控制器,即复用体。 系统传入的参数:
* holder:控制器,封装Item内的控件。自定义的RecyclerView.ViewHolder类型的对象
* index:Item的位置,始于1。对应list里的数据则始于0。
**/
@Override
public void onBindViewHolder(ViewHolder holder, int index) {
// 动态操作控件
holder.img.setImageResource((Integer)list.get(index));
}
/**
* 自定义控制器 内部封装了Item布局中的控件
* @author cuiweiyou.com
*/
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
public ImageView img;
/**
* 自定义控制器
* @param view Item布局实例
*/
public ViewHolder(View view) {
super(view);
// 从Item布局查找控件
this.img = (ImageView) view.findViewById(R.id.iv);
// 监听控件的单击事件
img.setOnClickListener(this);
}
@Override
public void onClick(View v) {
Log.e("ard",
"当前点击的位置:" + getPosition() // 可用。但deprecated
+ ",id:" + getItemId() // -1
+ ", oldpos:" + getOldPosition() // -1
+ ", lytpos:" + getLayoutPosition()); // 可用
}
}
}