查漏补缺之ListView

1. 初始ListView

ListView:
采用适配器设计模式,让数据和界面分离,更利于拓展和维护。

3个要素:
(1)ListView控件。
(2)适配器类。它是视图和数据直接的桥梁,负责提供对数据的访问,生成每一个列表项第一的View。常见的有ArrayAdapter、SimpleAdapter、BaseAdapter。
(3)数据源。

如图用一个字符串数组来测试:

10186693-b6ec6c974573b812.png
ListView

1.listview是展示大量数据的,需要实现将数据准备好,这些数据可从网上下载,也可从数据库获取。

2.数组中的数据不能直接传递给ListView,需要借助适配器。最简单的就是ArrayAdapter,可通过泛型来指定要适配的数据类型,然后在构造方法中把要适配的数据传入即可。

由于提供的数据是字符串,因此反省指定为String,在ArrayAdapter的构造函数中依次传入当前的上下文、每个条目布局的id、要适配的数据。这里使用了Android内置的布局文件——android.R.layout.simple_list_item_1,这里面只有一个TextView,可用于简单显示一列文本。这样适配器对象就构建好了。

3.调用ListView的setAdapter()方法,将构建好的适配器对象传递进去,这样ListView和适配器之间的关联就建立完成了。

2.定制ListView条目的界面

只能显示一段文本会有些单独,无法满足需求,可对其进行定制丰富内容。

e.g. 通过定制,显示旗帜+国家/地区。

  1. 字符串已经无法满足要求,可新建实体类Country。
    其中有两个字段,name为国家和地区的名字,ImageId表示旗帜对应图片的id。


    10186693-33332355aa8b54d4.png
    image.png
  2. 创建item_country.xml。1个ImageView(iv_flag)+1个TextView(tv_name)。

  3. 创建自定义适配器,继承自ArrayAdapter,泛型指定为Country类。
    新建一个类CountryAdapter:

public class CountryAdapter extends ArrayAdapter<Country>{

    int resourceId;

    public CountryAdapter(@NonNull Context context, int resource, @NonNull List<Country> objects) {
        super(context, resource, objects);

        resourceId = resource;  //记录布局资源Id
    }

    @NonNull
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        //position为当前项的位置,从0开始计数

        Country country = getItem(position);    //获取当前位置条目的数据实例
        //把xml转换成view对象
        View view = View.inflate(getContext(),resourceId,null);
        ImageView imageView = (ImageView)view.findViewById(R.id.iv_flag);
        TextView textView = (TextView)view.findViewById(R.id.tv_name);
        //设置数据
        imageView.setImageResource(country.getImageId());
        textView.setText(country.getName());
        return view;
    }
}

CountryAdapter 重写了父类的构造函数,用于上下文、ListView子项目布局的id和数据传递过来。另外重写了getView()方法,这个方法在每个子项滚动出屏幕时被调用。

修改 ListViewActivity.xml:

public class ListViewActivity extends AppCompatActivity {

   private List<Country> countryList = new ArrayList<>();
   ListView listView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_list_view);

        listView = findViewById(R.id.list_view);
        //初始化数据
        initDatas();
        //给ListView设置适配器,从而显示在界面上
        CountryAdapter adapter = new CountryAdapter(this,R.layout.item_country,countryList);
        listView.setAdapter(adapter);
    }

    private void initDatas() {
        countryList.add(new Country("中国",R.drawable.flag));
        countryList.add(new Country("日本",R.drawable.flag));
        countryList.add(new Country("韩国",R.drawable.flag));
        countryList.add(new Country("印度",R.drawable.flag));
        countryList.add(new Country("俄罗斯",R.drawable.flag));
        countryList.add(new Country("新加坡",R.drawable.flag));
        countryList.add(new Country("马来西亚",R.drawable.flag));
        countryList.add(new Country("越南",R.drawable.flag));
        countryList.add(new Country("泰国",R.drawable.flag));
    }
}
  1. 定制结果如图:
10186693-f637008c9b8cfa17.png
定制ListView条目结果

总结:
目前,CountryAdapter的getView()方法中每次都会把布局重新加载一遍,当ListView快速滑动时就会出现程序卡顿或崩溃。

3. 优化ListView

仔细观察,getView()方法还有一个convertView参数,这个参数用于将之前加载好的布局进行缓存,以便以后进行重用。修改CountryAdapter中getView()方法的代码,如下:

10186693-8389cb5cf159c9b3.png
优化1.0

如图可看出,在getView方法中进行了判断,如果convertView为空,则通过inflater()方法加载布局;如果不为空,则直接对convertView进行重用。这样就大大提高了ListView的运行效率,在快速滚动时也可以表现出更好的性能。

