Listview

Android 专栏收录该内容
47 篇文章 0 订阅

ListView的简单用法

布局文件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        
    </ListView>
</LinearLayout>

主程序

public class MainActivity extends Activity {
    private String[] data = {"Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape", "Pineapple",
            "Strawberry", "Cherry", "Mango"};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(MainActivity.this, android.R.layout.simple_list_item_1, data);
        ListView listView = (ListView) findViewById(R.id.list_view);
        listView.setAdapter(adapter);
    }
}

既然 ListView是用于展示大量数据的,那我们就应该先将数据提供好。这些数据可以是从网上下载的,也可以是从数据库中读取的,应该视具体的应用程序场景来决定。这里我们就简单使用了一个 data数组来测试,里面包含了很多水果的名称。

不过,数组中的数据是无法直接传递给 ListView的,我们还需要借助适配器来完成。Android中提供了很多适配器的实现类,其中我认为最好用的就是ArrayAdapter。它可以通过泛型来指定要适配的数据类型,然后在构造函数中把要适配的数据传入即可。ArrayAdapter有多个构造函数的重载,你应该根据实际情况选择最合适的一种。这里由于我们提供的数据都是字符串,因此将 ArrayAdapter的泛型指定为 String,然后在 ArrayAdapter的构造函数中依次传入当前上下文ListView 子项布局的 id,以及要适配的数据。注意我们使用了 android.R.layout.simple_list_item_1作为 ListView子项布局的 id,这是一个 Android内置的布局文件,里面只有一个 TextView,可用于简单地显示一段文本。这样适配器对象就构建好了。

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

定制 ListView的界面

定义一个实体类,作为 ListView适配器的适配类型。新建类 Fruit,代码如下所示:

public class Fruit {
    private String name;
    private int imageId;

    public Fruit(String name, int imageId) {
        this.name = name;
        this.imageId = imageId;
    }

    public String getName() {
        return name;
    }

    public int getImageId() {
        return imageId;
    }
}

Fruit类中只有两个字段,name表示水果的名字,imageId表示水果对应图片的资源 id。 然后需要为 ListView 的子项指定一个我们自定义的布局,在 layout 目录下新建 fruit_item.xml,代码如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
    android:id="@+id/fruit_image"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

<TextView
    android:id="@+id/fruit_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:layout_marginLeft="10dip" />
</LinearLayout>

在这个布局中,我们定义了一个 ImageView 用于显示水果的图片,又定义了一个 TextView用于显示水果的名称。

接下来需要创建一个自定义的适配器,这个适配器继承自 ArrayAdapter,并将泛型指定为 Fruit类。新建类 FruitAdapter,代码如下所示:

public class FruitAdapter extends ArrayAdapter<Fruit> {
    private int resourceId;

    public FruitAdapter(Context context, int textViewResourceId, List<Fruit> objects) {
        super(context, textViewResourceId, objects);
        resourceId = textViewResourceId;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Fruit fruit = getItem(position);// 获取当前项的Fruit实例
        View view = LayoutInflater.from(getContext()).inflate(resourceId, null);
        ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
        TextViewfruitName = (TextView) view.findViewById(R.id.fruit_name);
        fruitImage.setImageResource(fruit.getImageId());
        fruitName.setText(fruit.getName());
        return view;
    }
}

FruitAdapter重写了父类的一组构造函数,用于将上下文、ListView子项布局的 id和数据都传递进来。另外又重写了 getView()方法,这个方法在每个子项被滚动到屏幕内的时候会被调用。在 getView方法中,首先通过 getItem()方法得到当前项的 Fruit实例,然后使用 LayoutInflater来为这个子项加载我们传入的布局,接着调用 View的findViewById()方法分别获取到 ImageView和TextView的实例,并分别调用它们的 setImageResource()和 setText()方法来设置显示的图片和文字,最后将布局返回,这样我们自定义的适配器就完成了。

修改MainActivity部分的代码:

