Checkbox 复选框组件完全指南:多选管理、全选反选与协议确认

本文基于 HarmonyOS(ArkTS 声明式开发范式,API 12 / 5.0.0)写作,所有示例均可在 DevEco Studio 模拟器中验证。配套演示工程位于本文同级目录 ohos/,包含完整可运行的 EntryAbility.etsIndex.ets


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、引言

注册页底部那行小字,购物车里"全选"按钮旁边的方块,设置页里"接收推荐"前面的勾——这些看似不起眼的方框,承担着两种截然不同的职责:一种是多选,从一组候选中挑出任意多个,勾了还能反悔;另一种是承诺,点击"同意"往往意味着法律意义上的认可。它们有一个共同的名字:Checkbox 复选框。

复选框与单选框是一对天生的对照。单选框回答"二选一",复选框回答"可多可少"——Radio 的点击逻辑是"从未选到选中,选过的不能再点掉",Checkbox 则是"选中与取消双向切换"。这一条逻辑差异决定了它们的使用场景:凡是"可以不选"或"可以多选"的地方,都是复选框的领地。兴趣爱好、购物车结算、筛选条件、消息订阅……以及那条绕不开的"我已阅读并同意《用户协议》"。

复选框的难点从来不在"怎么勾",而在"状态怎么管"。一个全选复选框面对一列子项时,会出现三种状态:全部勾选、部分勾选、无一勾选——"部分勾选"正是多选管理里最容易出错的三角地带。ArkUI 为此提供了 CheckboxGroup 容器:组内复选框的变化统一收口成 CheckboxGroupResult,其中 status 字段(All/Part/None)直接告诉你整组处于什么状态,免去了手工统计的麻烦。但组件只能帮你报告状态,状态从哪来、由谁持有,仍然是业务代码的责任。


二、环境准备

Checkbox 属于 ArkUI 基础组件,API 8 起提供,CheckboxGroupCheckboxGroupResult 同步可用;本文用到的能力在 API 12 上全部稳定。

项目 推荐配置 说明
DevEco Studio 5.0 及以上 需支持 API 12 的 SDK
HarmonyOS SDK 5.0.0(12) compatibleSdkVersion 与之对应
设备 Phone 模拟器或真机 本文以模拟器验证为主
工程类型 Stage 模型 + ArkTS EntryAbility 继承 UIAbility

工程落地路径与前几篇一致,两种方式任选:

  • 方式一:在 DevEco Studio 新建 Empty Ability 工程,直接写 ArkTS 原生页面。本文演示工程即采用这种方式(目录结构见 ohos/README.md)。
  • 方式二:在已有的 Flutter·鸿蒙壳工程里,把 ohos/entry/src/main/ets/ 下的页面与组件放进原生工程。这种方式下 EntryAbility 通常继承自 FlutterAbility,演示组件的代码不受影响。

若你用方式二(Flutter 壳),只需关注 pages/Index.etsmodel/CheckboxModel.etscomponents/ 下的组件代码,其余配置沿用原工程即可。


三、核心 API 与原理解析

3.1 构造与基本属性:name、select 与 selectedColor

Checkbox 的构造参数是"身份",链式属性是"表现":

Checkbox({ name: 'agree', group: 'agreementGroup' })
  .select(true)                 // 选中状态(受控)
  .selectedColor('#0A59F7')     // 选中时的填充色
  .onChange((isChecked: boolean) => {
    this.agree = isChecked;
  })
参数/属性 类型 作用
name string 选项名,组事件里用于识别是谁被勾选
group string 所属组名,与 CheckboxGroup 的组名对应
select boolean 选中状态,受控时由状态驱动
selectedColor ResourceColor 选中填充色(未选中是空心框)
onChange (boolean) => void 变化回调,参数就是新的选中值

两个关键点:其一,onChange 的参数是 boolean 而非 name——想知道"哪个复选框变了",要么按位置捕获(ForEach 的 index),要么用受控写法把 select 绑到数据上;其二,select 是受控入口,同时把 selectedColor 主动定下来——默认主题色在不同深色模式下观感可能不一致,UI 一致性要求主动配色。

3.2 CheckboxGroup:组事件的统一收口

RadioGroup 类似,CheckboxGroup 把整组变化收口成一次回调,但回调参数丰富得多:

