ListView小拓展(主要是我想写嵌套)

ListView的Item点击事件、Item点击背景、ListView嵌套GridView

Posted by BlackDn on July 20, 2021

“我有一壶酒,足以慰风尘。尽倾江海里,赠饮天下人。”

ListView 小扩展

前言

之前写 app 的时候写一个分类+一堆点击按钮的页面,应该会放个页面截图在下面嵌套的部分
然后写着写着想,这用只用个 ListView 似乎有点简单
不如显摆一下,用 ListView 嵌套 GridView 吧!
然后折腾了好久,把自己都绕晕了,最后也算成功嵌套,所以有了这篇博客的大部分内容
又然后转念一想只写个嵌套好像显得没什么内容啊,毕竟嵌套也没什么难点,就是 ListView,GridView,Adapter 什么的容易绕晕
于是就在前面又顺便写了一下 ListView 其他一些用法凑数。。。

ListView 的 Item 点击事件

ListView 也没什么特别的逻辑事件,主要就是单击和长按两个事件,比较简单,主要可以分为三步

  1. 实现接口
  2. 设置监听器
  3. 重写方法

单击点击事件

对于单击事件,我们要实现的接口是 AdapterView.OnItemClickListener,然后重写他的抽象方法 onItemClick
一共有四个参数,看看官方文档

参数 作用
AdapterView<?> adapterView 点击所发生的 AdapterView
View view AdapterView 里被点击的具体控件(由 Adapter 提供的控件),通常是我们的 Item
int position 我们点击的 Item 的位置
long id 我们点击的 Item 的编号,通常和 position 相同

顺便补充一点,AdapterView 其实是 ViewGroup 的一个子类,个人理解为一些需要用的 Adapter 的控件类,比如 ListView、GridView 等都是他的后代类。像 setOnItemClickListener 这种方法都是在他里面的(我好像在哪里提到过这个点但是我不记得了…)

监听器的设置也简单,一句话 listView.setOnItemClickListener(this);
最后我们就可以在外面实现方法了,完整代码像这样:(Adapter 复用之前的 ArrayAdapter,详见之前博客

public class ListViewActivity extends AppCompatActivity implements AdapterView.OnItemClickListener {
    private ListView listView;

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

        String[] data = {"我是1", "我是2", "我是3","我是4", "我是5"};
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data);

        listView = findViewById(R.id.list_view);
        listView.setAdapter(adapter);
        listView.setOnItemClickListener(this);
    }
    @Override
    public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
        Toast.makeText(this, "click: " + position , Toast.LENGTH_SHORT).show();
    }
}

长按点击事件

长按点击事件也大同小异,就是方法名字的差别,我们可以先直接来看代码:

public class ListViewActivity extends AppCompatActivity implements AdapterView.OnItemClickListener
    , AdapterView.OnItemLongClickListener {
    private ListView listView;

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

        String[] data = {"我是1", "我是2", "我是3","我是4", "我是5"};
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data);

        listView = findViewById(R.id.list_view);
        listView.setAdapter(adapter);
        listView.setOnItemClickListener(this);
        listView.setOnItemLongClickListener(this);
    }
    @Override
    public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
        Toast.makeText(this, "click: " + position , Toast.LENGTH_SHORT).show();
    }
    @Override
    public boolean onItemLongClick(AdapterView<?> adapterView, View view, int position, long id) {
        Toast.makeText(this, "long click: " + position , Toast.LENGTH_SHORT).show();
        return false;
    }
}

没错,就是方法名里多了个 long
实现的接口名为 AdapterView.OnItemLongClickListener ,设置的监听器为 setOnItemLongClickListener, 重写的方法是 onItemLongClick ,其他的基本一样

当然细心的你可能发现了(我不是说你没发现就不细心的意思…),这个方法是 boolean 类型的,这又是为什么呢?
实际上,这里返回的 truefalse 代表“点击事件是否被消化”
如果是 false,代表不消化点击事件,所以点击事件还会继续向下传递。也就是说,当我们长按完了后,单击事件会继续执行。举个 🌰,我们长按第一个 item 然后松开。屏幕上先出现长按的 Toast “long click: 0”,然后出现单击的 Toast “click: 0”,相当于判定我们既长按,又单击。
同样的,如果是 true,表示点击事件被消化,不会继续传递,长按完了就完了,屏幕只会出现长按的 Toast “long click: 0”。(就像食物在消化道里一样, 被消化吸收了就不会往下走了)
一般都设置为 true 啦。

利用 Selector 进行 Item 点击背景的切换

Selector 大家都不陌生,因为它 state_pressedstate_checked等属性,能很方便地针对控件的不同状态进行修改,特别是按钮背景之类的样式。
那如果我想修改 Item 的背景呢?一般会很自然地想到我要去 item 的布局文件的根布局,修改 background 属性,但这样往往会出现一些意想不到的问题。
仔细一想我们好像也没有在根布局里给 background 增加 Selector,毕竟因为布局的嵌套往往会出现意料之外的错误。我们平时都是直接修改 Button、TextView 这些单独控件的 background。那对于 ListView 的 Item 要怎么办呢?

其实这点 ListView 已经帮我们考虑到了,就是 ListView 自带的一个属性 listSelector 。将我们的 Selector 放到这里,就不用担心出现奇奇怪怪的意外了。

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

顺便一提这是 Selector 的代码。未被点击就是透明的(#00000000),被点击了我随便挑了一个颜色作为新的背景。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@color/transparent" android:state_pressed="false" />
    <item android:drawable="@color/purple_200" android:state_pressed="true" />
</selector>

当然,如果 Item 里面有 Button 控件的话,Selector 还可以用来自定义 Button 的点击前后样式。
值得一提的是,在设置 Selector 之前,如果 Item 中有 Button 的话,点击 Item 可能会出现没有反馈的情况,Item 的背景不会有变化(默认是会整行变成灰色的吧)
这是由于 Item 中的 Button 抢夺焦点所导致的,需要在 Item 布局的最外层添加 android:descendantFocusability=”blocksDescendants” 属性来解决。

ListView 嵌套 GridView

这一部分就是我主要想写的内容啦,所以和上面的基本没什么关系,像 Item 的布局啊什么的都是新的了。

其实只要弄清楚几个 View 和 Adapter 之间的关系,ListView 嵌套 GridView 并没有什么难点,就是比较容易弄混,看着看着头就晕了。
而 GridView 和 ListView 大同小异,一个是网格布局,一个是列表布局。主要区别在于 GridView 的特有的一些属性,比如android:numColumns表示 Item 的列数,android:horizontalSpacing表示两列之间的间隔,android:columnWidth表示一列的宽度等等,关于 GridView 的使用就不介绍了,具体可以看这个:GridView 基本使用方法

自上而下分析

先来分析一下咱们的逻辑,毕竟我就是这里被绕晕的。当初实现的结果大致是这样,然后我要开始 bb 了= =

Wdcrzd.png

从 Activity 到 ListView 的 Adapter

我们都知道,ListVie w 说到底只是一个控件,而 Adapter 用来把数据载入到 ListView。所以我们在 Activity 中要做的其实就两件事

  1. 把数据传给 Adapter
  2. 让 View 绑定 Adapter

结合我们 ListView 嵌套 GridView,相当于 Activity 中有个 ListViewListView 的 Adapter,然后把数据传给 Adapter,让 ListView 绑定 Adapter。

那么数据是什么呢?
举个例子,我们之前学 自定义 Adapter 的时候,每个 ListView 的 Item 就是一个图片加一些文字,我们把它们放在一个 HashMap 里,作为一个 Item 的数据。最后把所有 HashMap 组成一个 ArrayList 传给 Adatper。在 Adapter 中,遍历 ArrayList 依次取出每个 HashMap,再从 HashMap 中取出数据传给 ListView 的 Item。

我们的 ListView 的每个 Item 中,有一个标题和一个 GridView ,而 GridView 需要的数据就是每个 Item 的文本(目前每个 Item 是一个按钮),所以我这的数据有两个,分别是 作为 ListView 的 Item 标题的 String[] 和 作为 GridView 里每个 Item 数据的 ArrayList<ArrayList<HashMap<String, Object>>>

好了,现在数据已经从 Activity 传到了 ListView 的 Adapter 中。所以接下来要在 Adapter 中做事了。

ListView 的 Adapter 到 GridView 的 Adapter

在 Adapter 中做事相信大家已经轻车熟路了,我们收到了上面传进来的两组数据,标题拿出来给到 ListView 的 Item 标题,而 ArrayList 则要额外操作一下。

因为 ArrayList 里是 GridView 的数据,我们要把他传到 GridView 的 Adapter 中。一个 GridView 有有很多个 Item,所以一个 GridView 的数据是一个 ArrayList<HashMap<String, Object>>。我们收到的 ArrayList<ArrayList<HashMap<String, Object>>> 则是所有 GridView 的数据,相当于把每个 GridView 一一放到列表里。所以在这里还要对这个列表进行个解封装,取出 ArrayList<HashMap<String, Object>> 传给 GridView 的 Adapter,所以 GridView 的 Adapter 是在这里创建的。

这也是为什么 ListView 的 Item 和 GridView 的 Item 要分开两组数组,因为前者是在 ListViewAdapter 中进行绑定,后者是要传到 GridViewAdapter 中进一步操作。说白了就是把 GridView 的数据额外拉出来,传给 GridView 的 Adapter。

GridView 的 Adapter

注意,我们现在收到的数据是单个 GridView 的数据,也就是 ArrayList<HashMap<String, Object>>,这看起来就简单了,和平时的 Adapter 相差无几
然后取出一个 HashMap 对应一个 Item 的数据即可。

总结一下数据源,是这样的:

  1. HashMap:某 GridView 的一个 Item 的数据
  2. ArrayList<HashMap<String, Object»:将所有 HashMap 组成列表,即一个 GridView 的数据
  3. ArrayList<ArrayList<HashMap<String, Object»>:将每个 GridView 的数据组成列表,即 ListView 的数据

自下而上敲码

为了避免在几个类中跳来跳去来回切换,我们就自下而上看看代码,不过最开始还是先搞定布局。

布局

1. 主页面布局:fragment_index.xml

因为我的主页面是 Fragment 而非 Activity,所以布局为 fragment_index.xml,非常简单,就一个 ListView

<?xml version="1.0" encoding="utf-8"?>
<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:id="@+id/index_listview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:focusable="false"/>
</LinearLayout>
2. ListView 的 Item 布局:item_index_listview.xml

ListView 的 Item 布局我命名为 item_index_listview.xml,此处的 NoScrollGridView

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/index_listview_textview"
        android:text="@string/default_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="24sp"
        android:paddingLeft="15dp" />
    <com.example.mmmianjing.view.NoScrollGridView
        android:id="@+id/index_listview_item_gridview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:stretchMode="columnWidth"
        android:verticalSpacing="15dp"
        android:numColumns="3"
        android:gravity="center"
        android:layout_marginBottom="20dp"
        android:layout_marginTop="10dp"/>
</LinearLayout>
3. GridView 的 Item 布局:item_index_listview.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/index_gridview_button"
        android:text="@string/default_text"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_gravity="center"
        android:textAllCaps="false"
        android:textColor="@color/black"/>
</LinearLayout>

GridView 的 Adapter

自下而上,最底下的是 GridView 的 Adapter,我这命名 MainListGridViewAdapter,我也不知道为什么脑子抽了起这个名字。
为了偷懒节省篇幅,getCount()等方法就不写了,就是对数据源 gridDataSource 的长度判定等内容。主体还是在 getView()方法里。

public class MainListGridViewAdapter extends BaseAdapter {
    private Context context;
    private ArrayList<HashMap<String, Object>> gridDataSource;

    public MainListGridViewAdapter(Context context, ArrayList<HashMap<String, Object>> gridDataSource) {
        super();
        this.context = context;
        this.gridDataSource = gridDataSource;
    }
	//getCount(), getItem(), getItemId()...
    @Override
    public View getView(int position, View convertView, ViewGroup viewGroup) {
        ViewHolder holder;
        if (convertView == null) {
            holder = new ViewHolder();
            convertView = LayoutInflater.from(this.context).inflate(R.layout.item_index_listview_gridview, null, false);
            holder.button =convertView.findViewById(R.id.index_gridview_button);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        if (gridDataSource != null) {
            HashMap<String, Object> hashMap = gridDataSource.get(position);
            if (holder.button != null) {
                holder.button.setText(hashMap.get("content").toString());
                holder.button.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        //TODO: 按钮点击
                    }
                });
            }
        }
        return convertView;
    }
    public class ViewHolder {
        Button button;
    }
}