public class MainActivity extends Activity {
    private List<Fruit> fruitList = new ArrayList<Fruit>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initFruits(); // 初始化水果数据
        FruitAdapter adapter = new FruitAdapter(MainActivity.this, R.layout.fruit_item, fruitList);
        ListView listView = (ListView) findViewById(R.id.list_view);

        listView.setAdapter(adapter);
    }

    private void initFruits() {
        Fruit apple = new Fruit("Apple", R.drawable.apple_pic);
        fruitList.add(apple);
        Fruit banana = new Fruit("Banana", R.drawable.banana_pic);
        fruitList.add(banana);
        Fruit orange = new Fruit("Orange", R.drawable.orange_pic);
        fruitList.add(orange);
        Fruit watermelon = new Fruit("Watermelon", R.drawable.watermelon_pic);
        fruitList.add(watermelon);
        Fruit pear = new Fruit("Pear", R.drawable.pear_pic);
        fruitList.add(pear);
        Fruit grape = new Fruit("Grape", R.drawable.grape_pic);
        fruitList.add(grape);
        Fruit pineapple = new Fruit("Pineapple", R.drawable.pineapple_pic);
        fruitList.add(pineapple);
        Fruit strawberry = new Fruit("Strawberry", R.drawable.strawberry_pic);
        fruitList.add(strawberry);
        Fruit cherry = new Fruit("Cherry", R.drawable.cherry_pic);
        fruitList.add(cherry);
        Fruit mango = new Fruit("Mango", R.drawable.mango_pic);
        fruitList.add(mango);
    }
}

ListView优化

目前我们ListView的运行效率是很低的,因为在FruitAdapter的getView() 方法中每次都将布局重新加载了一遍,当 ListView快速滚动的时候这就会成为性能的瓶颈。仔细观察,getView()方法中还有一个convertView参数,这个参数用于将之前加载好的布局进行缓存,以便之后可以进行重用。修改 FruitAdapter中的代码,如下所示:

public class FruitAdapter extends ArrayAdapter<Fruit> {
    ……

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Fruit fruit = getItem(position);
        View view;
        if (convertView == null) {
            view = LayoutInflater.from(getContext()).inflate(resourceId, null);
        } else {
            view = convertView;
        }
        ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
        TextViewfruitName = (TextView) view.findViewById(R.id.fruit_name);
        fruitImage.setImageResource(fruit.getImageId());
        fruitName.setText(fruit.getName());
        return view;
    }
}

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

不过,目前我们的这份代码还是可以继续优化的,虽然现在已经不会再重复去加载布局,但是每次在getView()方法中还是会调用View的findViewById()方法来获取一次控件的实例。我们可以借助一个 ViewHolder来对这部分性能进行优化,修改FruitAdapter中的代码,如下所示:

public class FruitAdapter extends ArrayAdapter<Fruit> {
    ……

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Fruit fruit = getItem(position);
        View view;
        ViewHolder viewHolder;
        if (convertView == null) {
            view = LayoutInflater.from(getContext()).inflate(resourceId, null);
            viewHolder = new ViewHolder();
            viewHolder.fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
            viewHolder.fruitName = (TextView) view.findViewById(R.id.fruit_name);
            view.setTag(viewHolder);// 将ViewHolder存储在View中
        } else {
            view = convertView;
            viewHolder = (ViewHolder) view.getTag();// 重新获取ViewHolder
        }
        viewHolder.fruitImage.setImageResource(fruit.getImageId());
        viewHolder.fruitName.setText(fruit.getName());
        return view;

    }

    class ViewHolder {
        ImageView fruitImage;
        TextView fruitName;
    }
}

我们新增了一个内部类 ViewHolder,用于对控件的实例进行缓存。当 convertView为空的时候,创建一个 ViewHolder对象,并将控件的实例都存放在 ViewHolder里,然后调用 View 的 setTag()方法,将 ViewHolder对象存储在 View中。当 convertView不为空的时候则调用 View的 getTag()方法,把 ViewHolder重新取出。这样所有控件的实例都缓存在了 ViewHolder 里,就没有必要每次都通过 findViewById()方法来获取控件实例了。