CheckboxGroup({ group: 'interestGroup' }) {
  Checkbox({ name: 'tech', group: 'interestGroup' })
  Checkbox({ name: 'music', group: 'interestGroup' })
}
.onChange((result: CheckboxGroupResult) => {
  // result.name / result.value:选中的名称列表
  // result.status:SelectStatus.All / Part / None
})

CheckboxGroupResult 的三个字段:

字段 类型 说明
name string[] 选中的 name 列表
value string[] 选中的 value 列表
status SelectStatus 整组状态:All / Part / None

status 是"部分选中"的直接答案:一组 5 项,勾了 2 项,回调里 status === SelectStatus.Part。它省掉的是手工统计,但三态如何呈现在 UI 上仍是你的工作——ArkUI 的原生 Checkbox 没有"半选"图形(无 indeterminate 属性),“部分选中"通常用文字或徽标表达(如"已选 2/5”)。

3.3 全选联动的数据流

全选复选框与列表子项之间是典型的"投影"关系——列表数据是唯一真相源,全选框只是它的投影

勾选全选复选框

把全部选项写入数据数组

每个 Checkbox 的 select 由数据驱动

数据变化触发 UI 重绘

统计函数重算已选数与金额

全选框状态由统计结果推导

勾选单个子项

onChange 更新数据数组中对应项

两条规则贯穿始终:写入路径——任何勾选动作最终都落在数据数组上,UI 只是读取;读取路径——全选框的 select、已选计数、合计金额,全部由数据推导,不另存一份"视图状态"。这样无论用户从哪条路径操作(点全选、点子项、点反选),界面永远一致。这也是 4.5 购物车场景的骨架。

3.4 状态空间:为什么复选框是"组合"的

单选与多选的状态空间差异可以用组合数学精确刻画:n 个复选框的任意勾选组合是一种状态,其总数为:

[
\sum_{k=0}^{n} \binom{n}{k} = 2^{n}
]

n 项里恰好勾 k 项的方案数是 (\binom{n}{k}),全部状态加起来是 (2^n)。对照 Radio 组的 n 种互斥状态,复选框的状态空间是指数级的——这正是"状态管理"在多选场景里比单选复杂得多的数学根源。也正因如此,用单一数据数组承载状态、用函数推导派生值(而非为每个复选框各存一个 @State)才是可维护的做法。

3.5 协议确认:勾选是门禁,点击是阅读

协议场景有两个动作必须分离:勾选复选框是"我同意"(承诺),点击协议文字是"我要看"(阅读)。常见错误是把两者绑在一个 onClick 上——点文字自动勾选,等于"同意前没看过协议"。正确写法:

Checkbox({ name: 'agree' })
  .select(this.agree)
  .onChange((isChecked: boolean) => {
    this.agree = isChecked;
  })

Text('《用户协议》')
  .fontColor('#0A59F7')
  .textDecoration(TextDecoration.Underline)
  .onClick(() => {
    this.showProtocol = !this.showProtocol;   // 只展开内容,不动勾选
  })

Button('注册')
  .enabled(this.agree)                         // 未勾选则禁用

"阅读"与"同意"分离后,注册按钮用 enabled(this.agree)门禁——未勾选置灰,勾选才可点。这是协议场景的行业惯例,也是无障碍的底线。

3.6 与相关组件的选型对比

组件 可多选 可取消 适用场景
Checkbox 多选、全选反选、协议勾选
Radio 必选其一(性别、支付方式)
Toggle - 二态开关(通知、夜间模式)
CheckboxGroup 整组事件收口与三态统计

选型一句话:"可多可少"用 Checkbox,"必选其一"用 Radio,"开或关"用 Toggle——016 的单选框与本章的复选框是互斥 vs 可多选的分工,两条路线的状态管理范式也一脉相承。


四、完整代码实现

下面给出演示工程的完整可运行代码。工程以 Tabs 组织三个模块:多选设置(核心场景)、商品全选、协议确认。数据模型 CheckboxModel.ets 提供兴趣与商品数据。

4.1 入口:EntryAbility.ets

import { UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  private readonly TAG: string = 'CheckboxGuideAbility';

  onCreate(want: object, launchParam: object): void {
    hilog.info(0x0000, this.TAG, '%{public}s', 'Ability onCreate');
  }

onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(0x0000, this.TAG, 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
        return;
      }
      hilog.info(0x0000, this.TAG, '%{public}s', 'Succeeded in loading the content.');
    });
  }

  // onForeground / onBackground / onDestroy / onWindowStageDestroy 生命周期
  // 回调仅打 hilog 日志,完整版见演示工程源码
}

4.2 数据模型:CheckboxModel.ets

export class InterestItem {
  name: string;
  label: string;

  constructor(name: string, label: string) {
    this.name = name;
    this.label = label;
  }
}

export const INTERESTS: InterestItem[] = [
  new InterestItem('tech', '技术'),
  new InterestItem('music', '音乐'),
  new InterestItem('movie', '电影'),
  new InterestItem('sport', '运动'),
  new InterestItem('travel', '旅行'),
];

export class GoodsItem {
  name: string;
  price: number;
  checked: boolean;

  constructor(name: string, price: number, checked: boolean = false) {
    this.name = name;
    this.price = price;
    this.checked = checked;
  }
}

// GOODS 常量:4 件商品(鸿蒙开发入门 69 元、蓝牙降噪耳机 399 元、
// 智能手环 249 元、快充充电器 129 元),完整定义见演示工程

4.3 主页面:Index.ets

import { MultiSelectDemo } from '../components/MultiSelectDemo';
import { SelectAllDemo } from '../components/SelectAllDemo';
import { AgreementDemo } from '../components/AgreementDemo';

@Entry
@Component
struct Index {
  @State currentIndex: number = 0;

  build() {
    Column() {
      Tabs({ barPosition: BarPosition.Start, index: this.currentIndex }) {
        TabContent() { MultiSelectDemo() }.tabBar('多选设置')
        TabContent() { SelectAllDemo() }.tabBar('商品全选')
        TabContent() { AgreementDemo() }.tabBar('协议确认')
      }
      .vertical(false)
      .scrollable(true)
      .barMode(BarMode.Scrollable)
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
  }
}
```### 4.4 核心场景一:多选设置(MultiSelectDemo.ets)

兴趣组演示了"受控复选框 + CheckboxGroup 三态"的完整闭环:`selected` 数组是唯一真相源,每个 Checkbox 的 `select` 由它驱动,组事件的 `onChange` 反哺它,全选/反选按钮改写它,组状态与已选计数由它推导。

```typescript
import { promptAction } from '@kit.ArkUI';
import { InterestItem, INTERESTS } from '../model/CheckboxModel';

@Component
export struct MultiSelectDemo {
  @State remember: boolean = true;
  @State selected: string[] = [];
  @State groupStatus: string = 'None';