比较标准的一个自定义 View,绑定布局、绑定控件、控件设置(设置按钮事件)一气呵成,因为很怕 NPE 所以加了些非空判断。
每个 GridView 的 Item 是一个按钮,我们给按钮加上个文字就好了。

ListView 的 Adapter

因为 GridViewAdapter 是在 ListViewAdapter 里实现的,数据也是来自 ListViewAdapter ,向上走我们来实现 ListViewAdapter 。我命名为 MainListAdapter。
同样,getCount() 等方法就不实现了。

public class MainListAdapter extends BaseAdapter {
    private Context context;
    private ArrayList<ArrayList<HashMap<String, Object>>> listDataSource;
    private  String[] topics;

    public MainListAdapter(Context context, ArrayList<ArrayList<HashMap<String, Object>>> listDataSource, String[] topics) {
        super();
        this.context = context;
        this.listDataSource = listDataSource;
        this.topics = topics;
    }
    @Override
    public View getView(int position, View convertView, ViewGroup viewGroup) {
        ViewHolder holder;
        if (convertView == null) {
            holder = new ViewHolder();
            convertView = LayoutInflater.from(this.context).inflate(R.layout.item_index_listview, null, false);
            holder.textView = convertView.findViewById(R.id.index_listview_textview);
            holder.gridView = convertView.findViewById(R.id.index_listview_item_gridview);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        ArrayList<HashMap<String, Object>> gridDataSource = listDataSource.get(position);
        MainListGridViewAdapter gridViewAdapter = new MainListGridViewAdapter(context, gridDataSource);
        holder.gridView.setAdapter(gridViewAdapter);
        holder.textView.setText(topics[position]);
        return convertView;
    }
    public class ViewHolder {
        TextView textView;
        GridView gridView;
    }
}

可以看到,在这里我们有两组数据,分别是 String[] topicsArrayList<ArrayList<HashMap<String, Object>>> listDataSource
前者是个字符串的标题,我们用 holder.textView.setText(topics[position]); 传给 TextView,后者则是所有 GridView 的数据。
但是我们的 GridViewAdapter 是在 getView() 方法里实现的,然后给当前的 GridView 绑定,因此我们传给 GridView 的数据只用是一个 GridView 的数据,所以我们要先 listDataSource.get(position) 来得到一个 GridView 的数据,然后传给 GridViewAdapter 就好了。

给 GridViewAdapter 传入数据后,最后给 ViewHolder 中每个 GridView 绑定这个传完数据的 GridViewAdapter 就完事了。

MainActivity 中(我这是 Fragment)

最后来到 MainActivity 中,不过我这是 Fragment,相信大家都能看懂。

public class IndexFragment extends Fragment {
    private ListView listView;
    private MainListAdapter listAdapter;
    private ArrayList<ArrayList<HashMap<String, Object>>> listViewDataSource;
    private final String[] TOPICS = new String[]{"Android基础知识", "java基础知识", "计算机基础", "代码"};
    private final String[][] SUBJECTS = new String[][]{
            {"四大组件", "自定义View & 动画", "性能优化", "IPC", "WebView"},
            {"java特性", "java基础", "抽象类及接口", "JVM", "java容器类","java多线程"},
            {"数据结构", "计算机网络", "操作系统"},
            {"链表", "栈&队列&堆", "递归&回溯&分治", "动态规划", "贪心算法"}};
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_index, container, false);
        listView = view.findViewById(R.id.index_listview);
        listViewDataSource = new ArrayList<>();
        for (int i = 0; i < TOPICS.length; i++) {
            ArrayList<HashMap<String, Object>> gridViewDataSource = new ArrayList<>();  //一个gridview的数据源
            for (int j = 0; j < SUBJECTS[i].length; j++) {
                HashMap<String, Object> gridViewItemDataSource = new HashMap<>();   //gridview每个item的数据源
                gridViewItemDataSource.put("content", SUBJECTS[i][j]);
                gridViewDataSource.add(gridViewItemDataSource);
            }
            listViewDataSource.add(gridViewDataSource);
        }
        listAdapter = new MainListAdapter(getContext(), listViewDataSource, TOPICS);
        listView.setAdapter(listAdapter);
        return view;
    }
}

可以看到,我们在一开始准备了两个数据源,分别是 ListView 的 Item 标题 TOPICS 和 GridView 的数据(按钮的文字) SUBJECTS
而在这里我们将SUBJECTS 逐个放入 HashMap 中,再将 HashMap 串成列表、把列表串成列表……总之就是把数据逐步封装进 ArrayList<ArrayList<HashMap<String, Object>>> listViewDataSource 传到 ListView 的 Adapter 中。
至于 TOPICS ,就直接传给 Adapter 了。

后记

啊哈哈,坑越来越多,写越来越懒得写(✿◡‿◡)
这一篇其实我个人也不是特别满意,因为干货挺少,比较水的一篇
下次一定!

参考

  1. 孙老师课堂
  2. developers:AdapterView.OnItemClickListener
  3. ListView 嵌套 GridView 使用详解
  4. GridView 基本使用方法
  5. Adapter:ArrayAdapter 和 SimpleAdapter 适配 ListView