2.AdapterView适配器控件

较早的网文说:Android是完全遵循MVC模式设计的框架,ActivityControllerActivity对应的/res/layout/xml文件是View;数据即Model。而AdapterModelController之间的桥梁。

适配器将数据整合应用到List类型的可视控件上,列表控件的每行条目一般都是同样的结构,适配器就是重复操作条目的模板使之一一绑定数据。下面是适配器继承关系:

  

常用适配器:

ArrayAdapter:数组型适配器,针对数组数据。它可以通过泛型指定数组的元素,主要适用于TextView控件。使用时同一时刻只能针对单一的控件。

SimpleAdapter:简约型,简约不简单。其操作List集合数据,而List的每个元素又是Map集合,Map中可以对应多个控件存储多个k-v数据。控件类型可以是单纯的TextView,也可以是ImageView等。

CursorAdapterCursor数据类型适配器。Cursor-游标,和SQLite数据库相关的数据封装对象,是查询数据库后返回的一行数据的封装,其中包含了众多的列的内容。本章节不展示CursorAdapter使用,因为和数据库的关联,将在后文SQLite时讲解。

BaseAdapter:最底层基础型适配器。如果列表控件的条目不再是简单的显示一个文本标签,或者一个图片配上一个文本;而是LinearLayoutRelativeLayoutImageViewTextViewButton等等大杂烩,那么BaseAdapter是必须的。

  

先看一下和适配器相关的控件,注意这张图展示的是AdapterView的继承关系:

   


1AutoCompleteTextView

自动补齐文本框。继承自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等控件也不影响使用,详见代码。

  

然后下文使用多个适配器的例子。

  

1ArrayAdapter 

当前条目布局中只有一个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 sint startint countint after) {

        Log.e("ard","beforeTextChanged:" + s);

    }

    

    /**

     * 有输入时系统调用的方法

     * s:本次输入结束后整行内容

     * start:始于0的位置,刚刚输入(或删除)的字符位置。一个汉字算1个位置

     * before:删除的字符数量。一般每次都是1个,除非先选择一段文字。一个汉字算1个

     * count:本次输入的字符数量。一个汉字算1个,一个英语单词计算其全部字符,一行中文计算全部汉字

     */

    @Override

    public void onTextChanged(CharSequence sint startint beforeint 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方法存储数据。

  

2SimpleAdapter  

简约型适配器-简单适配器,常用于显示单个ImageView+单个TextView这种很规整的条目布局。SimpleAdapter不是AutoCompleteTextView常用的适配器,它在SpinnerListView等直接展示的控件里中常用。

下面是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要求keyString类型valueObject类型,所以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);

  

3BaseAdapter  

终于轮到高大上的基础型适配器。在自补齐输入框使用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是属性私有并为属性提供了setget方法的一般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+sr,或者右键,SourceGenerate Getters and Setters... IDE为我们创建属性的setget方法。

一般为了使用方便,还会为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_autonull);

        

        // 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章节中讲解。

  


2Spinner

下拉框也是常用的列表型展示控件。常常和上面讲到的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 viewint positionlong id) {

        Log.d("ard""position:" + position + ", id:" + id + ", 内容:" + mSpinner.getSelectedItem().toString());

    }

    // 打开又关闭,没做出选择

    @Override

    public void onNothingSelected(AdapterView<?> parent) {

        Log.d("ard""选择恐惧症?");

    }});

  


3ListViewExpandableListView

又终于轮到高大上的列表展示控件ListView了。可以说这是app开发中使用频率最高、技术性最强的大批量数据展示控件。围绕其进行的网络请求、数据解析、线程通讯等技术更使之低奢内。

要用ListView基本必须配套BaseAdapter,本章节也是如此,同时解决前文留下的getView方法中优化问题。

  

准备一个有int-flag-国旗、string-country-国家、string-description-说明3String类型属性的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>

  

1ListView 

先看一下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.lengthi++) {

        list.add(new ItemBean(flag[i], country[i], description[i]));

    }

    

    // 2.定义适配器

    MyListAdapter adapter = new MyListAdapter(MainActivity.thislist);

    

    // 3.为ListView指定适配器

    ((ListView)findViewById(R.id.listView)).setAdapter(adapter);

}

其中涉及arr.xml的部分先了解即可。

  

自定义适配器的结构:

  

[1]Listview优化 

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.itemnull);

    }

    

    // 使用重用对象获取条目中的具体某个控件

    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建好模板之后重复使用即可。但是每次创建条目,仍然需要重新从模板中查找具体的控件,因此还需要优化。此时引入一个控制器-HolderHolder是把模板的每个控件进行封装,成为一个简单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.itemnull);

        

        // 随复用体初始化

        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.setOnItemClickListenerAdapterView.OnItemClickListener。同时比较常用的还有长按事件ListView.setOnItemLongClickListenerAdapterView.OnItemLongClickListener