  build() {
    Scroll() {
      Column({ space: 16 }) {
        Text('多选设置')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .width('92%')
          .textAlign(TextAlign.Start)

        Row({ space: 12 }) {
          Checkbox({ name: 'remember' })
            .select(this.remember)
            .selectedColor('#0A59F7')
            .onChange((isChecked: boolean) => {
              this.remember = isChecked;
            })
          Text('记住我的选择偏好').fontSize(15).fontColor('#333333')
          Blank()
        }
        .width('92%')
        .padding(14)
        .borderRadius(12)
        .backgroundColor(Color.White)
        .border({ width: 1, color: '#F0F0F0' })

        Row({ space: 12 }) {
          Text('选择你的兴趣爱好')
            .fontSize(15)
            .fontWeight(FontWeight.Medium)
            .fontColor('#333333')
          Blank()
          Text(`已选 ${this.selected.length}/${INTERESTS.length}`)
            .fontSize(13)
            .fontColor('#0A59F7')
        }
        .width('92%')

        CheckboxGroup({ group: 'interestGroup' }) {
          Column({ space: 0 }) {
            ForEach(INTERESTS, (item: InterestItem, index: number) => {
              Row({ space: 12 }) {
                Checkbox({ name: item.name, group: 'interestGroup' })
                  .select(this.isSelected(item.name))
                  .selectedColor('#0A59F7')
Text(item.label).fontSize(15).fontColor('#333333')
                Blank()
              }
              .width('100%')
              .height(52)
              .padding({ left: 16, right: 16 })
              if (index < INTERESTS.length - 1) {
                Divider().strokeWidth(1).color('#F2F2F2').margin({ left: 12 })
              }
            }, (item: InterestItem) => item.name)
          }
          .width('100%')
        }
        .width('92%')
        .borderRadius(14)
        .backgroundColor(Color.White)
        .border({ width: 1, color: '#F0F0F0' })
        .onChange((result: CheckboxGroupResult) => {
          this.selected = result.value;
          this.groupStatus = this.statusName(result.status);
        })

        Row({ space: 10 }) {
Button('全选')
            .layoutWeight(1).height(40).fontSize(14)
            .backgroundColor('#0A59F7')
            .onClick(() => { this.selectAll(true); })
          Button('反选')
            .layoutWeight(1).height(40).fontSize(14)
            .backgroundColor('#07C160')
            .onClick(() => { this.invertAll(); })
        }
        .width('92%')

Row({ space: 12 }) {
          Text(`组状态:${this.groupStatus}`)
            .fontSize(14)
            .fontColor(this.statusColor())
          Blank()
          Text(`已选 ${this.selected.length}`)
            .fontSize(13)
            .fontColor('#666666')
        }
        .width('92%')
        .padding(14)
        .borderRadius(12)
        .backgroundColor('#F7F9FF')

Button('提交')
          .width('92%').height(44).borderRadius(22).fontSize(16)
          .onClick(() => {
            promptAction.showToast({ message: `已选择 ${this.selected.length}`, duration: 1500 });
          })
      }
      .width('100%')
      .padding({ top: 16, bottom: 24 })
    }
    .width('100%')
    .height('100%')
  }

// 其余工具方法见演示工程源码:
  // isSelected/selectAll/invertAll/statusName/statusNameFromCount/statusColor
}

值得注意:按钮全选/反选直接改写 selected 数组并自行推导 groupStatus——因为 CheckboxGroup.onChange 只在用户操作组内复选框时触发,程序化改数组不会回调它。两套入口最终都收敛到同一个 selected,界面自然一致。

4.5 场景二:商品全选(SelectAllDemo.ets)

购物车演示"行数据为源、全选为投影"的另一种组织方式——这里不使用 CheckboxGroup,而是把选中状态直接放进 GoodsItem.checked,通过统计函数推导全选三态与合计金额:

import { promptAction } from '@kit.ArkUI';
import { GoodsItem, GOODS } from '../model/CheckboxModel';

@Component
export struct SelectAllDemo {
  @State goods: GoodsItem[] = [];

  aboutToAppear(): void {
    this.goods = GOODS.map((item: GoodsItem) => {
      return new GoodsItem(item.name, item.price, item.checked);
    });
  }

  build() {
    Column({ space: 16 }) {
      Text('购物车').fontSize(16).fontWeight(FontWeight.Bold)
        .width('92%').textAlign(TextAlign.Start)

      Row({ space: 12 }) {
        Checkbox({ name: 'all' })
          .select(this.allChecked())
          .selectedColor('#0A59F7')
          .onChange((isChecked: boolean) => {
            this.setAll(isChecked);
          })
        Text('全选').fontSize(15).fontColor('#333333')
        Blank()
        Text(this.partialText()).fontSize(13).fontColor(this.partialColor())
      }
      .width('92%').padding(14).borderRadius(12)
      .backgroundColor(Color.White).border({ width: 1, color: '#F0F0F0' })

      Column({ space: 0 }) {
        ForEach(this.goods, (item: GoodsItem, index: number) => {
          Row({ space: 12 }) {
            Checkbox({ name: `goods_${index}` })
              .select(item.checked)
              .selectedColor('#0A59F7')
              .onChange((isChecked: boolean) => {
                this.toggleItem(index, isChecked);
              })
            Text(item.name)
              .fontSize(15)
              .fontColor(item.checked ? '#333333' : '#999999')
              .textDecoration(item.checked ? TextDecoration.LineThrough : TextDecoration.None)
            Blank()
            Text(`¥${item.price}`)
              .fontSize(15).fontWeight(FontWeight.Bold).fontColor('#E84026')
          }
          .width('100%').height(56).padding({ left: 16, right: 16 })
          if (index < this.goods.length - 1) {
            Divider().strokeWidth(1).color('#F2F2F2').margin({ left: 12 })
          }
        }, (item: GoodsItem, index: number) => item.name + index.toString())
      }
      .width('92%').borderRadius(14)
      .backgroundColor(Color.White).border({ width: 1, color: '#F0F0F0' })

      Row({ space: 12 }) {
        Button('反选')
          .layoutWeight(1).height(44).fontSize(15)
          .backgroundColor('#07C160')
          .onClick(() => { this.invertAll(); })
        Button(`结算(¥${this.totalPrice()}`)
          .layoutWeight(2).height(44).fontSize(15)
          .backgroundColor('#E84026')
          .onClick(() => {
            if (this.checkedCount() === 0) {
              promptAction.showToast({ message: '请先勾选商品', duration: 1200 });
              return;
            }
            promptAction.showToast({ message: `结算金额:¥${this.totalPrice()}`, duration: 1500 });
          })
      }
      .width('92%')
    }
    .width('100%')
    .padding({ top: 16 })
  }

allChecked(): boolean {
    return this.checkedCount() === this.goods.length && this.goods.length > 0;
  }