但是还可以继续优化。无论是通过inflate()加载布局,还是复用convertView,都会调用findViewById()方法去初始化控件,这步操作也是可以复用的。

public class CountryAdapter extends ArrayAdapter<Country>{
     int rescourId;

    public CountryAdapter(@NonNull Context context, int resource, @NonNull List<Country> objects) {
        super(context, resource, objects);

          rescourId = resource;//记录布局资源Id
    }

    @NonNull
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        //position为当前项的位置,从0开始计数

        Country country = getItem(position);  //获取当前位置条目的数据实例
        View view;
        ViewHolder viewHolder;
        //当convertView不为空,复用convertView
        if (convertView == null) {
            viewHolder = new ViewHolder();  //创建ViewHolder
            //把xml转换成view对象
            view = View.inflate(getContext(), rescourId, null);
            viewHolder.imageView =  view.findViewById(R.id.iv_flag);
            viewHolder.textView =  view.findViewById(R.id.tv_name);
            view.setTag(viewHolder);        //将ViewHolder保存到view对象中
        }else {
            view = convertView;
            viewHolder = (ViewHolder)view.getTag();
        }
        //设置数据
        viewHolder.imageView.setImageResource(country.getImageId());
        viewHolder.textView.setText(country.getName());
        return view;
    }

    private class ViewHolder {
        ImageView imageView;
        TextView textView;
    }
}

当convertView为空时,创建一个ViewHolder对象,并将控件的实例都存放在ViewHolder中,然后调用View的setTag()方法,将ViewHolder的对象缓存到View中。当convertView不为空时,则调用View的getTag()方法,把ViewHolder重新取出。这样所有控件的实例都缓存在了ViewHolder中,就不会每次都通过findViewById()方法来获取实例了。

4.ListView的点击事件

使用setOnItemClickListener()来为ListView注册一个监听器,当用户点击任意一个条目时,就会回调onItemClick()方法,在这个方法中可以通过position参数判断出用户点击的是哪一个条目。然后获取到相应的国家,Toast显示国家/地区名称。

public class ListViewActivity extends AppCompatActivity {

   private List<Country> countryList = new ArrayList<>();
   ListView listView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_list_view);

        listView = findViewById(R.id.list_view);
        //初始化数据
        initDatas();
        //给ListView设置适配器,从而显示在界面上
        final CountryAdapter adapter = new CountryAdapter(this,R.layout.item_country,countryList);
        listView.setAdapter(adapter);

        //设置ListView条目的点击事件
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                /*
                 * 参数:1.parent:指ListView, 2.view:指被点击条目的View, 3.posiyion:条目的位置
                 *4.id:如果Adapter继承自ArrayAdapter,id和position一致
                 * */

                Toast.makeText(getApplicationContext(),countryList.get(position).getName(),Toast.LENGTH_SHORT).show();
            }
        });
        //长按事件
        listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
            @Override
            public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {

                countryList.remove(position);   //删除长按位置的数据
                adapter.notifyDataSetChanged(); //刷新ListView界面
                return true;        //返回true时,处理完长按事件,不会再处理点击事件
                //return false;     //返回false时,处理完长按事件,还会处理点击事件
            }
        });

    }

    private void initDatas() {
        countryList.add(new Country("中国",R.drawable.flag));
        countryList.add(new Country("日本",R.drawable.flag));
        countryList.add(new Country("韩国",R.drawable.flag));
        countryList.add(new Country("印度",R.drawable.flag));
        countryList.add(new Country("俄罗斯",R.drawable.flag));
        countryList.add(new Country("新加坡",R.drawable.flag));
        countryList.add(new Country("马来西亚",R.drawable.flag));
        countryList.add(new Country("越南",R.drawable.flag));
        countryList.add(new Country("泰国",R.drawable.flag));
    }
}

其中还设置长按事件,需要注意的是,OnItemLongClick()有一个boolean的返回值,代表是否消费掉这个事件。

  • 返回false时,处理完长按事件,还会处理点击事件
  • 返回true时,处理完长按事件,不会再处理点击事件
10186693-59841274049782fe.png
长按事件

当长按一个条目时,就会把这个条目从界面中移除。因为数据集合和适配器绑定,在集合在删除数据,然后调用适配器notifyDataSetChanged()方法刷新,就是实现了删除ListView条目操作。

10186693-8ec0ae850776c496.png
条目点击事件的传递

5.ListView常用属性

  • android:divider 设置分割线的颜色或者指定分割线图片
  • android:dividerHeight 设置分割线的高度
  • android:scrollbars = "none" 隐藏滚动条
10186693-ec16eef823f0b06d.png
添加分割线

猜你喜欢

转载自blog.csdn.net/weixin_34194551/article/details/87420191