两个事件结合使用的例子:当点击时判断一个开关是否打开,打开了做出响应;而这个开关只有长按条目后才打开。

既然在2个监听中用到同一个开关,因此这个开关是个全局属性,来看一下Activity的结构:

mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {

    @Override

    public void onItemClick(AdapterView<?> parent, View viewint positionlong 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 viewint positionlong id) {

        clickAble = true;

        Log.d("ard""开关打开了 ");

        

        return true;    // 返回true表示在条目内完成了处理,click事件不再下发引起其它事件

    }

});

   

滚动

如果条目数量很多ListView即可滚动显示超出屏幕的内容,这些条目的信息可以通过setOnScrollListenerAbsListView.OnScrollListener得到。

mListView.setOnScrollListener(new AbsListView.OnScrollListener() {

    /**

     * 滚动过程中持续调用,监听滚动状态改变的方法

     * view:mListView

     * scrollState:状态码,0-停止;1-正在滚动;2-手指离开屏幕

     */

    @Override

    public void onScrollStateChanged(AbsListView viewint 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 viewint firstVisibleItemint visibleItemCountint totalItemCount) {

        Log.d("ard""当前可见最顶条目索引:" + firstVisibleItem + ", 可见总条目数量:" + visibleItemCount + ",全部条目数量:" + totalItemCount);

        

        /**

         * 注意,如果添加了 setOnScrollListener(new AbsListView.OnScrollListener的同时,

         * 又添加了 setOnItemClickListener(new AdapterView.OnItemClickListener

         * 那么 点击mListView,会首先执行 onScroll ,然后才执行 onItemClick。若手指在屏幕上滑动,不会触发单击

         */

    }

});

代码的注释也提到了,同时添加条目单击和列表滚动监听时的并发情况。没有哪种业务要求在单击事件、滚动事件的onScroll里同时做相同的处理,所以这种并发也不会带来什么悲剧。

  

Item内控件的事件 

ListView确实很复杂呃~~~~~~~~~~~

每个条目是一个自定义的布局,其中又有各种子控件,TextViewImageViewButton... ,大杂烩。每个控件都可以单独添加事件处理。

要控制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.itemnull);

        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上长按触发的仍旧是单击事件。

  

[2]HeaderFooter 

  

2ExpandableListView 

可扩展列表控件。实际继承自ListView,看起来也一样,仅默认多了一个下拉箭头。但它的每个条目是一个分组,可以点开显示下一级子列表。常用于聊天好友分组、通讯录分组、部门级别等。

级别关系也说明了所属关系:第2级列表属于第1级列表的Item。数据封装时,外层ItemFatherBean会多出一个属性存储第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"描述" + ilistchild));

    }

}

  

既然是2级列表,那么也就有对应不同级别条目的不同布局。新建2xml文件:item_group.xmlitem_child.xml,注意只能用小写字母和下划线。布局如何全靠个人审美,没什么要注意的。

  

然后先看一下扩展列表的准备:

  

最重要的适配器部分。MyListAdapter不再是继承自BaseAdapter,而是前文没有讲过的BaseExpandableListAdapter

BaseExpandableListAdapterBaseAdapter没有任何关系,但它的使用雷同,结构非常相像。

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 groupPositionint childPosition) {    // 内层条目bean

        return list.get(groupPosition).getListChild().get(childPosition);

    }

    @Override

    public long getGroupId(int groupPosition) {    // 外层条目id

        return groupPosition;

    }

    @Override

    public long getChildId(int groupPositionint childPosition) {    // 内层条目id

        return childPosition;

    }

    @Override

    public boolean hasStableIds() {    // 显示时复用已有对象

        return true;

    }

    @Override

    public View getGroupView(int groupPositionboolean isExpanded, View convertView, ViewGroup parent) {

        // TODO

        returnull;

    }

    @Override

    public View getChildView(int groupPositionint childPositionboolean isLastChild, View convertView, ViewGroup parent) {

        // TODO

        return null;

    }

    @Override

    public boolean isChildSelectable(int groupPositionint childPosition) {    // 内层条目是否接收点击事件

        return false;

    }

}

  

下面看getGroupViewgetChildView方法,它们分别创建Item的布局实例,系统分别给它们提供了convertView复用体,因此和ListView一样可以做出优化。getGroupView中系统提供了一个isExpanded属性,保存组条目的展开状态,只要复用了,滚出滚入屏幕会都是之前的展开状态。getChildViewisLastChild用于指定子条目在本组中是否是最后一条。

/**

 * 外层条目

 * groupPosition:条目索引

 * isExpanded:是否展开,默认false

 * convertView:复用体

 */

@Override

