ListView

前言


ListView 是Android中显示数据常用且难用的控件之一,主要用于显示一个垂直滚动的数据集合,随着Android 手机对性能要求越来越高,一个更现代,更灵活,显示列表性能更优异的RecyclerView将会逐渐取代ListView的数据显示方式,但是目前为止,ListView在开发中还是十分常见的,并未被弃用。

下面是大体思路:

具体使用


如图所示,ListView一般是以这种竖直滚动列表的形式出现,显示的数据类型大都一致。

那么,使用 ListView 的话就离不开 Adapter 了,Adapter 本身是一个接口,Adapter 接口及其子类的继承关系如下图:

  • 红色字体的类表示这个类的所有抽象方法已经实现,可以直接实例化,无需再写它的子类。

  • BaseAdapter:抽象类,实际开发中通常会继承这个类并且重写相关方法,用得最多的一个 Adapter!

  • ArrayAdapter:支持泛型操作,最简单的一个 Adapter,如果扩展泛型,可以展示复杂结构的数据。

  • SimpleAdapter:同样具有良好扩展性的一个 Adapter,可以自定义多种效果!

  • SimpleCursorAdapter:用于显示简单文本类型的 listView,一般在数据库那里会用到,不过有点过时, 不推荐使用!

ArrayAdapter简单使用


首先来创建一个ListViewTest项目,让Android Studio自动为我们创建好Activity,然后修改activity_main.xml中的代码,如下所示

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

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

</LinearLayout>

接下来修改MainActivity中的代码,如下所示

class ListViewActivity1 : AppCompatActivity(){
    private lateinit var listView: ListView
    private val data =
        arrayOf("张三", "李四", "王五", "赵六", "陈浮生", "陈富贵", "竹叶青", "陈龙象", "陈半仙", "王虎胜", "张三千",
            "张三", "李四", "王五", "赵六", "陈浮生", "陈富贵", "竹叶青", "陈龙象", "陈半仙", "王虎胜", "张三千",
            "张三", "李四", "王五", "赵六", "陈浮生", "陈富贵", "竹叶青", "陈龙象", "陈半仙", "王虎胜", "张三千")
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_listview1)
        //获取控件
        listView = findViewById(R.id.listView)
        val adapter: ArrayAdapter<String> = ArrayAdapter(this,android.R.layout.simple_list_item_1,data)
        listView.adapter = adapter
    }
}

注意,我们使用了 android.R.layout.simple_list_item_1 , data 作为ListView子项布局的id,这是Android内置的一个布局文件,里面只有一个TextView,只用于简单的显示一段文本。这样适配器对象就构建好了。

其实除了 android.R.layout.simple_list_item_1 之外,还有一些其他的样式布局,如下图,可以自行尝试。

我们先看一下运行效果:

ArrayAdapter + 泛型


现在我们来定制更加丰富的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;
    }
}

子项布局如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal" 
    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_vertical"
        android:layout_marginLeft="10dp"
        />
</LinearLayout>

自定义适配器:

public class FruitAdapter extends ArrayAdapter<Fruit> {

    private int resourceId;

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

    /**
     * 每个子项滚动到屏幕内的时候getView()都会被调用
     */
    @NonNull
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        //获取当前项的Fruit实例
        Fruit fruit = getItem(position);
        //加载我们传入的布局
        View view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
        // 获取ImageView实例
        ImageView fruitImage = view.findViewById(R.id.fruit_image);
        // 获取TextView实例
        TextView fruitName = view.findViewById(R.id.fruit_name);
        // 为ImageView设置要显示的图片
        fruitImage.setImageResource(fruit.getImageId());
        // 为TextView设置要显示的名字
        fruitName.setText(fruit.getName());
        //将布局返回
        return view;
    }
}

接下来就是调用了


public class MainActivity extends AppCompatActivity {
    private String[] data = {"张三", "李四", "王五", "赵六", "陈浮生", "陈富贵", "竹叶青", "陈龙象", "陈半仙", "王虎胜", "张三千"};

    private List<Fruit>fruitList=new ArrayList<>();

    public void initFruits()
    {
        for (int i=0;i<2;i++)
        {
            Fruit zs = new Fruit("张三", R.drawable.apple_pic);
            fruitList.add(zs);
            Fruit ls = new Fruit("李四", R.drawable.banana_pic);
            fruitList.add(ls);
            Fruit ww = new Fruit("王五", R.drawable.orange_pic);
            fruitList.add(ww);
            Fruit cl = new Fruit("赵六", R.drawable.watermelon_pic);
            fruitList.add(cl);
            Fruit cfs = new Fruit("陈浮生", R.drawable.pear_pic);
            fruitList.add(cfs);
            Fruit clx = new Fruit("陈龙象", R.drawable.grape_pic);
            fruitList.add(clx);
            Fruit cbx = new Fruit("陈半仙", R.drawable.pineapple_pic);
            fruitList.add(cbx);
            Fruit zsq = new Fruit("张三千", R.drawable.strawberry_pic);
            fruitList.add(zsq);
            Fruit cys = new Fruit("陈圆殊", R.drawable.cherry_pic);
            fruitList.add(cys);
            Fruit whs = new Fruit("王虎胜", R.drawable.mango_pic);
            fruitList.add(whs);

        }
    }
    @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 = findViewById(R.id.list_view);
        listView.setAdapter(adapter);
    }
}

