3

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

 1 year ago
source link: https://blackdn.github.io/2021/07/20/more-ListView-2021/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

ListView小扩展

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

ListView的Item点击事件

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

  1. 设置监听器

单击点击事件

对于单击事件,我们要实现的接口是 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了。

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



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK