Android Material Design 组件实战:SwitchMaterial、Chip与ChipGroup深度解析
1. 从 Material Design 2 到 Material 3:为什么你还在用 SwitchMaterial?
如果你最近几年一直在做 Android 开发,打开一个几年前的老项目,或者在网上搜索“Android Switch 开关”,大概率会看到SwitchMaterial这个组件。它曾经是 Material Design 组件库(MDC)中用于替代原生Switch的明星控件,提供了符合 Material Design 规范的视觉设计和开箱即用的无障碍支持。但今天,当 Material Design 3(Material You)已经成为 Android 13+ 的默认设计语言,Google 也大力推广 Jetpack Compose 时,我们为什么还要花时间了解SwitchMaterial、Chip和ChipGroup这些“老古董”呢?
原因很简单:存量与过渡。市面上仍有海量的 App 基于传统的 View 系统开发,它们可能短期内无法、也没必要全面迁移到 Compose。对于这些项目,Material Components for Android(MDC)库依然是构建现代化 UI 最可靠、最高效的选择。SwitchMaterial作为 MDC 对开关控件的标准化实现,理解了它,你就能明白 Material Design 在交互细节上的考量。而Chip和ChipGroup,更是筛选、标签、输入等场景下不可或缺的“瑞士军刀”,它们的灵活性和功能完整性,即使在今天看也毫不过时。
更重要的是,学习这些组件不仅仅是学习 API 调用,更是理解 Material Design 的设计模式。比如,SwitchMaterial的触摸区域(thumb track)设计、Chip的各种形态(Input, Filter, Choice, Action)所对应的不同交互语义,这些都是构建优秀用户体验的基石。无论你未来是继续深耕 View 系统,还是转向 Compose,这些设计思想都是相通的。Compose 的 Material 3 组件库中,同样有Switch、Chip、FilterChip、ChipGroup等,其核心概念一脉相承。
所以,这篇文章将深入这三个组件的每一个角落。我不会只给你一堆代码片段,而是会结合 Material Design 规范,告诉你为什么要这样设计,在什么场景下该用哪种变体,以及在实际开发中容易踩哪些坑。我们从那个看似简单,却暗藏玄机的SwitchMaterial开始。
2. SwitchMaterial 深度解析:不只是个会动的开关
androidx.appcompat.widget.SwitchCompat是支持库时代的产物,而com.google.android.material.switchmaterial.SwitchMaterial则是它的 Material Design 升级版,继承自SwitchCompat,并添加了 Material 主题的样式支持。它的核心价值在于提供了符合 Material Design 2 规范的视觉表现,包括更大的触摸目标、更流畅的动画以及更协调的颜色系统集成。
2.1 基础使用与主题集成
在布局文件中使用SwitchMaterial非常简单:
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/switch_notification" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="推送通知" android:checked="true" app:useMaterialThemeColors="true" />这里有几个关键属性:
android:text:开关旁边的描述文本。android:checked:初始选中状态。app:useMaterialThemeColors="true":这是一个非常重要的属性。当设置为true时,开关的颜色(包括开启状态的轨道和拇指颜色)将自动从你的 Material 主题中提取colorSecondary(MD2)或colorPrimary(MD3)等颜色属性。这确保了开关的颜色与你的应用整体配色方案保持一致。如果你不设置这个属性,或者设置为false,它将使用默认的 Android 系统配色(通常是蓝色),这可能会破坏你的应用视觉统一性。
在代码中监听状态变化是最常见的操作:
binding.switchNotification.setOnCheckedChangeListener { buttonView, isChecked -> // isChecked 是新的状态 if (isChecked) { // 开启通知逻辑 saveNotificationPreference(true) } else { // 关闭通知逻辑 saveNotificationPreference(false) } // 注意:buttonView 就是 SwitchMaterial 实例本身 }注意:
setOnCheckedChangeListener有一个常见的“坑”。这个监听器不仅在用户交互时触发,当你通过代码调用setChecked()方法改变状态时,它同样会触发。如果你在初始化视图或者在某个业务逻辑中通过代码设置状态,这可能会导致你的业务逻辑被意外执行两次。为了避免这个问题,你可以在需要代码设置状态前,先移除监听器,设置完成后再添加回去。或者,更优雅的做法是,在监听器内部通过判断buttonView.isPressed或记录事件来源来区分是用户操作还是代码调用。
2.2 自定义与样式覆盖
虽然主题集成很方便,但总有需要微调的时候。SwitchMaterial的自定义主要通过覆盖其样式属性实现。
1. 修改轨道(Track)和拇指(Thumb)的颜色:你可以直接通过 XML 属性或样式主题来覆盖。
XML 属性方式(优先级高):
<com.google.android.material.switchmaterial.SwitchMaterial ... app:trackTint="@color/custom_track_tint_selector" app:thumbTint="@color/custom_thumb_tint_selector" />这里的
@color/...应该是一个 ColorStateList(颜色选择器),而不是一个简单的颜色值。因为开关在不同状态(checked/unchecked, enabled/disabled)下需要显示不同的颜色。例如,custom_track_tint_selector.xml可能长这样:<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="@color/green_500" android:state_checked="true" android:state_enabled="true"/> <item android:color="@color/grey_300" android:state_checked="false" android:state_enabled="true"/> <item android:alpha="0.38" android:color="@color/black" android:state_enabled="false"/> <!-- 禁用状态 --> </selector>主题样式方式(统一管理):在你的
styles.xml中定义一个继承自 Material 组件样式的新样式:<style name="Widget.MyApp.SwitchMaterial" parent="Widget.MaterialComponents.CompoundButton.Switch"> <item name="trackTint">@color/custom_track_tint_selector</item> <item name="thumbTint">@color/custom_thumb_tint_selector</item> <item name="android:minWidth">?attr/switchMinWidth</item> <item name="android:minHeight">?attr/switchMinHeight</item> </style>然后在你的应用主题或控件上应用这个样式:
style="@style/Widget.MyApp.SwitchMaterial"。
2. 修改文本样式:SwitchMaterial的文本实际上是一个TextView,你可以通过android:textAppearance属性或者直接设置android:textSize、android:textColor等来修改。
3. 关于app:thumbIcon和app:trackDecoration:在一些 Material Design 示例或更高级的定制中,你可能会看到为拇指添加图标(比如一个小太阳或月亮)或者在轨道上添加装饰。这些属性在SwitchMaterial中并不直接存在。它们是 Material Design 3 中Switch的 Filled 或 Outlined 变体才支持的特性。在 MDC 的SwitchMaterial中,要实现类似效果,通常需要更复杂的自定义 Drawable 或考虑使用其他第三方库,这超出了基础使用的范围。如果你的设计强烈要求这种效果,可能需要评估是接受 MD2 的标准样式,还是推动设计向 MD3 靠拢,或者投入成本进行深度自定义。
2.3 无障碍支持与测试要点
SwitchMaterial一个重要的优点是它内置了较好的无障碍支持。开关状态(选中/未选中)会被自动报告给屏幕阅读器(如 TalkBack)。但作为开发者,你仍需确保:
- 有意义的文本:
android:text属性必须清晰描述这个开关控制什么(例如,“开启夜间模式”比“开关”要好得多)。 - 状态描述:在某些复杂场景下,如果开关状态需要更详细的描述,可以考虑使用
android:contentDescription属性,但通常text已足够。 - 测试:在真机上开启 TalkBack,导航到你的开关,听听朗读的内容是否准确、易懂。同时测试开关操作是否顺畅。
一个常见的无障碍问题是触摸目标大小。Material Design 指南要求触摸目标至少为 48dp x 48dp。SwitchMaterial默认已经考虑了这一点,它的可点击区域是大于视觉上的拇指和轨道的。你可以通过android:minWidth和android:minHeight属性来进一步调整,但一般不需要。
3. Chip 组件全览:四种形态应对四大场景
Chip是 Material Design 中一个极其灵活的组件,它本质是一个紧凑的按钮,可以显示文本、图标,并能被选中或删除。根据其行为和用途,主要分为四种类型,在 MDC 中通过style属性来指定:
- 输入型芯片(Input Chip):
style="@style/Widget.MaterialComponents.Chip.Input" - 筛选型芯片(Filter Chip):
style="@style/Widget.MaterialComponents.Chip.Filter" - 选择型芯片(Choice Chip):
style="@style/Widget.MaterialComponents.Chip.Choice" - 操作型芯片(Action Chip):
style="@style/Widget.MaterialComponents.Chip.Action"
此外,还有一个基础的Chip样式 (@style/Widget.MaterialComponents.Chip.Entry),但通常我们直接使用上述四种语义更明确的变体。
这四种芯片在 UI 上看起来可能相似,但它们的交互语义和用途有本质区别。用错类型会导致用户困惑。
3.1 输入型芯片(Input Chip):代表一个输入实体
这是最常见于搜索框、邮件收件人输入框的芯片。它代表用户已经输入或选择的一个实体(如联系人、标签),并且通常带有一个删除图标(“X”),允许用户移除它。
典型场景:邮件应用的收件人输入框、标签添加、搜索过滤器(已添加的筛选条件)。
关键特性:
- 可删除:点击尾部的删除图标移除芯片。
- 可能可点击:点击芯片主体可能触发查看详情等操作(但移除操作优先通过删除图标)。
- 通常放在一个可编辑的文本输入框附近或内部。
示例代码:
<com.google.android.material.chip.Chip style="@style/Widget.MaterialComponents.Chip.Input" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="张三" app:chipIcon="@drawable/ic_person" // 可选,头像图标 app:closeIconVisible="true" // 显示删除图标 />监听事件:
chip.setOnClickListener { // 处理点击芯片主体(如查看联系人详情) } chip.setOnCloseIconClickListener { // 处理点击删除图标,通常是从列表中移除该芯片 (parent as? ViewGroup)?.removeView(chip) // 同时更新底层数据模型 }3.2 筛选型芯片(Filter Chip):用于多选筛选
这种芯片用于筛选内容,类似于多选框(Checkbox)。它有两种状态:选中和未选中。选中状态通常有更显著的视觉填充。同一组筛选芯片允许多个同时被选中。
典型场景:电商网站的商品分类筛选(如“手机”、“电脑”、“家电”可以多选)、内容标签过滤。
关键特性:
- 可选择:点击芯片在选中/未选中状态间切换。
- 多选:一组内的筛选芯片是独立的多选框。
- 选中状态有视觉强化。
示例代码:
<com.google.android.material.chip.Chip style="@style/Widget.MaterialComponents.Chip.Filter" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="仅看有货" android:checked="true" app:checkable="true" // 对于Filter Chip,这个属性默认为true />监听事件:
chip.setOnCheckedChangeListener { buttonView, isChecked -> // isChecked 为新的选中状态 applyFilter(isChecked) }3.3 选择型芯片(Choice Chip):用于单选选择
这种芯片用于在多个互斥的选项中进行单选,类似于单选框(RadioButton)。同一组内,任何时候只能有一个选择型芯片被选中。
典型场景:排序方式选择(“最新”、“最热”、“价格”)、尺寸选择(“S”、“M”、“L”)。
关键特性:
- 可选择:点击选中。
- 单选:需要与
ChipGroup配合使用才能实现真正的单选行为(见下一节)。 - 视觉上与 Filter Chip 选中态类似,但逻辑是互斥的。
示例代码:
<com.google.android.material.chip.Chip style="@style/Widget.MaterialComponents.Chip.Choice" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="价格升序" app:checkable="true" />重要:单个Choice Chip不会自动实现单选。你必须将它放入ChipGroup并设置app:singleSelection="true"才能获得单选行为。
3.4 操作型芯片(Action Chip):触发一个动作
这种芯片用于触发一个与主要内容相关的动作,它看起来像一个扁平的按钮(Flat Button),但形状是胶囊形的。点击它执行一个操作,它没有选中状态。
典型场景:在卡片底部显示“分享”、“查看更多”、“播放视频”等辅助操作。
关键特性:
- 可点击:点击触发一个动作。
- 无状态:没有选中/未选中概念。
- 类似于按钮,但样式更轻量,用于次级操作。
示例代码:
<com.google.android.material.chip.Chip style="@style/Widget.MaterialComponents.Chip.Action" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="分享结果" app:chipIcon="@drawable/ic_share" // 操作图标很常见 />监听事件:
chip.setOnClickListener { // 执行分享操作 performShareAction() }3.5 Chip 的通用属性与自定义
无论哪种类型的 Chip,都共享一系列强大的属性来控制其外观:
- 文本与图标:
android:text:主文本。app:chipIcon:设置左侧的图标(通常是一个小头像或标志)。app:closeIcon:设置输入型 Chip 的删除图标(默认为“X”)。app:checkedIcon:设置选中状态下显示的图标(Filter/Choice Chip 用)。
- 形状与大小:
app:chipCornerRadius:控制圆角半径,实现“胶囊”形状。app:chipMinHeight:最小高度。
- 颜色:
app:chipBackgroundColor:背景颜色(ColorStateList,支持不同状态)。app:chipStrokeColor:边框颜色(ColorStateList)。app:chipStrokeWidth:边框宽度。app:textStartPadding/app:textEndPadding:文本内边距。
- 交互:
app:checkable:定义 Chip 是否可被选中(对于 Filter/Choice Chip 应为 true)。app:closeIconVisible:控制删除图标是否可见(Input Chip 用)。
深度自定义示例:创建一个带有渐变边框的 Filter Chip这需要结合 ShapeAppearance 和 ColorStateList。首先,定义一个渐变的边框颜色选择器gradient_stroke_selector.xml(这里简化用纯色示例)和形状:
<!-- res/drawable/chip_stroke_background.xml --> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <stroke android:width="2dp" android:color="@color/purple_500" /> <corners android:radius="16dp" /> <!-- 匹配 chipCornerRadius --> <solid android:color="@android:color/transparent" /> </shape>然后,在 Chip 上应用:
<com.google.android.material.chip.Chip style="@style/Widget.MaterialComponents.Chip.Filter" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="自定义边框" app:checkable="true" app:chipStrokeColor="@color/purple_500" app:chipStrokeWidth="2dp" app:chipCornerRadius="16dp" />更复杂的渐变需要使用android:gradient,并可能需在代码中动态创建GradientDrawable。
4. ChipGroup 的布局管理与单选控制
单个 Chip 很有用,但 Chip 的真正威力在于成群出现。ChipGroup是一个特殊的FlowLayout,它专门用于排列多个Chip子视图,并提供了两个核心功能:流式布局和单选行为管理。
4.1 流式布局:自动换行的容器
ChipGroup默认继承自FlowLayout,这意味着其内部的 Chip 会从左到右排列,当一行放不下时,会自动换到下一行。这非常适合处理数量动态变化、长度不一的标签集合。
基本用法:
<com.google.android.material.chip.ChipGroup android:id="@+id/chip_group_tags" android:layout_width="match_parent" android:layout_height="wrap_content" app:singleLine="false" <!-- 允许多行,默认为 false --> app:singleSelection="false"> <!-- 默认 false,允许多选 --> <com.google.android.material.chip.Chip style="@style/Widget.MaterialComponents.Chip.Filter" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Android" /> <com.google.android.material.chip.Chip style="@style/Widget.MaterialComponents.Chip.Filter" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Kotlin" /> <!-- 可以动态添加更多 Chip --> </com.google.android.material.chip.ChipGroup>关键布局属性:
app:singleLine:如果设为true,则所有 Chip 会挤在一行,超出部分不可见。通常保持false。app:chipSpacingHorizontal/app:chipSpacingVertical:分别控制 Chip 之间的水平和垂直间距。你可以用?attr/chipSpacing来引用主题中定义的全局间距。android:layout_gravity/app:alignmentMode:可以控制 Chip 在组内的对齐方式(如center_horizontal,center等)。
4.2 单选行为管理:实现互斥选择
这是ChipGroup最常用的功能之一,尤其是配合Choice Chip。通过设置app:singleSelection="true",ChipGroup会确保其内部所有checkable=true的 Chip 中,有且只有一个处于选中状态。
实现单选筛选的完整示例: 假设我们有一个排序选项组。
<com.google.android.material.chip.ChipGroup android:id="@+id/chip_group_sort" android:layout_width="match_parent" android:layout_height="wrap_content" app:singleSelection="true" <!-- 关键:启用单选模式 --> app:selectionRequired="false"> <!-- 是否必须选中一个?false表示可以不选 --> <com.google.android.material.chip.Chip style="@style/Widget.MaterialComponents.Chip.Choice" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="最新" android:checked="true" /> <!-- 默认选中 --> <com.google.android.material.chip.Chip style="@style/Widget.MaterialComponents.Chip.Choice" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="最热" /> <com.google.android.material.chip.Chip style="@style/Widget.MaterialComponents.Chip.Choice" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="价格" /> </com.google.android.material.chip.ChipGroup>在代码中获取选中的 Chip:
val chipGroup = findViewById<ChipGroup>(R.id.chip_group_sort) // 方法1:设置组级别的监听器(推荐) chipGroup.setOnCheckedStateChangeListener { group, checkedIds -> // checkedIds 是一个 List<Int>,但在 singleSelection=true 时,它通常只有一个元素 val selectedChipId = checkedIds.firstOrNull() selectedChipId?.let { id -> val selectedChip = group.findViewById<Chip>(id) val sortBy = selectedChip.text.toString() applySorting(sortBy) } // 如果 selectionRequired=false 且用户取消了所有选择,checkedIds 会是空的 if (checkedIds.isEmpty()) { clearSorting() } } // 方法2:也可以通过遍历查找 val checkedChipId = chipGroup.checkedChipId // 返回选中的 Chip 的 ID,如果没有选中则返回 View.NO_ID if (checkedChipId != View.NO_ID) { val selectedChip = chipGroup.findViewById<Chip>(checkedChipId) // ... 处理选中项 }重要提示:app:singleSelection="true"只对ChipGroup内的 Chip 有效。如果你有多个ChipGroup,它们之间的选择是独立的。app:selectionRequired="false"允许用户取消所有选择(例如,再次点击已选中的 Chip 会取消选中),这在某些场景下是需要的。
4.3 动态管理 ChipGroup 中的 Chip
在实际开发中,Chip 的数据往往来自网络或数据库,需要动态增删。
动态添加 Chip:
fun addChipsToGroup(tags: List<String>, chipGroup: ChipGroup) { chipGroup.removeAllViews() // 清空现有 Chip tags.forEach { tagName -> val chip = Chip(chipGroup.context).apply { layoutParams = ChipGroup.LayoutParams( ChipGroup.LayoutParams.WRAP_CONTENT, ChipGroup.LayoutParams.WRAP_CONTENT ) text = tagName style = R.style.Widget_MaterialComponents_Chip_Filter // 通过资源 ID 设置样式 isCheckable = true // 设置其他属性... } chip.setOnCheckedChangeListener { buttonView, isChecked -> // 处理单个 Chip 的选中变化 } chipGroup.addView(chip) } }动态移除 Chip: 对于 Input Chip,用户点击关闭图标时,你需要从ChipGroup和你的数据源中移除它。
chip.setOnCloseIconClickListener { chipGroup.removeView(it) // 从视图层级移除 dataList.remove(tagAssociatedWithChip) // 从数据模型移除 // 可选:通知适配器或更新 UI }4.4 常见问题与性能考量
Chip 数量过多:如果一个
ChipGroup内可能有成百上千个 Chip(比如所有用户标签),一次性创建所有视图会导致性能问题(内存和布局计算)。解决方案是使用RecyclerView配合FlexboxLayoutManager(来自com.google.android.flexbox:flexbox库)来替代ChipGroup,实现 Chip 的流式布局和视图复用。// 在 RecyclerView 中使用 FlexboxLayoutManager val layoutManager = FlexboxLayoutManager(context) layoutManager.flexDirection = FlexDirection.ROW layoutManager.flexWrap = FlexWrap.WRAP recyclerView.layoutManager = layoutManager recyclerView.adapter = MyChipAdapter(tagList)在 Adapter 中,每个 Item 都是一个
Chip。Chip 状态保存:在配置变更(如屏幕旋转)或页面重建时,
ChipGroup中 Chip 的选中状态需要保存。如果 Chip 是静态写在 XML 中的,并且设置了android:id,系统会自动保存其checked状态。如果是动态添加的,你需要自己在onSaveInstanceState和onRestoreInstanceState中手动保存和恢复选中 Chip 的 ID 或位置信息。与 EditText 结合实现“输入型 Chip”:常见的模式是在
EditText上方或下方放置一个ChipGroup,用于显示已输入的标签。当用户在EditText中输入内容并按下逗号、回车或特定按钮时,动态创建一个 Input Chip 添加到ChipGroup中,并清空EditText。这需要处理文本输入监听和 Chip 的动态添加逻辑。ChipGroup 内嵌其他视图:虽然
ChipGroup设计用来放Chip,但它本质上是一个ViewGroup。理论上你可以放其他视图进去,但这可能会破坏其布局逻辑和单选管理功能,不推荐这样做。
5. 实战:构建一个完整的标签筛选与管理系统
让我们结合SwitchMaterial、Chip和ChipGroup,构建一个模拟电商商品筛选侧边栏的完整示例。这个示例将包含:
- 一个全局开关(
SwitchMaterial),控制是否只显示有货商品。 - 一个品牌单选选择区(
ChipGroup+Choice Chip)。 - 一个标签多选筛选区(
ChipGroup+Filter Chip)。 - 一个已选标签展示区(
ChipGroup+Input Chip),展示已选的多选标签并可删除。
1. 布局文件 (activity_filter_demo.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" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="16dp"> <!-- 1. 有货开关 --> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="库存筛选" android:textAppearance="?attr/textAppearanceSubtitle1" /> <com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/switch_in_stock" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="仅显示有货" app:useMaterialThemeColors="true" /> <View style="@style/Divider" /> <!-- 2. 品牌单选 --> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="品牌" android:textAppearance="?attr/textAppearanceSubtitle1" /> <com.google.android.material.chip.ChipGroup android:id="@+id/chip_group_brand" android:layout_width="match_parent" android:layout_height="wrap_content" app:singleSelection="true" app:selectionRequired="true"> <!-- Chip 可以通过代码动态添加,这里写死示例 --> <com.google.android.material.chip.Chip style="@style/Widget.MaterialComponents.Chip.Choice" ... android:text="品牌A" /> <com.google.android.material.chip.Chip style="@style/Widget.MaterialComponents.Chip.Choice" ... android:text="品牌B" /> <com.google.android.material.chip.Chip style="@style/Widget.MaterialComponents.Chip.Choice" ... android:text="品牌C" /> </com.google.android.material.chip.ChipGroup> <View style="@style/Divider" /> <!-- 3. 标签多选 --> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="商品标签" android:textAppearance="?attr/textAppearanceSubtitle1" /> <com.google.android.material.chip.ChipGroup android:id="@+id/chip_group_all_tags" android:layout_width="match_parent" android:layout_height="wrap_content" app:singleSelection="false"> <com.google.android.material.chip.Chip style="@style/Widget.MaterialComponents.Chip.Filter" ... android:text="包邮" /> <com.google.android.material.chip.Chip style="@style/Widget.MaterialComponents.Chip.Filter" ... android:text="新品" /> <com.google.android.material.chip.Chip style="@style/Widget.MaterialComponents.Chip.Filter" ... android:text="促销" /> <com.google.android.material.chip.Chip style="@style/Widget.MaterialComponents.Chip.Filter" ... android:text="耐用" /> </com.google.android.material.chip.ChipGroup> <View style="@style/Divider" /> <!-- 4. 已选标签展示区 --> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="已选标签" android:textAppearance="?attr/textAppearanceSubtitle1" /> <com.google.android.material.chip.ChipGroup android:id="@+id/chip_group_selected_tags" android:layout_width="match_parent" android:layout_height="wrap_content" app:singleSelection="false" /> <!-- 这里初始为空,将通过代码动态添加 Input Chip --> <Button android:id="@+id/btn_apply_filter" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:text="应用筛选" /> </LinearLayout>2. 在 Activity/Fragment 中的逻辑处理:
class FilterDemoActivity : AppCompatActivity() { private lateinit var binding: ActivityFilterDemoBinding private val selectedFilterTags = mutableSetOf<String>() // 存储已选的多选标签 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityFilterDemoBinding.inflate(layoutInflater) setContentView(binding.root) // 1. 监听有货开关 binding.switchInStock.setOnCheckedChangeListener { _, isChecked -> // 这里可以实时响应,也可以等点击“应用”按钮再统一处理 Log.d("Filter", "仅显示有货: $isChecked") } // 2. 监听品牌单选组 binding.chipGroupBrand.setOnCheckedStateChangeListener { group, checkedIds -> checkedIds.firstOrNull()?.let { checkedId -> val selectedChip = group.findViewById<Chip>(checkedId) Log.d("Filter", "选中品牌: ${selectedChip.text}") } } // 3. 监听标签多选组,并同步到已选展示区 // 假设所有 Filter Chip 已经预先在 XML 中定义好了 // 我们需要为每一个 Chip 设置监听器 for (i in 0 until binding.chipGroupAllTags.childCount) { val chip = binding.chipGroupAllTags.getChildAt(i) as Chip chip.setOnCheckedChangeListener { buttonView, isChecked -> val tagText = buttonView.text.toString() if (isChecked) { selectedFilterTags.add(tagText) addSelectedTagChip(tagText) // 添加到展示区 } else { selectedFilterTags.remove(tagText) removeSelectedTagChip(tagText) // 从展示区移除 } } } // 4. 应用筛选按钮 binding.btnApplyFilter.setOnClickListener { applyAllFilters() } } private fun addSelectedTagChip(tag: String) { // 避免重复添加 if (binding.chipGroupSelectedTags.findViewWithTag<View>(tag) != null) return val inputChip = Chip(this).apply { text = tag style = R.style.Widget_MaterialComponents_Chip_Input isCloseIconVisible = true tag = tag // 设置 Tag,方便后续查找移除 } inputChip.setOnCloseIconClickListener { // 点击删除图标时,不仅要移除这个 Input Chip,还要取消对应 Filter Chip 的选中状态 binding.chipGroupSelectedTags.removeView(it) selectedFilterTags.remove(tag) // 找到对应的 Filter Chip 并取消选中 for (i in 0 until binding.chipGroupAllTags.childCount) { val filterChip = binding.chipGroupAllTags.getChildAt(i) as Chip if (filterChip.text.toString() == tag) { filterChip.isChecked = false break } } } binding.chipGroupSelectedTags.addView(inputChip) } private fun removeSelectedTagChip(tag: String) { val chipToRemove = binding.chipGroupSelectedTags.findViewWithTag<Chip>(tag) chipToRemove?.let { binding.chipGroupSelectedTags.removeView(it) } } private fun applyAllFilters() { val inStockOnly = binding.switchInStock.isChecked val selectedBrandId = binding.chipGroupBrand.checkedChipId val selectedBrand = if (selectedBrandId != View.NO_ID) { (binding.chipGroupBrand.findViewById<Chip>(selectedBrandId)).text.toString() } else { null } val selectedTags = selectedFilterTags.toList() // 构建筛选参数对象,传递给 ViewModel 或发起网络请求 val filterParams = FilterParams(inStockOnly, selectedBrand, selectedTags) Log.d("Filter", "应用筛选: $filterParams") // viewModel.applyFilters(filterParams) // 或者 startActivity(ResultActivity.newIntent(filterParams)) } } data class FilterParams( val inStockOnly: Boolean, val brand: String?, val tags: List<String> )这个实战示例涵盖了从布局到交互的完整链路,展示了如何将这三个组件有机结合起来,构建一个功能清晰、交互流畅的筛选界面。关键点在于理解不同 Chip 类型的语义,并妥善管理它们之间的状态同步(例如,Filter Chip 的选中状态与 Input Chip 的增删联动)。