效果图:

理论上来说,ArrayAdapter 也可以满足各种复杂布局的ListView,至于性能,也是可以像BaseAdapter一样优化的,但是还是建议只在简单布局使用。

SimpleAdapter


虽然有人说 SimpleAdapter 的功能相比 ArrayAdapter 更加强大,但在我看来其实差不了多少,而且SimpleAdapter 的实用性不强,下面介绍一下怎么使用的。

step1:在 xml 中添加 ListView

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

step2: 创建单项布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingVertical="6dp">

    <ImageView
        android:id="@+id/fruit_image"
        android:layout_width="50dp"
        android:layout_height="50dp"
        />

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

step3: 构建数据源,格式如下:ArrayList<HashMap<String, Any>>

private lateinit var listData: ArrayList<HashMap<String, Any>>
private fun initData() {
        listData = ArrayList()
        val name = arrayOf("小明","小花","小明","小花","小明","小花","小明","小花","小明","小花")
        val imageId = arrayOf(R.drawable.code_coach,R.drawable.code_coach,
            R.drawable.code_coach,R.drawable.code_coach,
            R.drawable.code_coach,R.drawable.code_coach,
            R.drawable.code_coach,R.drawable.code_coach,
            R.drawable.code_coach,R.drawable.code_coach)

        for (i in name.indices){
            val item = HashMap<String, Any>()
            item["name"] = name[i]
            item["imageId"] = imageId[i]
            listData.add(item)
        }

       // 将 hashMap 的 key 组成一个字符串数组
        val form = arrayOf("name", "imageId")
        // 将 item 布局中的 view 的 id 组成一个数组,要和 form 对应
        val to = intArrayOf(R.id.fruit_name, R.id.fruit_image)
    }

step4: SimpleAdapter绑定ListView

/**
 * @param this 上下文
 * @param listData 数据源
 * @param R.layout.layout_listview_item1 自定义子项布局
 * @param form 数据源的key
 * @param to 子项布局中控件的ID
 */
val simpleAdapter = SimpleAdapter(this, listData, R.layout.layout_listview_item1, form, to)
//完成绑定
listView.adapter = simpleAdapter

归结起来就是,构建一个 List<?extends Map<String,? >> 格式的数据源,然后预置好 key组件ID 的关系,创建SimpleAdapter后进行绑定。

效果和扩展后的ArrayAdapter是一样的,如图:

BaseAdapter


这种适配器可以说是使用最多的了,可以最大限度的定制我们的子项,下面说一下使用方法。

step1:ListView和子项布局和上边一样。

step2:创建BaseAdapter适配器

public class MyAdapter extends BaseAdapter {

    private Context context;
    private ArrayList<Fruit> data;

    public MyAdapter(Context context, ArrayList<Fruit> data) {
        this.context = context;
        this.data = data;
    }

    @Override
    public int getCount() {
        return data.size();
    }

    @Override
    public Object getItem(int position) {
        return data.get(position);
    }

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null){
            convertView = LayoutInflater.from(context).inflate(R.layout.layout_listview_item1, null);
        }
        //注意:这里只是缓存了convertView和convertView里面的控件,
        //convertView里面控件的数据可并没有缓存,因此需要更新convertView里面控件的数据
        ImageView img = (ImageView)convertView.findViewById(R.id.fruit_image);
        TextView title = (TextView)convertView.findViewById(R.id.fruit_name);
        img.setImageResource(data.get(position).getImageId());
        title.setText(data.get(position).getName());
        return convertView;
    }
}

step3:创建数据源

private fun initFruits() {
        for (i in 0..3) {
            val zs = Fruit("张三", R.drawable.code_coach)
            fruitList.add(zs)
            val ls = Fruit("李四", R.drawable.code_coach)
            fruitList.add(ls)
            val ww = Fruit("王五", R.drawable.code_coach)
            fruitList.add(ww)
            val cl = Fruit("赵六", R.drawable.code_coach)
            fruitList.add(cl)
            val cfs = Fruit("陈浮生", R.drawable.code_coach)
            fruitList.add(cfs)
            val clx = Fruit("陈龙象", R.drawable.code_coach)
            fruitList.add(clx)
            val cbx = Fruit("陈半仙", R.drawable.code_coach)
            fruitList.add(cbx)
            val zsq = Fruit("张三千", R.drawable.code_coach)
            fruitList.add(zsq)
            val cys = Fruit("陈圆殊", R.drawable.code_coach)
            fruitList.add(cys)
            val whs = Fruit("王虎胜", R.drawable.code_coach)
            fruitList.add(whs)
        }
    }

