“我有一壶酒,足以慰风尘。尽倾江海里,赠饮天下人。”
ListView 小扩展
前言
之前写 app 的时候写一个分类+一堆点击按钮的页面,应该会放个页面截图在下面嵌套的部分
然后写着写着想,这用只用个 ListView 似乎有点简单
不如显摆一下,用 ListView 嵌套 GridView 吧!
然后折腾了好久,把自己都绕晕了,最后也算成功嵌套,所以有了这篇博客的大部分内容
又然后转念一想只写个嵌套好像显得没什么内容啊,毕竟嵌套也没什么难点,就是 ListView,GridView,Adapter 什么的容易绕晕
于是就在前面又顺便写了一下 ListView 其他一些用法凑数。。。
ListView 的 Item 点击事件
ListView 也没什么特别的逻辑事件,主要就是单击和长按两个事件,比较简单,主要可以分为三步
- 实现接口
- 设置监听器
- 重写方法
单击点击事件
对于单击事件,我们要实现的接口是 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
类型的,这又是为什么呢?
实际上,这里返回的 true
和 false
代表“点击事件是否被消化” 。
如果是 false,代表不消化点击事件,所以点击事件还会继续向下传递。也就是说,当我们长按完了后,单击事件会继续执行。举个 🌰,我们长按第一个 item 然后松开。屏幕上先出现长按的 Toast “long click: 0”,然后出现单击的 Toast “click: 0”,相当于判定我们既长按,又单击。
同样的,如果是 true,表示点击事件被消化,不会继续传递,长按完了就完了,屏幕只会出现长按的 Toast “long click: 0”。(就像食物在消化道里一样, 被消化吸收了就不会往下走了)
一般都设置为 true
啦。
利用 Selector 进行 Item 点击背景的切换
Selector 大家都不陌生,因为它 state_pressed
、state_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 了= =
从 Activity 到 ListView 的 Adapter
我们都知道,ListVie w 说到底只是一个控件,而 Adapter 用来把数据载入到 ListView。所以我们在 Activity 中要做的其实就两件事
- 把数据传给 Adapter
- 让 View 绑定 Adapter
结合我们 ListView 嵌套 GridView,相当于 Activity 中有个 ListView 和 ListView 的 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 的数据即可。
总结一下数据源,是这样的:
- HashMap:某 GridView 的一个 Item 的数据
- ArrayList<HashMap<String, Object»:将所有 HashMap 组成列表,即一个 GridView 的数据
- 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[] topics
和 ArrayList<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 了。
后记
啊哈哈,坑越来越多,写越来越懒得写(✿◡‿◡)
这一篇其实我个人也不是特别满意,因为干货挺少,比较水的一篇
下次一定!