public View getGroupView(int groupPositionboolean isExpanded, View convertView, ViewGroup parent) {

    

    /** TODO Holder优化 **/

    

    if(convertView == null){

        convertView = View.inflate(context, R.layout.item_groupnull);

    }

    

    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 groupPositionint childPositionboolean isLastChild, View convertView, ViewGroup parent) {

    /** TODO Holder优化 **/

    if(convertView == null){

        convertView = View.inflate(context, R.layout.item_childnull);

    }

    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方法,字面意思说是否子条目可以被选择。这是相对于扩展列表整体而言的,对应ExpandableListViewsetOnChildClickListener方法,参数为ExpandableListView.OnChildClickListener

// 子条目的点击事件

mListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {

    // 适配器的isChildSelectable方法返回false时点击子条目不能触发此方法返回true时触发。不影响父条目开关

// parentmListView

// v:被点击子条目布局实例

// groupPosition:被点击子条目所在的组索引

// childPosition:被点击子条目在本组中的索引

// id:被点击的子条目在所属列表中的id,一般和childPosition相同

    @Override

    public boolean onChildClick(ExpandableListView parent, View vint groupPositionint childPositionlong 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);// 关闭状态图片

}

  

3)联系人分组示例

  

  


4GridView

网格展示控件。说到它总会想到TableLayout表格布局,都是按照格子的形式分配空间。实际上GridView让可视元素的可控性更高,用的更多。

GridView默认先水平添加Item,宽度用满时再向垂直方向添加。所以和ListView一样,如果Item数量很多,GridView是可以垂直滚动的。

ListView一样使用:在Activityxml里注册,定义一个Item布局,定义适配器,定义数据,指定适配器并绑定数据。

  

xml里配置用到的属性:

numColumns,列的数量,取值有系统提供的auto_fit,根据columnWidth指定的列宽和屏幕宽度自动设置数值。或者直接指定一个数值。

columnWidth,每列的宽度,如果指定,当numColumnsauto_fit时有效;如果不指定,会根据numColumns指定的列数平均分配。

stretchMode,余留宽度的分配模式。columnWidth-将宽度平均分配给每列使用;SpacingWidth-将宽度平分给列与列之间的间隔。

verticalSpacing,行间距。

horizontalSpacing,列间。 

Item布局宽指定为match_parent和指定为具体数值,配合上面的属性使用时的效果是不一样的,具体就看业务需求了,这里不再配图。Item布局的高如果指定为match_parent无意义,被当做wrap_content处理。常用的情景是,将Item的宽设为match_parent,然后指定网格控件的numColumns为具体数字,再结合horizontalSpacingverticalSpacing使用。

  

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.itemnull);

            ((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;

        }

    });

}

  


5v4.ViewPager

视图页控件,出自android support v4扩展库,它不是AdapterView的子类而是extends ViewGroup。是一个容器控件,可以左右切换多个View对象。

直接干吧,先看看Activityxml里怎么整:

<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_viewpager1null);

View view2 = View.inflate(this, R.layout.view_viewpager2null);

View view3 = View.inflate(this, R.layout.view_viewpager3null);

View view4 = View.inflate(this, R.layout.view_viewpager4null);

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 containerint 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 containerint 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)随时设置当前显示的是哪一页,truefalse指的的是否使用动画平滑切换。

  

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 indexfloat scaleint pixel) {

        Log.d("ard""当前页面: " + index);

    }

    /**

     * state:0-停止滚动,1-随手指滚动,2-页面自己滚动,或者复位或者滚动到下一个页面

     */

    @Override

    public void onPageScrollStateChanged(int state) {

        Log.d("ard""切换状态: " + state);

    }

});

  

切换方式

默认页面切换方式是一个由快到慢的过程,平凡而踏实。ViewPagersetPageTransformer方法设置,共2个参数,第一个Boolean类型的参数reverseDrawingOrder指定方向,其实正反无所谓好看就行;第二个参数指定自定义方式。

2个参数,须是ViewPager.PageTransformer接口的实现类。逗逼的是,官方没有在api里提供实现类,而是提供了2种实现代码,DepthPageTransformerZoomOutPageTransformer。瞧瞧:

纵深提升方式DepthPageTransformer

public class DepthPageTransformer implements android.support.v4.view.ViewPager.PageTransformer {

    private static float MIN_SCALE = 0.75f;

    public void transformPage(View viewfloat 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(truenew 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 viewfloat 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);

        }

    }

}

  

看得出来,要自定义切换方式,直接实现PageTransformertransformPage方法,第1个参数是页面实例,第2个参数不是页面索引。上面代码中View的动画使用的是属性动画,是3.0推出,所以3.0以下不支持,以目前的市场占有率还是不要考虑顾及了。

应用启动初始,进行页面切换变动,如果通过Log打印transformPage2个参数会持续打印2个页面的信息;而一旦当前页面不是索引为0的,Log打印的会是3个页面的信息。以下是通过设置tag打印的信息,其中注明了分析:

----------------- 4个页面,索引03 -------

----------------- 初始,从最左索引0页面切换到索引1页面 ------

D/ard(1904): view0, position:0.0         当前显示页面左移隐藏,从0-1

D/ard(1904): view1, position:1.0         右侧隐藏页面左移显示,从10

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           当前显示页面右移隐藏,从01

D/ard(21471): view0, position:-1.0          左侧隐藏页面右移显示,从-10

D/ard(21471): view2, position:1.0           右隐藏页面继续右移还是隐藏,从12

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        右侧隐藏页面左移显示,从10

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       左侧隐藏页面右移显示,从-10

D/ard(2301): view3, position:0.0        当前显示页面右移隐藏,从01

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

可以看出,如果position1奔着2去或者从-1奔着-2去的,都是不显示的;从01或者从0-1的都从显示转为隐藏的;从-10或者从10都是从隐藏转为显示的。这个position值是持续变化的,进行相应的设置就好了。

  

这是个简单实现的摇摆切换:

mViewPager.setPageTransformer(

    true

    new ViewPager.PageTransformer() {

        @Override

        public void transformPage(View viewfloat 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还有两个子控件:PagerTitleStripPagerTabStripPagerTitleStrip类直接继承自ViewGroup类,PagerTabStrip类继承子PagerTitleStrip,这两个类也是容器类。如果使用须注意,在定义XMLlayout的时候,这两个控件必须包裹在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>

  

PagerTabStripPagerTitleStrip不要同时使用,因为不能同时为它们设置数据。要对这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];

}

还有个问题是,应用启动后标题并不显示,当手动切换页面后才显示出来。遗留给后人解决吧。

  

话说回来,这个标题实在是长的很励志,有没有办法整整容啊,答案是肯定的。来学习一个新的类:

  

Spannable

Html里的<span>标签一定都熟悉,可以设置一段文字的颜色大小字体等,Android里的Spannable接口也封装了如此功能,有实现子类SpannableStringSpannableStringBuilder。 SpannableString如同String,构造对象时传入一个String,之后再无法更改String的内容,也无法拼接多个 SpannableString;而SpannableStringBuilder则如图StringBuilder,还实现了Editable接口,它可以通过其append()方法来拼接多个String

Adapter的为页面头设置标题的getPageTitleChar方法要求返回值是Sequence,无比机智的Spannable正是其子类,所以在这里认定SpannableStringBuilder

然后,1.定义一个SpannableStringBuilder ssbssb.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

   


6v7.RecyclerView

复用展示控件,ListViewGridView的替代者。来自于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  

另外一种引入的方式:FileImport,选择Android分组下的Existing Android Code Into WorkspaceNext进入下一个界面,点击Brows按钮,定位到%sdk%\extras\android\support\v7\recyclerview目录,确定。记得勾选“Copy projects into workspace”。这样项目面板多了一个“recyclerview”的项目,右键,Properties,左侧Android,右侧勾选“Is Library”,ApplyOK。再右键当前开发的项目,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创建布局,现在用个简单的图片展示控件,配点简单的数据:

  

来看RecyclerView3要素:

RecyclerView.Adapter

适配器是AdapterView比不可少的内容,它的结构比ListView使用的BaseAdapter简单多了,仅须重写3个方法。

其中的复用体,在BaseAdapter中是currentView,并且需手动优化为自定义的JavaBeanHolder类。而这里,强制要求是系统提供的RecyclerView.ViewHolder类型的对象,这个对象内部进行Item布局的封装。

  

LayoutManager

布局管理器,指定RecyclerView的滚动方向。

  

ItemAnimator

DefaultItemAnimator

  

RecycleView.setOnClickListener+View.OnClickListener没有反应;RecycleView.setOnScrollListener+RecyclerView.OnScrollListenerdeprecation;并且没有提供setOnItemClick方法。这也是为什么它代码量少了很多单还不能替代ListView的原因。

但有一种比较简单的解决方法,RecyclerView.ViewHolder中有个内置的getPosition()方法可以得到当前Item的位置。索引如果为每个复用体添加点击事件就可以了。注意要添加到具体的控件上,直接在ItemView实例上似乎不能触发。

来看一下到这一步的完整代码:

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(thislist);

        // 实例化控件

        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 contextList 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 viewGroupint index) {

        return new ViewHolder(View.inflate(context, R.layout.itemnull));

    }

    /**

     * 3.绑定Item的控制器,即复用体。 系统传入的参数:

     * holder:控制器,封装Item内的控件。自定义的RecyclerView.ViewHolder类型的对象

     * index:Item的位置,始于1。对应list里的数据则始于0。

     **/

    @Override

    public void onBindViewHolder(ViewHolder holderint 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());    // 可用

        }

    }

}