step4:绑定适配器

//获取控件
listView = findViewById(R.id.listView)
listView.adapter = MyAdapter(this,fruitList)

一通操作下来,看一下显示效果:

性能优化


此章讲性能优化,主要是针对BaseAdapter类型适配器做的优化。

上面说到的BaseAdapter使用方法,仅仅对convertView做了缓存,内部组件并没有做缓存,如果数据量过大的话,将会造成极大的内存消耗,结果可想而知,那么我们可以对此做出如下优化。

优化一:引入ViewHolder

public class MyAdapter extends BaseAdapter {

    private Context context;
    private ArrayList<Fruit> data;

    public MyAdapter(Context context, ArrayList<Fruit> data) {
        this.context = context;
        this.data = data;
    }

    @Override
    public int getCount() {
        return data.size();
    }

    @Override
    public Object getItem(int position) {
        return data.get(position);
    }

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (convertView == null){
            holder = new ViewHolder();
            convertView = LayoutInflater.from(context).inflate(R.layout.layout_listview_item1, null);
            holder.img = (ImageView)convertView.findViewById(R.id.fruit_image);
            holder.title = (TextView)convertView.findViewById(R.id.fruit_name);
            convertView.setTag(holder);
        }else{
            holder = (ViewHolder) convertView.getTag();
        }
        holder.img.setImageResource(data.get(position).getImageId());
        holder.title.setText(data.get(position).getName());
        return convertView;
    }

    static class ViewHolder{
        ImageView img;
        TextView title;
    }
}

如果convertView为空,则构建子项布局,同时用 tag 标记 ViewHolder,加载后续子项时,只需要复用这个ViewHolder即可,极大的减少了页面的重绘,提升性能。

优化二: 开启线程执行耗时任务

构建数据源,绑定适配器

class ListViewActivity4 : AppCompatActivity(){

    private lateinit var listView: ListView

    private val fruitList = arrayListOf<Fruit>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_listview1)
        initFruits()
        //获取控件
        listView = findViewById(R.id.listView)
        listView.adapter = MyAdapter(this,fruitList)
    }

    private fun initFruits() {
        for (i in 0..3) {
            val zs = Fruit("张三", R.drawable.code_coach,"https://t7.baidu.com/it/u=1956604245,3662848045&fm=193&f=GIF")
            fruitList.add(zs)
            val ls = Fruit("李四", R.drawable.code_coach,"https://t7.baidu.com/it/u=825057118,3516313570&fm=193&f=GIF")
            fruitList.add(ls)
            val ww = Fruit("王五", R.drawable.code_coach,"https://t7.baidu.com/it/u=2511982910,2454873241&fm=193&f=GIF")
            fruitList.add(ww)
            val cl = Fruit("赵六", R.drawable.code_coach,"https://t7.baidu.com/it/u=2397542458,3133539061&fm=193&f=GIF")
            fruitList.add(cl)
            val cfs = Fruit("陈浮生", R.drawable.code_coach,"https://t7.baidu.com/it/u=3251197759,2520670799&fm=193&f=GIF")
            fruitList.add(cfs)
            val clx = Fruit("陈龙象", R.drawable.code_coach,"https://t7.baidu.com/it/u=1423490396,3473826719&fm=193&f=GIF")
            fruitList.add(clx)
            val cbx = Fruit("陈半仙", R.drawable.code_coach,"https://t7.baidu.com/it/u=1358795231,3900411654&fm=193&f=GIF")
            fruitList.add(cbx)
            val zsq = Fruit("张三千", R.drawable.code_coach,"https://t7.baidu.com/it/u=3087260382,3061316530&fm=193&f=GIF")
            fruitList.add(zsq)
            val cys = Fruit("陈圆殊", R.drawable.code_coach,"https://t7.baidu.com/it/u=2731006131,4023805140&fm=193&f=GIF")
            fruitList.add(cys)
            val whs = Fruit("王虎胜", R.drawable.code_coach,"https://t7.baidu.com/it/u=3378243861,1554020226&fm=193&f=GIF")
            fruitList.add(whs)
        }
    }
}

接下来就是,BaseAdapter + AsyncTask + HttpURLConnection + 接口回调

public class MyAdapter extends BaseAdapter {

    private Context context;
    private ArrayList<Fruit> data;

    public MyAdapter(Context context, ArrayList<Fruit> data) {
        this.context = context;
        this.data = data;
    }

    @Override
    public int getCount() {
        return data.size();
    }

    @Override
    public Object getItem(int position) {
        return data.get(position);
    }

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        final ViewHolder holder;

        if (convertView == null){
            holder = new ViewHolder();
            convertView = LayoutInflater.from(context).inflate(R.layout.layout_listview_item2, null);
            holder.img = (ImageView)convertView.findViewById(R.id.fruit_image);
            holder.title = (TextView)convertView.findViewById(R.id.fruit_name);
            holder.ivBig = (ImageView)convertView.findViewById(R.id.iv_big);
            convertView.setTag(holder);
        }else{
            holder = (ViewHolder) convertView.getTag();
        }