  checkedCount(): number {
    // 统计 checked === true 的个数
    return this.goods.filter((item: GoodsItem) => item.checked).length;
  }

  totalPrice(): number {
    // 勾选商品单价求和
    return this.goods.filter((item: GoodsItem) => item.checked)
      .reduce((sum: number, item: GoodsItem) => sum + item.price, 0);
  }

  // partialText / partialColor:三态文案与颜色(未选灰/部分橙/全选绿),见演示工程源码

  setAll(value: boolean): void {
    // 用 map 生成新数组,保证 @State 引用比较能感知变化
    this.goods = this.goods.map((item: GoodsItem) => {
      return new GoodsItem(item.name, item.price, value);
    });
  }

  toggleItem(index: number, value: boolean): void {
    this.goods = this.goods.map((item: GoodsItem, i: number) => {
      if (i === index) {
        return new GoodsItem(item.name, item.price, value);
      }
      return item;
    });
  }

  invertAll(): void {
    this.goods = this.goods.map((item: GoodsItem) => {
      return new GoodsItem(item.name, item.price, !item.checked);
    });
  }
}

一个 ArkTS 陷阱值得点名:@State goods 持有对象数组时,直接改元素的 checked 字段不会触发重绘——@State 做的是引用比较。所以每次修改都用 map 生成新对象数组(toggleItem/setAll/invertAll),既保持数据不可变,又保证状态更新可靠。

4.6 场景三:协议确认(AgreementDemo.ets)

协议页演示"勾选是门禁、点击是阅读"的分离设计,以及 enabled 对主按钮的守门:

import { promptAction } from '@kit.ArkUI';

@Component
export struct AgreementDemo {
  @State agree: boolean = false;
  @State showProtocol: boolean = false;

  build() {
    Column({ space: 16 }) {
      Text('注册账号').fontSize(16).fontWeight(FontWeight.Bold)
        .width('92%').textAlign(TextAlign.Start)

// …… 账号/密码输入等表单占位行(此处省略,完整代码见演示工程)……

      Row({ space: 12 }) {
        Checkbox({ name: 'agree' })
          .select(this.agree)
          .selectedColor('#0A59F7')
          .onChange((isChecked: boolean) => {
            this.agree = isChecked;
          })
        Text('我已阅读并同意').fontSize(14).fontColor('#666666')
        Text('《用户协议》')
          .fontSize(14).fontColor('#0A59F7')
          .textDecoration(TextDecoration.Underline)
          .onClick(() => {
            this.showProtocol = !this.showProtocol;
          })
        Text('和').fontSize(14).fontColor('#666666')
        Text('《隐私政策》')
          .fontSize(14).fontColor('#0A59F7')
          .textDecoration(TextDecoration.Underline)
          .onClick(() => {
            promptAction.showToast({ message: '已打开隐私政策', duration: 1200 });
          })
        Blank()
      }
      .width('92%')
      .alignItems(VerticalAlign.Center)

if (this.showProtocol) {
        Scroll() {
          Text('《用户协议》摘要:服务内容、账号规范、隐私保护、知识产权、'
            + '协议更新(重大变更提前 7 日通知)。(演示用摘要,以官方文本为准)')
            .fontSize(13).fontColor('#555555').lineHeight(22).width('100%')
        }
        .width('92%').height(160).padding(14)
        .borderRadius(12).backgroundColor('#F7F9FF')
      }

      Button('注册')
        .width('92%').height(44).borderRadius(22).fontSize(16)
        .enabled(this.agree)
        .backgroundColor(this.agree ? '#0A59F7' : '#C9D8F5')
.onClick(() => {
          promptAction.showToast({ message: '注册成功', duration: 1500 });
        })
    }
    .width('100%')
    .padding({ top: 16 })
  }
}

协议展开区是条件渲染而非覆盖层:showProtocol 为 true 时插入带边框的文本区,展开/收起不打断勾选状态;《隐私政策》用 Toast 示意详情入口。注册按钮未勾选时呈浅灰禁用,勾选后点亮品牌色。这里略去了账号/密码占位行与"保持登录状态"复选框,完整代码见演示工程。


五、模拟器运行与效果展示

启动模拟器,点击 Run 部署演示工程,依次切换三个 Tab 验证:

  • 多选设置 Tab:单个复选框"记住偏好"默认勾选;兴趣组逐个勾选时,"已选 x/5"实时变化,组状态徽标在 未选择(灰)/部分选中(橙)/全部选中(绿) 间切换;全选/反选按钮联动整组。
  • 商品全选 Tab:勾选商品行出现删除线与橙色金额;"全选"复选框随行数据实时推导;反选按钮一步翻转;结算按钮金额随勾选即时刷新。
  • 协议确认 Tab:未勾选时注册按钮置灰;点击"《用户协议》"文字展开摘要(不改变勾选);勾选后按钮点亮。

截图占位(模拟器实机拍摄后替换):

占位图 场景
screenshot_01_initial.png 初始状态:兴趣组未勾选、组状态为 None、注册按钮置灰
screenshot_02_partial.png 部分选中:已勾选 2 项,组状态为 Part(橙色"已选 2/5")
screenshot_03_all.png 全选状态:5 项全勾,组状态为 All(绿色"全部选中")

六、调试与常见问题

现象 原因 解决
点击复选框 UI 不变 忘了把 select 绑到数据,或数据是局部变量 受控写法:select(this.isSelected(x))
onChange 拿不到"谁被勾了" 回调参数是 boolean 不是 name 用 index 捕获,或用组回调的 result.name
全选后组状态仍是 Part 程序化赋值不触发组回调 赋值路径自行推导状态,不把 UI 状态当唯一来源
改了 item.checked 不刷新 @State 是引用比较 map 生成新对象数组再赋值
全选框没有"半选"图形 原生无 indeterminate 用文字/徽标表达部分选中
点协议文字自动勾选 阅读与同意绑在一个 onClick 拆开:文字只展开内容,勾选只写 agree
注册按钮点击无反馈 协议门禁未生效 enabled(this.agree) + 禁用色

调试技巧:状态链路肉眼难查时,可以在 CheckboxGroup.onChangehilog 打印 result.nameresult.status,对照 3.3 流程图逐条核对"点击 → 数据 → 推导 → UI"四步。另注意 group 参数是字符串匹配,拼写不一致会静默失效(不报错)。


七、总结与扩展

状态单一来源
选中数据(数组或行字段)是唯一真相源,全选框与统计都由它推导,任何操作路径最终都写入同一份数据。
三态用统计表达
ArkUI 复选框没有原生半选图形,用"已选 x/n"与 All/Part/None 徽标表达部分选中。
点击与勾选分离
协议文本的阅读点击不改变勾选,勾选是承诺、点击是阅读,注册按钮用 enabled 做门禁。
多选即可多可少
可多选、可取消的场景归 Checkbox,必选其一的场景归 Radio,二态开关归 Toggle。
复选框的本质不是"勾选一个方块",而是"维护一份名单"——把名单管好,界面自然一致。

扩展方向:① 购物车多级全选(分类全选 → 全局全选,仍是一条统计链);② 权限申请多选(一次勾选多个权限,每项附说明文字);③ 与 ListItem 组合做滑动批量选择;④ 深色模式为 selectedColor 配置资源色;⑤ 无障碍:协议文本独立可聚焦朗读,勾选控件补语义标签。下一篇将进入日期时间选择器家族(TimePicker)。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