组件使用之ListView

组件使用之ListView

  列表在很多应用场景中都是非常重要的信息展示方式,表型数据结构也是大部分用户都能轻松接受并且看懂的一种。无论是联系人列表,还是新闻标题列表,或者论坛主题列表,邮件列表,待办事项列表等等;可以说有了列表就有了实现大部分用户所需要的应用功能的基础。


简单实用的ListView

  安卓中的ListView组件就是个很常用的列表数据展示组件,它能灵活地实现各种各样的列表效果,满足大部分应用的数据展示和操作需求。在实践中,它的布局控制能力以及View重用功能也极大地方便了开发者们的工作。
  ListView的使用方法有很多种,但都脱离不开它的基本工作方式,也就是ListView组件+Adapter的模式,每个ListView想要生效都必须绑定一个Adapter。
  举例来说,使用ListView首先要在XML布局中写一个ListView组件。

<ListView
    android:id="@+id/lvContent"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
</ListView>

  随后在对应的Activity或者Fragment代码中为它设置Adapter。
  ListView要求使用任何继承了BaseAdapter的类作为Adapter,而安卓系统自带了一些Adapter方便使用,包括ArrayAdapter,SimpleAdapter,SimpleCursorAdapter。
  ArrayAdapter是最简单的一种Adapter,它只需要一个指定类型的List或者数组即可自动实现列表展示。

List<String> strList = new ArrayList<>();
String[] strArray = new String[10];
ArrayAdapter<String> arrayAdapter;
public void setupList() {
    ListView listView = (ListView) findViewById(R.id.lvContent);
    // 通过List<String>列表创建ArrayAdapter,指定子项布局为R.layout.layout_rlv_item
    arrayAdapter = new ArrayAdapter<>(resContext, R.layout.layout_rlv_item, strList);
    // 通过String数组创建ArrayAdapter,指定子项布局为R.layout.layout_rlv_item
    arrayAdapter = new ArrayAdapter<>(resContext, R.layout.layout_rlv_item, strArray);
    // 通过List<String>列表创建ArrayAdapter,指定子项布局为R.layout.layout_rlv_item,指定文字展示的组件ID为tvItem
    arrayAdapter = new ArrayAdapter<>(resContext, R.layout.layout_rlv_item, R.id.tvItem, strList);
    // 通过String数组创建ArrayAdapter,指定子项布局为R.layout.layout_rlv_item,指定文字展示的组件ID为tvItem
    arrayAdapter = new ArrayAdapter<>(resContext, R.layout.layout_rlv_item, R.id.tvItem, strArray);
    // 必须将Adapter设置给ListView方可生效
    listView.setAdapter(arrayAdapter);
    // 数据变化后使用Adapter通知ListView进行刷新
    arrayAdapter.notifyDataSetChanged();
}

  然后是SimpleAdapter,它比ArrayAdapter灵活,能应付较为复杂的列表展示需求。

List<Map<String,Object>> dataList = new ArrayList<>();
SimpleAdapter simpleAdapter;
public void setupList() {
    ListView listView = (ListView) findViewById(R.id.lvContent);
    // 每行数据中每个子项各自的Key
    String[] keys = new String[]{"name", "desc", "status"};
    // 每行中每个子项所用到的组件ID
    int[] views = new int[]{R.id.tvArg_1, R.id.tvArg_2, R.id.tvArg_3};
    simpleAdapter = new SimpleAdapter(resContext, dataList, R.layout.layout_rlv_item, keys, views);
    listView.setAdapter(simpleAdapter);
    simpleAdapter.notifyDataSetChanged();
}

  如上所示,dataList中储存着每一行所需展示的数据,每行的数据以键值对的形式储存,创建Adapter时要给出这些数据的Key,以String[]的形式;而最后一个参数是每个数据所对应的组件ID,SimpleAdapter会自动将读取到的数据设置到指定的组件上。
  如果组件是Text View,则数据会被当做文本设置;如果组件是Image View,数据会被当做图片资源ID设置。
  使用SimpleAdapter已经可以做出基本的图文并茂型的列表了,但它的设置比较死板,必须按照一个固定的规则设置数据,对于列表项的控制也有所欠缺。
  至于SimpleCursorAdapter,这个Adapter是为了方便快捷地展示数据库数据而设计的,但它限定了只能使用Cursor来访问数据库数据,因此局限性较大,使用场景不多。


灵活多变的BaseAdapter

  安卓自带的Adapter总是不能满足所有需求的,因此在列表展示需求复杂或者交互要求较高的时候,自定义Adapter才是主流选择。
  BaseAdapter是ListView所使用的Adapter的抽象基类,查看源代码可知ArrayAdapter和SimpleAdapter等都是继承了BaseAdapter的实现类。
  BaseAdapter又实现了两个接口分别为ListAdapter和SpinnerAdapter,进一步追查会发现两个接口的父级均为Adapter接口,它提供了Adapter的所有基本方法。
  使用BaseAdapter的方法是继承它并实现所有接口方法,标准的实现如下