        holder.img.setImageResource(data.get(position).getImageId());
        holder.title.setText(data.get(position).getName());

        //获取Bitmap并加载ImageView
        BitmapAsyncTask bitmapAsyncTask = new BitmapAsyncTask(data.get(position).getImageUrl());
        bitmapAsyncTask.execute();

        bitmapAsyncTask.setOnDataFinishedListener(new OnDataFinishedListener() {
            @Override
            public void onDataSuccess(Object result) {
                holder.ivBig.setImageBitmap((Bitmap) result);
            }
            @Override
            public void onDataFailure() {
                Toast.makeText(context, "加载图片失败",Toast.LENGTH_SHORT).show();
            }
        });

        return convertView;
    }

    static class ViewHolder{
        ImageView img;
        ImageView ivBig;
        TextView title;
    }

    /**
     * 异步下载图片
     */
    public static class BitmapAsyncTask extends AsyncTask<String, Void, Bitmap> {
        private String mUrl;
        private OnDataFinishedListener onDataFinishedListener;

        public BitmapAsyncTask(String url) {
            this.mUrl = url;
        }

        public void setOnDataFinishedListener(OnDataFinishedListener onDataFinishedListener) {
            this.onDataFinishedListener = onDataFinishedListener;
        }

        @Override
        protected Bitmap doInBackground(String... strings) {
            InputStream in = null;
            try {
                URL url = new URL(this.mUrl);
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                connection.setRequestMethod("GET");
                connection.setConnectTimeout(5000);

                if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
                    in = connection.getInputStream();
                    Bitmap bitmap = BitmapFactory.decodeStream(in);
                    in.close();
                    return bitmap;
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (in != null)
                        in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return null;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            if (bitmap != null) {
                onDataFinishedListener.onDataSuccess(bitmap);
            } else {
                onDataFinishedListener.onDataFailure();
            }
        }
    }

    public interface OnDataFinishedListener {
        void onDataSuccess(Object result);
        void onDataFailure();
    }
}

效果图:

点击事件


点击事件就比较简单了,ListView有自带的点击事件监听

listView.setOnItemClickListener { parent, view, position, id ->
   Toast.makeText(this,fruitList[position].name,Toast.LENGTH_SHORT).show()
}

Header和Footer


//获取控件
listView = findViewById(R.id.listView)
listView.adapter =
    MyAdapter(
        this,
        fruitList
    )

listView.setOnItemClickListener { parent, view, position, id ->
    Toast.makeText(this,fruitList[position].name,Toast.LENGTH_SHORT).show()
}

val headerLayout: LinearLayout  = LayoutInflater.from(this@ListViewActivity5)
    .inflate(R.layout.layout_listview_header1, null) as LinearLayout

initHeadListView(headerLayout)
listView.addHeaderView(headerLayout)

val footerLayout: LinearLayout  = LayoutInflater.from(this@ListViewActivity5)
    .inflate(R.layout.layout_listview_footer1, null) as LinearLayout

val footerLayout2: LinearLayout  = LayoutInflater.from(this@ListViewActivity5)
    .inflate(R.layout.layout_listview_footer2, null) as LinearLayout


//header 和 footer 可以重复添加,可以添加多个
listView.addFooterView(footerLayout)
listView.addFooterView(footerLayout2)
//        listView.removeFooterView(footerLayout)


/**
 * 初始化头部控件布局初始化
 */
private fun initHeadListView(view: View) {
    view.findViewById<LinearLayout>(R.id.ll_header_left).apply {
        this.layoutParams.width = 200
    }
}

吸顶效果


吸顶,一般是列表顶部某个区域会随着列表滚动,当此区域置顶时会固定在顶部,不随着列表向上滚动。

网上有篇文章对此做了总结: 传送门

我这边只提两种方式,其中一个就是预置组件显示与隐藏的方式,比如: 传送门

另外一种方式,利用android5.0的新特性 CoordinatorLayout+AppbarLayout+ CollapsingToolbarLayout,这种方式应该也是官方推荐方式。

直接看布局即可:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true">
        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true"
            app:layout_scrollFlags="scroll|exitUntilCollapsed"
            app:statusBarScrim="@android:color/transparent">
            <View
                android:id="@+id/view_A"
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:gravity="center"
                android:background="@color/cardview_dark_background"
                app:layout_scrollFlags="scroll|enterAlways"/>
        </com.google.android.material.appbar.CollapsingToolbarLayout>
        <View
            android:id="@+id/view_B"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            tools:ignore="MissingConstraints"
            android:background="@color/colorPrimaryDark"/>
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        
            <ListView
                android:id="@+id/listView"
                android:background="#d2ebaf"
                app:layout_behavior="@string/appbar_scrolling_view_behavior"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />

    </androidx.core.widget.NestedScrollView>
    
</androidx.coordinatorlayout.widget.CoordinatorLayout>