增加常用的几点:

1. 目前我们ListView的运行效率是很低的,因为在FruitAdapter的getView() 方法中每次都将布局重新加载了一遍,当 ListView快速滚动的时候这就会成为性能的瓶颈。在 getView()方法中进行了判断,如果 convertView为空,则使用 LayoutInflater去加载布局,如果不为空则直接对 convertView进行重用。这样就大大提高了 ListView的运行效率,在快速滚动的时候也可以表现出更好的性能。

2. 每次在getView()方法中还是会调用View的findViewById()方法来获取一次控件的实例。 我们可以借助一个 ViewHolder来对这部分性能进行优。我们新增了一个内部类ViewHolder,用于对控件的实例进行缓存。当 convertView为空 的时候,创建一个 ViewHolder对象,并将控件的实例都存放在 ViewHolder里,然后调用 View 的setTag()方法,将 ViewHolder对象存储在View中。当 convertView不为空的时候则调用 View的getTag()方法,把 ViewHolder重新取出。这样所有控件的实例都缓存在了 ViewHolder 里,就没有必要每次都通过 findViewById()方法来获取控件实例了。(参考<第一行代码> page 135-136)



1.Adapter的getView方法里面convertView没有使用setTag和getTag方式;

2.在getView方法里面ViewHolder初始化后的赋值或者是多个控件的显示状态和背景的显示没有优化好,抑或是里面含有复杂的计算和耗时操作;

3.在getView方法里面 inflate的row 嵌套太深(布局过于复杂)或者是布局里面有大图片或者背景所致;

4.Adapter多余或者不合理的notifySetDataChanged;

5.listview 被多层嵌套,多次的onMessure导致卡顿,如果多层嵌套无法避免,建议把listview的高和宽设置为fill_parent. 如果是代码继承的listview,那么也请你别忘记为你的继承类添加上LayoutPrams,注意高和宽都是fill_parent的;

RecycleView与传统ListView比较

RecycleView的使用 http://blog.csdn.net/lmj623565791/article/details/45059587

RecyclerView与老前辈ListView的不同点,主要在于以下几个特性:

· Adapter中的ViewHolder模式 - 对于ListView来说,通过创建ViewHolder来提升性能并不是必须的。因为ListView并没有严格的ViewHolder设计模式。但是在使用RecyclerView的时候,Adapter必须实现至少一个ViewHolder,必须遵循ViewHolder设计模式。

· 定制Item条目 -ListView只能实现垂直线性排列的列表视图,与之不同的是,RecyclerView可以通过设置RecyclerView.LayoutManager来定制不同风格的视图,比如水平滚动列表或者不规则的瀑布流列表。

· Item动画 - 在ListView中没有提供任何方法或者接口,方便开发者实现Item的增删动画。相反地,可以通过设置RecyclerView的RecyclerView.ItemAnimator来为条目增加动画效果。

· 设置数据源 - 在LisView中针对不同数据封装了各种类型的Adapter,比如用来处理数组的ArrayAdapter和用来展示Database结果的CursorAdapter。相反地,在RecyclerView中必须自定义实现RecyclerView.Adapter并为其提供数据集合。

· 设置条目分割线 - 在ListView中可以通过设置android:divider属性来为两个Item间设置分割线。如果想为RecyclerView添加此效果,则必须使用RecyclerView.ItemDecoration,这种实现方式不仅更灵活,而且样式也更加丰富。

· 设置点击事件 - 在ListView中存在AdapterView.OnItemClickListener接口,用来绑定条目的点击事件。但是,很遗憾的是在RecyclerView中,并没有提供这样的接口,不过,提供了另外一个接口RcyclerView.OnItemTouchListener,用来响应条目的触摸事件。