public class MyAdapter extends BaseAdapter {
    @Override
    public int getCount() {
        return 0;
    }
    @Override
    public Object getItem(int position) {
        return null;
    }
    @Override
    public long getItemId(int position) {
        return 0;
    }
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        return null;
    }
}

  四个方法都实现好了,一个Adapter就完成并可以投入使用了。

  • 方法getCount
    • 该方法用于返回列表所需显示的项目数量,一般而言是某个List的长度,也可以根据需求自行确定
    • 需要注意的是,直接返回List.size()可能出错,建议判空后再返回
  • 方法getItem
    • 该方法用于按照位置索引返回对应的数据项,返回值为Object,具有通用性,在使用时需要强制转换
    • 强制转换一般不会出问题,但也可以不使用这个方法,直接通过数据列表获得数据项
  • 方法getItemId
    • 该方法用于获取列表项的唯一ID,一般情况下它是由AdapterView的getItemIdAtPosition方法调用,返回某个位置的列表项的ID,方便使用
    • 实际使用中一般不会刻意去设置ID,直接返回position即可应付大部分情况
  • 方法getView
    • 该方法是BaseAdapter的核心方法,每个列表项都是通过它创建并返回的
    • 参数中的position是列表索引,convertView表示重用的列表项,parent表示父级组件,一般就是ListView本身
    • 如果直接在方法中创建子项并设置数据然后返回,列表可以正常显示,但性能非常不友好,尤其是当子项数量庞大或者子项本身比较复杂的时候。
    • 这是由于ListView的特点,每当列表滑动的时候它会检测是否有新的项目被推倒屏幕上显示,如果有就调用getView获取,因此如果不进行重用优化则滑动列表会导致大量的列表项被创建和丢弃,性能影响很大
    • 所谓的重用即是指将ListView推出显示区域之外的列表项重新拿来使用,这样就避免了大批量的项目创建和销毁,getView将只会创建有限个数的项目,每次有新项目需要显示时都是就地修改重用项目
    • convertView参数就是用于这个目的,它会返回ListView发现的被推出显示区域的列表项,只要检查它是否为空即可完成重用
public View getView(int position, View convertView, ViewGroup parent) {
    if(convertView == null) {
        // 创建新的View
        convertView = LayoutInflater.from(resContext).inflate(R.layout.layout_rlv_item, null);
    } else {
        // 在此处将需要修改的组件获取到
    }
    // 修改展示数据
    return convertView;
}

  上面的代码简单说明了如何重用,虽然一般而言只要利用了convertView返回重用项目这个特性的情况都可以称为“重用”,但在实际项目中还是讲究一个方式方法的,大部分情况都可以使用ViewHolder技巧来方便地重用项目。
  所谓ViewHolder技巧是指将列表项所有可以变动的组件都写入一个ViewHolder类中,在创建新项目的同时创建该类的实例,设置各个组件并将该实例设置为convertView的Tag。

public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder;
    if(convertView == null) {
        // 创建新的View
        convertView = LayoutInflater.from(resContext).inflate(R.layout.layout_rlv_item, null);
        holder = new ViewHolder();
        holder.arg1 = (TextView) findViewById(R.id.tvArg_1);
        holder.arg2 = (TextView) findViewById(R.id.tvArg_2);
        holder.arg3 = (TextView) findViewById(R.id.tvArg_3);
        convertView.setTag(holder);
    } else {
        // 在此处将需要修改的组件获取到
        holder = (ViewHolder) convertView.getTag();
    }
    // 修改展示数据
    return convertView;
}

class ViewHolder {
    TextView arg1;
    TextView arg2;
    TextView arg3;
}

  如上就是一个例子,使用ViewHolder能很好地兼顾重用以及便利性,此外还有很多不同的重用方法,但都脱离不了“将需要变化的组件缓存下来”这样的基本思路。


进一步使用ListView

  ListView乍一看像是一个专门用来展示列表的组件,但通过介绍Base Adapter的自定义可以看到,它的灵活性很高,并不一定只能用于列表,只要适合需求,ListView也能作为一般的滑动型组件使用。
  举个简单的例子,如果应用的某个页面需求是一个可以滑动的组件,组件内分成多个不同的模块,每个模块有自己的数据以及获取数据的渠道,如果直观考虑那一定是使用ScrollView组件,将各个组件在内部码好,按需获取数据并展示。
  这样的做法当然是可行的,可一旦需求变化或者模块数量变多,内部逻辑复杂化,把这些模块都挤在一个Fragment或者Activity中并不好,即便是使用MVP模式也不一定能避开大量的前端逻辑代码聚集。
  ListView可以在此提供另一种解决方案,那便是将模块都写成自定义的组件,或者写一个自定义抽象框架将模块统一抽象出来;接着在Fragment或者Activity中创建一个列表对象,里面放上所有需要的模块,然后将这个列表作为输入来自定义一个Adapter并交由ListView来展示。
  这样一来虽然重用是不可能了,但因为模块本身就是缓存好的,也不会对性能有太大影响,要控制模块的显示与否可以直接设置标志位,按照position设置convertView的Visibility,getView方法中按索引获得模块,直接通知模块本身进行更新即可。

// 组件View抽象基类
public abstract class ComponentView {
    protected Context context;
    protected View contentView;

    public ComponentView(Context resContext) {
        context = resContext;
    }

    public void initView(View origin) { // 从现有View初始化
        contentView = origin;
        loadViews();
    }

    protected abstract void loadViews();

    protected abstract void loadMembers();

    public abstract void updateViews(); // 刷新界面组件

    public View getContentView() {
        return contentView;
    }
}

List<ComponentView> viewList = new ArrayList<>();

public class ComponentListAdapter extends BaseAdapter {

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

    @Override
    public Object getItem(int i) {
        return viewList==null?null:viewList.get(i);
    }

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

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        ComponentView current = (ComponentView) getItem(i);
        if(current != null) {
            current.updateViews();
            return current.getContentView();
        }
        return null;
    }
}

  于是模块的代码不需要由Fragment或者Activity来维护了,每个模块自己维护自己的数据,有通信需求则使用Handler来实现,这样的方法比使用ScrollView要灵活方便的得多。
  ListView的每行项目本身也是一般的组件,因此它可以和属性动画或者值动画配合使用来实现比较好看的列表动画,包括滑动删除,长按拖动,动画进入等。
  总结起来,ListView看似列表胜似列表,它能做到多少事全看设计人员的想象力有多强。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值