运行之后发现,ListView只能显示一项,ohh~

查资料之后,发现由于AppBarLayout折叠后在滑动NestedScrollView时与listview冲突,导致listview滑动延时,需要手指触摸listview一段时间才行,解决方法在自定义listview中重写onMeasure方法。

public class NestedListView  extends ListView {

    public NestedListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //测量的大小由一个32位的数字表示,前两位表示测量模式,后30位表示大小,这里需要右移两位才能拿到测量的大小
        int heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, heightSpec);
    }
}

再次运行,发现可以正常显示和滑动吸顶,但是新的问题又来了,当我的子项中有异步加载图片的情况时,会出现这么一种现象:所有的图片都在第一个子项中闪过,然后列表才会显示正常。

那么,我就不想用ListView了,所以我改用RecycleView,附上布局文件:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true">
        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fitsSystemWindows="true"
            app:layout_scrollFlags="scroll|exitUntilCollapsed"
            app:statusBarScrim="@android:color/transparent">
            <View
                android:id="@+id/view_A"
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:gravity="center"
                android:background="#00ff00"
                app:layout_scrollFlags="scroll|enterAlways"/>
        </com.google.android.material.appbar.CollapsingToolbarLayout>
        <View
            android:id="@+id/view_B"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            tools:ignore="MissingConstraints"
            android:background="#FF0000"/>
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:background="#d2ebaf"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

最后实现效果图:

复杂类型显示


step1:构建数据源

public class Artical {
    private int type;
    private String username;
    private int avatarId;
    private int bigImageId;
    private String dateString;

    public Artical(int type, String username, int avatarId, int bigImageId, String dateString) {
        this.type = type;
        this.username = username;
        this.avatarId = avatarId;
        this.bigImageId = bigImageId;
        this.dateString = dateString;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAvatarId() {
        return avatarId;
    }

    public void setAvatarId(int avatarId) {
        this.avatarId = avatarId;
    }

    public int getBigImageId() {
        return bigImageId;
    }

    public void setBigImageId(int bigImageId) {
        this.bigImageId = bigImageId;
    }

    public String getDateString() {
        return dateString;
    }

    public void setDateString(String dateString) {
        this.dateString = dateString;
    }
}
private val articalList = arrayListOf<Artical>()

private fun initData() {
        for (i in 0..1) {
            articalList.add(Artical(0,"张三",R.drawable.code_coach,R.drawable.big_img,"2022-11-09"))
            articalList.add(Artical(0,"李四",R.drawable.code_coach,R.drawable.big_img,"2022-11-09"))
            articalList.add(Artical(1,"王五",R.drawable.code_coach,R.drawable.big_img,"2022-11-09"))
            articalList.add(Artical(1,"赵六",R.drawable.code_coach,R.drawable.big_img,"2022-11-09"))
            articalList.add(Artical(0,"陈浮生",R.drawable.code_coach,R.drawable.big_img,"2022-11-09"))
            articalList.add(Artical(0,"陈龙象",R.drawable.code_coach,R.drawable.big_img,"2022-11-09"))
            articalList.add(Artical(0,"陈半仙",R.drawable.code_coach,R.drawable.big_img,"2022-11-09"))
            articalList.add(Artical(1,"王虎胜",R.drawable.code_coach,R.drawable.big_img,"2022-11-09"))
            articalList.add(Artical(0,"张三千",R.drawable.code_coach,R.drawable.big_img,"2022-11-09"))
            articalList.add(Artical(0,"陈圆殊",R.drawable.code_coach,R.drawable.big_img,"2022-11-09"))
        }
    }

step2:适配器

public class ArticalListAdapter extends BaseAdapter {

    private Context context;
    private ArrayList<Artical> data;

    public ArticalListAdapter(Context context, ArrayList<Artical> data) {
        this.context = context;
        this.data = data;
    }

    @Override
    public int getCount() {
        return data.size();
    }

    @Override
    public Object getItem(int position) {
        return data.get(position);
    }

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

    @Override
    public int getItemViewType(int position) {
        //TODO 此处要从0开始, 而不能从1开始
        return data.get(position).getType();
    }