Android ListView工作原理完全解析,带你从源码的角度彻底理解

Android ListView工作原理完全解析,带你从源码的角度彻底理解(面试有问到)

Android ListView滑动过程中图片显示重复错位闪烁问题解决

郭哥的文章: 

Android ListView异步加载图片乱序问题,原因分析及解决方案(面试有问到)

主要分析Android ListView滚动过程中图片显示重复、错乱、闪烁的原因及解决方法,顺带提及ListView的缓存机制。
1、原因分析
ListView item缓存机制:为了使得性能更优,ListView会缓存行item(某行对应的View)。ListView通过adapter的getView函数获得每行的item。滑动过程中,

a. 如果某行item已经滑出屏幕,若该item不在缓存内,则put进缓存,否则更新缓存;
b. 获取滑入屏幕的行item之前会先判断缓存中是否有可用的item,如果有,做为convertView参数传递给adapter的getView。
更具体可见源代码ListView.obtainView

这样,如下的getView写法就可以充分利用缓存大大提升ListView的性能。即便上万个行item,最多inflate的次数为n,n为一屏最多显示ListView 行item的个数。

ListView Adapter getView写法

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder;
    if (convertView == null) {
        convertView = inflater.inflate(R.layout.list_item, null);
        holder = new ViewHolder();
        ……
        convertView.setTag(holder);
    } else {
        holder = (ViewHolder)convertView.getTag();
    }
}

/**
 * ViewHolder
 */
private static class ViewHolder {

    ImageView appIcon;
    TextView  appName;
    TextView  appInfo;
}

这样提升了性能,但同时也会造成另外一些问题:

a. 行item图片显示重复
这个显示重复是指当前行item显示了之前某行item的图片。
比如ListView滑动到第2行会异步加载某个图片,但是加载很慢,加载过程中listView已经滑动到了第14行,且滑动过程中该图片加载结束,第2行已不在屏幕内,根据上面介绍的缓存原理,第2行的view可能被第14行复用,这样我们看到的就是第14行显示了本该属于第2行的图片,造成显示重复。

b. 行item图片显示错乱
这个显示错乱是指某行item显示了不属于该行item的图片。
比如ListView滑动到第2行会异步加载某个图片,但是加载很慢,加载过程中listView已经滑动到了第14行,第2行已不在屏幕内,根据上面介绍的缓存原理,第2行的view可能被第14行复用,第14行显示了第2行的View,这时之前的图片加载结束,就会显示在第14行,造成错乱。

c. 行item图片显示闪烁
上面b的情况,第14行图片又很快加载结束,所以我们看到第14行先显示了第2行的图片,立马又显示了自己的图片进行覆盖造成闪烁错乱。

2、解决方法
通过上面的分析我们知道了出现错乱的原因是异步加载及对象被复用造成的,如果每次getView能给对象一个标识,在异步加载完成时比较标识与当前行item的标识是否一致,一致则显示,否则不做处理即可。
下面以使用ImageCache为ListView提供图片获取缓存为例。

最简单的解决方法就是网上说的,给 ImageView 设置一个 tag, 并预设一个图片。

当 Item1 比 Item8 图片下载的快时, 你滚下去使 Item8 可见,这时 ImageView 的 tag 被设成了

Item8 的 URL, 当 Item1 下载完时,由于 Item1 不可见 现在的 tag 是 Item8 的 URL,所以不满足条件,

虽然下载下来了但不会设置到 ImageView 上, tag 标识的永远是可见 view 中图片的 URL。

关键代码如下:

// 给 ImageView 设置一个 tag
holder.img.setTag(imgUrl);// 预设一个图片holder.img.setImageResource(R.drawable.ic_launcher);
// 通过 tag 来防止图片错位
if (imageView.getTag() != null && imageView.getTag().equals(imageUrl)) {
    imageView.setImageBitmap(result);
}

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值