    @Override
    public int getViewTypeCount() {
        return 2;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        int type = data.get(position).getType();
        ViewHolder holder;
        ViewHolder2 holder2;

        if(type == 0){
            if (convertView == null){
                holder = new ViewHolder();
                convertView = LayoutInflater.from(context).inflate(R.layout.layout_listview_item3, null);
                holder.ivBig = (ImageView)convertView.findViewById(R.id.big_image);
                holder.tvUsername = (TextView)convertView.findViewById(R.id.tv_username);
                holder.tvDate = (TextView)convertView.findViewById(R.id.tv_date);
                convertView.setTag(holder);
            }else{
                holder = (ViewHolder) convertView.getTag();
            }
            holder.ivBig.setImageResource(data.get(position).getBigImageId());
            holder.tvUsername.setText(data.get(position).getUsername());
            holder.tvDate.setText(data.get(position).getDateString());
        }
        else if(type == 1){
            if (convertView == null){
                holder2 = new ViewHolder2();
                convertView = LayoutInflater.from(context).inflate(R.layout.layout_listview_item4, null);
                holder2.ivAvatar = (ImageView)convertView.findViewById(R.id.iv_avatar);
                holder2.tvUsername = (TextView)convertView.findViewById(R.id.tv_username);
                holder2.tvDate = (TextView)convertView.findViewById(R.id.tv_date);
                holder2.ivBig = (ImageView)convertView.findViewById(R.id.iv_big);
                convertView.setTag(holder2);
            }else{
                holder2 = (ViewHolder2) convertView.getTag();
            }
            holder2.ivAvatar.setImageResource(data.get(position).getAvatarId());
            holder2.ivBig.setImageResource(data.get(position).getBigImageId());
            holder2.tvUsername.setText(data.get(position).getUsername());
            holder2.tvDate.setText(data.get(position).getDateString());
        }

        return convertView;
    }

    static class ViewHolder{
        ImageView ivBig;
        TextView tvUsername;
        TextView tvDate;
    }

    static class ViewHolder2{
        ImageView ivAvatar;
        ImageView ivBig;
        TextView tvUsername;
        TextView tvDate;
    }

}

step3:绑定适配器

listView = findViewById(R.id.listView)
listView.adapter =
    ArticalListAdapter(
        this,
        articalList
    )

效果图:

下拉刷新和上拉加载


下拉刷新Header布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="80dp"
    android:paddingBottom="10dp"
    android:gravity="center_vertical"
    android:paddingTop="10dp" >
    <ImageView
        android:id="@+id/pulldown_header_arrow"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:layout_alignParentLeft="true"
        android:layout_marginLeft="120dp"
        android:scaleType="centerCrop"
        android:src="@drawable/z_arrow_down2"
        android:visibility="invisible" />
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@+id/pulldown_header_arrow"
        android:layout_alignTop="@+id/pulldown_header_arrow"
        android:layout_centerHorizontal="true"
        android:gravity="center_vertical"
        android:orientation="vertical" >
        <TextView
            android:id="@+id/pulldown_header_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="加载中..." />
        <TextView
            android:id="@+id/pulldown_header_date"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="更新于:"
            android:visibility="gone" />
    </LinearLayout>
    <ProgressBar
        android:id="@+id/pulldown_header_loading"
        style="@android:style/Widget.ProgressBar.Small.Inverse"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="2dp"
        android:layout_marginLeft="120dp" />
</RelativeLayout>

上拉加载Footer布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:gravity="center"
    android:paddingVertical="12dp"
    android:layout_height="50dp">
    <TextView
        android:id="@+id/tv_loading"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="12dp"
        android:text="加载中..."/>
</LinearLayout>

自定义ListView组件:

public class PullToRefreshListView extends ListView {

    private View headerView;
    private float downY;
    private int headerViewHeight;
    /** 状态:下拉刷新 */
    private static final int STATE_PULL_TO_REFRESH = 0;
    /** 状态:松开刷新 */
    private static final int STATE_RELEASE_REFRESH = 1;
    /** 状态:正在刷新 */
    private static final int STATE_REFRESHING = 2;
    /** 当前状态 */
    private int currentState = STATE_PULL_TO_REFRESH;  // 默认是下拉刷新状态
    private ImageView iv_arrow;
    private ProgressBar progress_bar;
    private TextView tv_state;
    private TextView tv_loading;
    private RotateAnimation upAnim;
    private RotateAnimation downAnim;
    private OnRefreshingListener mOnRefreshingListener;
    private View footerView;
    private int footerViewHeight;
    private boolean isNoMoreData = false;

    /**
     * 正在加载更多
     */
    private boolean loadingMore;
    public PullToRefreshListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initHeaderView();
        initFooterView();
    }

    private void initHeaderView() {
        headerView = View.inflate(getContext(), R.layout.layout_listview_header3, null);
        iv_arrow = (ImageView) headerView.findViewById(R.id.pulldown_header_arrow);
        progress_bar = (ProgressBar) headerView.findViewById(R.id.pulldown_header_loading);
        showRefreshingProgressBar(false);
        tv_state = (TextView) headerView.findViewById(R.id.pulldown_header_text);
        headerView.measure(0, 0);  // 主动触发测量,mesure内部会调用onMeasure
        headerViewHeight = headerView.getMeasuredHeight();
        hideHeaderView();
        super.addHeaderView(headerView);
        upAnim = createRotateAnim(0f, -180f);
        downAnim = createRotateAnim(-180f, -360f);
    }

    private void initFooterView() {
        footerView = View.inflate(getContext(), R.layout.layout_listview_footer3, null);
        tv_loading = footerView.findViewById(R.id.tv_loading);
        footerView.measure(0, 0);// 主动触发测量,mesure内部会调用onMeasure
        footerViewHeight = footerView.getMeasuredHeight();
        hideFooterView();
        super.addFooterView(footerView);
        super.setOnScrollListener(new OnScrollListener() {
            // 当ListView滚动的状态发生改变的时候会调用这个方法
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
                if (scrollState == OnScrollListener.SCROLL_STATE_IDLE  // ListView处于空闲状态
                        && getLastVisiblePosition() == getCount() - 1  // 界面上可见的最后一条item是ListView中最后的一条item
                        && loadingMore == false              // 如果当前没有去做正在加载更多的事情
                ) {
                    loadingMore = true;
                    showFooterView();
                    setSelection(getCount() - 1);
                    if (mOnRefreshingListener != null) {
                        mOnRefreshingListener.onLoadMore();
                    }
                }
            }
            // 当ListView滚动的时候会调用这个方法
            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
            }
        });
    }

    public void showNoMoreDataView(){
        isNoMoreData = true;
        showFooterView();
    }

    private void hideFooterView() {
        int paddingV = -footerViewHeight;
        setFooterViewPaddingTop(paddingV);
    }

    private void showFooterView() {
        int paddingV = 30;
        setFooterViewPaddingTop(paddingV);
    }

    private void setFooterViewPaddingTop(int paddingV) {
        footerView.setPadding(0, paddingV, 0, paddingV);
        if(isNoMoreData){
            tv_loading.setText("没有更多了~");
        }else{
            tv_loading.setText("加载中...");
        }
    }

    /**
     * 设置显示进度的圈圈
     * @param showProgressBar 如果是true,则显示ProgressBar,否则的话显示箭头
     */
    private void showRefreshingProgressBar(boolean showProgressBar) {
        progress_bar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE);
        iv_arrow.setVisibility(!showProgressBar ? View.VISIBLE : View.GONE);
        if (showProgressBar) {
            iv_arrow.clearAnimation();  // 有动画的View要清除动画才能真正的隐藏
        }
    }

    /**
     * 创建旋转动画
     * @param fromDegrees 从哪个角度开始转
     * @param toDegrees 转到哪个角度
     * @return
     */
    private RotateAnimation createRotateAnim(float fromDegrees, float toDegrees) {
        int pivotXType = RotateAnimation.RELATIVE_TO_SELF;    // 旋转点的参照物
        int pivotYType = RotateAnimation.RELATIVE_TO_SELF;    // 旋转点的参照物
        float pivotXValue = 0.5f;  // 旋转点x方向的位置
        float pivotYValue = 0.5f;  // 旋转点y方向的位置
        RotateAnimation ra = new RotateAnimation(fromDegrees, toDegrees, pivotXType, pivotXValue, pivotYType, pivotYValue);
        ra.setDuration(300);
        ra.setFillAfter(true);  // 让动画停留在结束位置
        return ra;
    }

    /** 隐藏HeaderView */
    private void hideHeaderView() {
        int paddingV = -headerViewHeight;
        setHeaderViewPaddingTop(paddingV);
    }

    /** 显示HeaderView */
    private void showHeaderView() {
        int paddingV = 50;
        setHeaderViewPaddingTop(paddingV);
    }

    /**
     * 设置HeaderView的paddingTop
     * @param paddingV
     */
    private void setHeaderViewPaddingTop(int paddingV) {
        headerView.setPadding(0, paddingV, 0, paddingV);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                if (currentState == STATE_REFRESHING) {
                    // 如果当前已经是“正在刷新“的状态了,则不用去处理下拉刷新了
                    return super.onTouchEvent(ev);
                }
                int fingerMoveDistanceY = (int) (ev.getY() - downY);    // 手指移动的距离
                // 如果是向下滑动,并且界面上可见的第一条item是ListView的索引为0的item时我们才处理下拉刷新的操作
                if (fingerMoveDistanceY > 0 && getFirstVisiblePosition() == 0) {
                    int paddingTop = -headerViewHeight + fingerMoveDistanceY;
                    if(paddingTop <= 200){
                        setHeaderViewPaddingTop(paddingTop);
                    }

                    if (paddingTop < 0 && currentState != STATE_PULL_TO_REFRESH) {
                        // 如果paddingTop小于0,说明HeaderView没有完全显示出来,则进入下拉刷新的状态
                        currentState = STATE_PULL_TO_REFRESH;
                        tv_state.setText("下拉刷新");
                        iv_arrow.startAnimation(downAnim);
                        showRefreshingProgressBar(false);
                        // 让箭头转一下
                    } else if (paddingTop >= 0 && currentState != STATE_RELEASE_REFRESH) {
                        // 如果paddingTop>=0,说明HeaderView已经完全显示出来,则进入松开刷新的状态
                        currentState = STATE_RELEASE_REFRESH;
                        tv_state.setText("松开刷新");
                        iv_arrow.startAnimation(upAnim);
                        showRefreshingProgressBar(false);
                    }
                    return true;
                }
                break;
            case MotionEvent.ACTION_UP:
                if (currentState == STATE_RELEASE_REFRESH) {
                    // 如果当前状态是松开刷新,并且抬起了手,则进入正在刷新状态
                    currentState = STATE_REFRESHING;
                    tv_state.setText("正在刷新");
                    showRefreshingProgressBar(true);
                    showHeaderView();
                    if (mOnRefreshingListener != null) {
                        mOnRefreshingListener.onRefreshing();
                    }
                } else if (currentState == STATE_PULL_TO_REFRESH) {
                    // 如果抬起手时是下拉刷新状态,则把HeaderView完成隐藏
                    hideHeaderView();
                }
                break;
        }
        return super.onTouchEvent(ev);
    }

    public void setOnRefreshingListener(OnRefreshingListener mOnRefreshingListener) {
        this.mOnRefreshingListener = mOnRefreshingListener;
    }

    /**
     * ListView刷新的监听器
     */
    public interface OnRefreshingListener {
        //当ListView可以刷新数据的时候会调用这个方法
        void onRefreshing();
        //当ListView可以加载更多的时候会调用这个方法
        void onLoadMore();
    }

    /**
     * 联网刷新数据的操作已经完成了
     */
    public void onRefreshComplete() {
        hideHeaderView();
        hideFooterView();
        isNoMoreData = false;
        currentState = STATE_PULL_TO_REFRESH;
        showRefreshingProgressBar(false);
    }

    /**
     * 加载更多新数据的操作已经完成了
     */
    public void onLoadmoreComplete() {
        if(!isNoMoreData){
            hideFooterView();
        }
        loadingMore = false;
    }
}

使用控件:

class ListViewActivity8 : AppCompatActivity(){

    private lateinit var listView: PullToRefreshListView
    private val fruitList = arrayListOf<Fruit>()
    private var index = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_listview4)
        refreshData()
        //获取控件
        listView = findViewById(R.id.listView)
        listView.adapter =
            MyAdapter2(
                this,
                fruitList
            )

        listView.setOnRefreshingListener(object : PullToRefreshListView.OnRefreshingListener{
            override fun onRefreshing() {
                Handler().postDelayed({
                    refreshData()
                    listView.onRefreshComplete()
                },1500)
            }

            override fun onLoadMore() {
                Handler().postDelayed({
                    concatData()
                    listView.onLoadmoreComplete()
                },1500)
            }

        })
    }

    private fun refreshData() {
        fruitList.clear()
        for (i in 0..20) {
            fruitList.add(Fruit("item_0", R.drawable.code_coach,""))
        }
        index = 1
    }

    private fun concatData() {
        if(index >= 4){
            listView.showNoMoreDataView()
        }else{
            for (i in 0..20) {
                fruitList.add(Fruit("item_$index", R.drawable.code_coach,""))
            }
            index ++
        }
    }
}

工作原理


ListView 仅是作为容器(列表),用于装载显示数据(就是上面的一个个的红色框的内容,也称为 item)。item 中的具体数据是由适配器(adapter)来提供的。

适配器(adapter):作为 View (不仅仅指的 ListView)和数据之间的桥梁或者中介,将数据映射到要展示的 View 中。这就是最简单适配器模式,也是适配器的主要作用!

当需要显示数据的时候,ListView 会从适配器(Adapter)中取出数据,然后来加载数据。

ListView 负责以列表的形式向我们展示 Adapter 提供的内容

缓存原理


前面讲了 ListView 负责把 Adapter 提供的内容一一的展现出来,每一条数据对应一个 item 。试想如果把所有的数据信息全部加载到 ListView 上显示,加入这些数据有 100 条。那么 ListView 就要创建 100 个视图。如果有更多的数据,那么 ListView 就会创建更多的视图。这种行为显然是不可取的,这样会消耗大量的内容。

解决方案:

为了节省内存的占用,ListView 是不会为每一条数据创建一个视图的,而是采用了 Recycler组件 的方式。回收和复用 View。

那么是如何来复用的呢?

我们都知道一个屏幕可见的内容就是那么大,所以用户一次能看到的 item 就是固定的那么几个。假如当屏幕一次可以显示 x 个 item 时(不用是完整的),那么 ListView 会创建 x+1 个视图;当第1个 item 离开屏幕的时候,此时这个 item 的 View 就会被回收,再入屏的 item 的 View 就会优先从该缓存中获取。

只有 item 完全离开屏幕后才会复用,这也是为什么 ListView 要创建比屏幕需要显示视图多 1 个的原因:缓冲显示视图。
第 1 个 item 离开屏幕是有一个过程的,会有 1 个 第一个 item 的下半部分 & 第 X+1 个 item 的上半部分同时在屏幕中显示的状态 这种情况是没法使用缓存的 View 的。只能继续用新创建的视图 View。

实例演示:

假如屏幕一次只能显示 5 个 item,那么 ListView 会创建 (5+1)个 item 视图;当第 1 个 item 完全离开屏幕后才会回收至缓存,从而复用。(用于显示第 7 个 item)。

演示图来自网络:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容