Toggle 开关和 Slider 滑块是移动端最基础也最高频的交互组件——从设置页面的选项开关到音乐播放器的音量调节,从空调温度选择到屏幕亮度控制。ArkUI 对这两个组件做了深度优化:Toggle 支持多种开关样式和无障碍适配,Slider 支持范围、步长、方向、颜色全维度自定义。本文用一个"智能家居控制面板"Demo,展示 Toggle 和 Slider 在实际项目中的组合使用模式。


一、Toggle 组件

1.1 三种开关类型

Toggle 支持三种视觉样式,通过 type 参数切换:

// 单选复选框 — 方形勾选框
Toggle({ type: ToggleType.Checkbox, isOn: true })

// 开关按钮 — iOS 风格的滑动开关(默认)
Toggle({ type: ToggleType.Switch, isOn: true })

// 单选按钮 — 圆形选择钮
Toggle({ type: ToggleType.Button, isOn: true })

ToggleType.Switch 是最常用的类型,适用于开/关二元状态切换——灯、空调、插座的通断状态。Checkbox 适用于多选场景——如批量选择设备、同意协议。Button 适用于单选场景——如支付方式选择。

1.2 基本用法

@State isLightOn: boolean = true;

Toggle({ type: ToggleType.Switch, isOn: this.isLightOn })
  .selectedColor('#FF6B6B')
  .onChange((value: boolean) => {
    this.isLightOn = value;
    console.log(`灯已${value ? '打开' : '关闭'}`);
  })
  • isOn:当前开关状态,必填参数。为 true 时开关处于开启位置。
  • .selectedColor():开启状态下的颜色。默认是系统主题色,可自定义为品牌色或按功能区分(如灯用暖黄色,空调用冷蓝色)。
  • .onChange():用户滑动/点击开关时触发,参数 value 为新状态。

1.3 自定义样式

Toggle({ type: ToggleType.Switch, isOn: this.isOn })
  .selectedColor('#52C41A')          // 开启时滑道颜色(绿)
  .switchPointColor('#FFFFFF')       // 滑块颜色(白)
  .width(50)                          // 开关总宽度
  .height(28)                         // 开关总高度

switchPointColor 控制开关内部圆形滑块的颜色。selectedColor 控制滑道(滑块移动的轨道)在开启状态下的颜色。两者的默认对比度已为无障碍优化。

1.4 无障碍适配

Toggle 组件内置无障碍支持——屏幕阅读器会自动朗读开关状态(“灯开关,已开启”)。开发者不需要额外配置 accessibilityText,但可以通过 .id() 为关键开关添加标识,方便自动化测试定位。


在这里插入图片描述

二、Slider 组件

2.1 四种样式

Slider 支持四种视觉样式:

Slider({ style: SliderStyle.OutSet })   // 滑块在轨道外侧(默认)
Slider({ style: SliderStyle.InSet })    // 滑块在轨道内侧
Slider({ style: SliderStyle.None })     // 无滑块(仅进度展示)
Slider({ style: SliderStyle.OutSetHorizontal }) // 水平外侧样式

InSet 样式适合精细调节场景(如温度、湿度),视觉上更紧凑。OutSet 适合粗略调节场景(如音量、亮度),滑块更突出易操作。

2.2 基本用法

@State temperature: number = 24;

Slider({
  value: this.temperature,    // 当前值
  min: 16,                   // 最小值
  max: 30,                   // 最大值
  step: 1,                   // 步长
  style: SliderStyle.InSet
})
  .blockColor('#FF6B6B')     // 滑块颜色
  .trackColor('#FF6B6B20')   // 轨道背景色
  .selectedColor('#FF6B6B')  // 已选轨道颜色
  .trackThickness(6)         // 轨道厚度
  .onChange((value: number) => {
    this.temperature = value;
  })

关键参数说明:

  • value:当前值,必须在 minmax 之间
  • min / max:取值范围
  • step:步长。设为 1 表示整数值(适合温度),设为 0.1 表示精确调节(适合亮度百分比)
  • style:滑块样式,推荐 InSet 用于嵌入式设备控制
  • .trackThickness():轨道高度,默认约 4vp,增大到 6~8vp 可提升操作体验

2.3 颜色语义化设计

在智能家居场景中,不同设备类型的 Slider 应该使用不同颜色——帮助用户用直觉(而非文字)理解正在调节什么:

getSliderColor(device: Device): string {
  if (device.sliderUnit === '℃') return '#FF6B6B';       // 温度 → 红色
  if (device.name.includes('湿度')) return '#4ECDC4';      // 湿度 → 青色
  if (device.sliderUnit === '档') return '#FFD93D';       // 风速 → 金色
  return AppColors.PRIMARY;                               // 亮度 → 蓝色
}

颜色匹配:

  • 温度(空调/热水器):红色系 → 暖/热
  • 湿度(加湿器):青色 → 水/清爽
  • 风速(油烟机):金色 → 中等速度感
  • 亮度(灯):蓝色 → 默认主色

2.4 步长设计建议

调节对象 最小值 最大值 步长 原因
灯光亮度 10 100 10 人眼对 10% 亮度差有明显感知
空调温度 16 30 1 通常 1℃ 为最小感知单位
热水器温度 30 75 5 5℃ 一跳足够
加湿器湿度 30 80 5 5% 一跳,避免频繁调节
风扇速度 1 5 1 整数挡位(1档~5档)

步长太大 → 调节颗粒度不够。步长太小 → 用户需要滑动很多次,操作效率低。上面的建议基于真实家电产品的控制逻辑。

2.5 方向与交互

Slider 默认是水平的,但可以通过 .direction(Axis.Vertical) 变为垂直滑块——适合竖排布局的音量推子、灯光调光台等。

Slider({ value: this.volume, min: 0, max: 100, step: 1 })
  .direction(Axis.Vertical)
  .height(200)

用户交互方式:

  • 拖动滑块:手指按住滑块沿轨道滑动
  • 点击轨道:直接跳到点击位置
  • 键盘/遥控器:左右方向键调节(TV 场景)

在这里插入图片描述

三、Demo:智能家居控制面板

本 Demo 构建一个四房间智能家居系统——客厅、主卧、厨房、书房各有不同设备,Toggle 控制通断,Slider 调节参数。

页面结构

SmartHomePage (~240行)
├── Header(房间名 + 设备开启数 + 一键全开/全关)
├── 房间选择栏(🛋️客厅/🛏️主卧/🍳厨房/📚书房,横向滚动)
├── Scroll
│   └── 设备卡片 × N
│       ├── 左侧:设备图标(56×56 圆角方块,彩色背景)
│       ├── 中部:设备名 + 状态文字(已关闭 / 80% / 24℃)
│       ├── 右侧:Toggle 开关
│       └── 下方(开启时):Slider 参数调节条

数据模型

两个核心实体类:Room(房间)和 Device(设备)。

class Room {
  id: number;
  name: string;
  icon: string;
  color: string;
}

class Device {
  id: number;
  name: string;
  icon: string;
  roomId: number;        // 所属房间
  isOn: boolean;         // 开关状态
  hasSlider: boolean;    // 是否有调节参数
  sliderLabel: string;   // 参数名(亮度/温度/风速)
  sliderMin: number;
  sliderMax: number;
  sliderStep: number;
  sliderValue: number;   // 当前参数值
  sliderUnit: string;    // 单位(%/℃/档)
}

15 个设备分布在 4 个房间:

  • 客厅(5):主灯、空调、电视、窗帘、氛围灯带
  • 主卧(4):吸顶灯、空调、窗帘、床头灯
  • 厨房(3):顶灯、抽油烟机、热水器
  • 书房(4):台灯、空调、加湿器

Toggle 控制设备开关

每个设备卡片右侧嵌入了 Switch 类型的 Toggle,颜色匹配设备类型:

Toggle({ type: ToggleType.Switch, isOn: device.isOn })
  .selectedColor(this.getSliderColor(device))
  .onChange((value: boolean) => {
    this.toggleDevice(device.id);
  })

切换房间后,设备列表自动刷新——每张卡片的 Toggle 状态、Slider 值都从数据中读取。@State devices 的不可变更新模式(map 创建新对象)确保 UI 正确响应:

toggleDevice(deviceId: number): void {
  this.devices = this.devices.map((d: Device) => {
    if (d.id === deviceId) {
      return new Device(d.id, d.name, d.icon, d.roomId,
        !d.isOn, d.hasSlider, d.sliderLabel, d.sliderMin,
        d.sliderMax, d.sliderStep, d.sliderValue, d.sliderUnit);
    }
    return d;
  });
}

Slider 参数调节

设备开启时,卡片下方展开一个 Slider,显示参数名、当前值和滑块:

if (device.isOn && device.hasSlider) {
  Column() {
    Row() {
      Text(device.sliderLabel).fontSize(10).fontColor('#86909C')
      Blank()
      Text(`${device.sliderValue}${device.sliderUnit}`).fontSize(12)
        .fontColor(this.getSliderColor(device)).fontWeight(FontWeight.Bold)
    }
    .width('100%').margin({ bottom: 8 })

    Slider({
      value: device.sliderValue,
      min: device.sliderMin,
      max: device.sliderMax,
      step: device.sliderStep,
      style: SliderStyle.InSet
    })
      .blockColor(this.getSliderColor(device))
      .trackColor(this.getSliderColor(device) + '30')
      .selectedColor(this.getSliderColor(device))
      .trackThickness(6)
      .onChange((value: number) => {
        this.updateSlider(device.id, value);
      })
  }
}

Slider 的 onChange 实时更新设备参数值——温度、亮度、湿度、风速实时响应。

房间选择栏

四个房间用彩色卡片展示,选中态为填充色,非选中态为灰色:

Column() {
  Text(room.icon).fontSize(24)
  Text(room.name).fontSize(11)
  Text(`${this.getRoomOnCount(room.id)}/${this.getRoomTotalCount(room.id)}`)
    .fontSize(9)
}
.width(68).height(76)
.backgroundColor(this.selectedRoomId === room.id ? room.color : '#F5F6FA')

每个房间卡片显示"已开启/总设备数"统计(如"3/5"),用户一眼看出各房间的用电/运行状态。

一键全开/全关

Header 右上角"一键"按钮切换所有设备的开关状态:

toggleAllDevices(): void {
  this.toggleAll = !this.toggleAll;
  this.devices = this.devices.map((d: Device) => {
    return new Device(d.id, d.name, d.icon, d.roomId,
      this.toggleAll, d.hasSlider, d.sliderLabel, d.sliderMin,
      d.sliderMax, d.sliderStep, d.sliderValue, d.sliderUnit);
  });
}

按钮显示当前"全部开启"或"全部关闭"状态(toggleAll 为 true 时显示金色高亮)。

四个交互点

  1. 房间切换 — 点击四个房间卡片,设备列表切换,显示该房间所有设备
  2. 开关控制 — 每个设备卡片的 Toggle 开关,控制通断状态
  3. 参数调节 — 开启的设备显示 Slider,实时调节亮度/温度/湿度/风速
  4. 一键全开/全关 — 点击右上角"一键"按钮,全部设备同步切换

在这里插入图片描述

四、完整代码

import { AppColors, BorderRadius, FontSize, Spacing } from '../common/Constants';

class Device {
  id: number;
  name: string;
  icon: string;
  roomId: number;
  isOn: boolean;
  hasSlider: boolean;
  sliderLabel: string;
  sliderMin: number;
  sliderMax: number;
  sliderStep: number;
  sliderValue: number;
  sliderUnit: string;

  constructor(id: number, name: string, icon: string, roomId: number,
    isOn: boolean, hasSlider: boolean, sliderLabel: string, sliderMin: number,
    sliderMax: number, sliderStep: number, sliderValue: number,
    sliderUnit: string) {
    this.id = id;
    this.name = name;
    this.icon = icon;
    this.roomId = roomId;
    this.isOn = isOn;
    this.hasSlider = hasSlider;
    this.sliderLabel = sliderLabel;
    this.sliderMin = sliderMin;
    this.sliderMax = sliderMax;
    this.sliderStep = sliderStep;
    this.sliderValue = sliderValue;
    this.sliderUnit = sliderUnit;
  }
}

class Room {
  id: number;
  name: string;
  icon: string;
  color: string;

  constructor(id: number, name: string, icon: string, color: string) {
    this.id = id;
    this.name = name;
    this.icon = icon;
    this.color = color;
  }
}

const ROOMS: Room[] = [
  new Room(1, '客厅', '🛋️', '#1677FF'),
  new Room(2, '主卧', '🛏️', '#722ED1'),
  new Room(3, '厨房', '🍳', '#E67E22'),
  new Room(4, '书房', '📚', '#52C41A'),
];

const DEVICES: Device[] = [
  new Device(1, '主灯', '💡', 1, true, true, '亮度', 10, 100, 10, 80, '%'),
  new Device(2, '空调', '❄️', 1, true, true, '温度', 16, 30, 1, 24, '℃'),
  new Device(3, '电视', '📺', 1, false, false, '', 0, 0, 0, 0, ''),
  new Device(4, '窗帘', '🪟', 1, true, false, '', 0, 0, 0, 0, ''),
  new Device(5, '氛围灯带', '🌈', 1, true, true, '亮度', 10, 100, 10, 50, '%'),

  new Device(6, '吸顶灯', '💡', 2, true, true, '亮度', 10, 100, 10, 60, '%'),
  new Device(7, '空调', '❄️', 2, true, true, '温度', 16, 30, 1, 26, '℃'),
  new Device(8, '窗帘', '🪟', 2, false, false, '', 0, 0, 0, 0, ''),
  new Device(9, '床头灯', '🛋️', 2, false, true, '亮度', 10, 100, 10, 40, '%'),

  new Device(10, '顶灯', '💡', 3, true, true, '亮度', 10, 100, 10, 90, '%'),
  new Device(11, '抽油烟机', '🌀', 3, false, true, '风速', 1, 5, 1, 3, '档'),
  new Device(12, '热水器', '🔥', 3, true, true, '温度', 30, 75, 5, 55, '℃'),

  new Device(13, '台灯', '💡', 4, true, true, '亮度', 10, 100, 10, 70, '%'),
  new Device(14, '空调', '❄️', 4, false, true, '温度', 16, 30, 1, 25, '℃'),
  new Device(15, '加湿器', '💧', 4, true, true, '湿度', 30, 80, 5, 55, '%'),
];

@Entry
@Component
struct SmartHomePage {
  @State selectedRoomId: number = 1;
  @State devices: Device[] = [...DEVICES];
  @State toggleAll: boolean = true;

  getRoomDevices(): Device[] {
    return this.devices.filter((d: Device) => d.roomId === this.selectedRoomId);
  }

  getSelectedRoom(): Room {
    return ROOMS.find((r: Room) => r.id === this.selectedRoomId) || ROOMS[0];
  }

  getOnCount(): number {
    return this.getRoomDevices().filter((d: Device) => d.isOn).length;
  }

  getTotalCount(): number {
    return this.getRoomDevices().length;
  }

  getRoomOnCount(roomId: number): number {
    return this.devices.filter((d: Device) => d.roomId === roomId && d.isOn).length;
  }

  getRoomTotalCount(roomId: number): number {
    return this.devices.filter((d: Device) => d.roomId === roomId).length;
  }

  getDeviceStatusText(device: Device): string {
    if (!device.isOn) return '已关闭';
    if (device.hasSlider) {
      return `${device.sliderValue}${device.sliderUnit}`;
    }
    return '已开启';
  }

  toggleAllDevices(): void {
    this.toggleAll = !this.toggleAll;
    this.devices = this.devices.map((d: Device) => {
      return new Device(d.id, d.name, d.icon, d.roomId,
        this.toggleAll, d.hasSlider, d.sliderLabel, d.sliderMin,
        d.sliderMax, d.sliderStep, d.sliderValue, d.sliderUnit);
    });
  }

  toggleDevice(deviceId: number): void {
    this.devices = this.devices.map((d: Device) => {
      if (d.id === deviceId) {
        return new Device(d.id, d.name, d.icon, d.roomId,
          !d.isOn, d.hasSlider, d.sliderLabel, d.sliderMin,
          d.sliderMax, d.sliderStep, d.sliderValue, d.sliderUnit);
      }
      return d;
    });
  }

  updateSlider(deviceId: number, value: number): void {
    this.devices = this.devices.map((d: Device) => {
      if (d.id === deviceId) {
        return new Device(d.id, d.name, d.icon, d.roomId,
          d.isOn, d.hasSlider, d.sliderLabel, d.sliderMin,
          d.sliderMax, d.sliderStep, value, d.sliderUnit);
      }
      return d;
    });
  }

  getSliderColor(device: Device): string {
    if (device.sliderUnit === '℃') return '#FF6B6B';
    if (device.sliderUnit === '%' && device.name.includes('湿度')) return '#4ECDC4';
    if (device.sliderUnit === '档') return '#FFD93D';
    return AppColors.PRIMARY;
  }

  build() {
    Column() {
      Row() {
        Column() {
          Text(this.getSelectedRoom().name)
            .fontSize(FontSize.HEADLINE)
            .fontWeight(FontWeight.Bold)
            .fontColor(Color.White)
          Text(`${this.getOnCount()}/${this.getTotalCount()} 设备开启`)
            .fontSize(10)
            .fontColor('#FFFFFF88')
            .margin({ top: 2 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Column() {
          Text(`${this.devices.filter((d: Device) => d.isOn).length}`)
            .fontSize(FontSize.HEADLINE)
            .fontWeight(FontWeight.Bold)
            .fontColor(Color.White)
          Text('全部设备')
            .fontSize(10)
            .fontColor('#FFFFFF88')
        }
        .alignItems(HorizontalAlign.Center)

        Column() {
          Text('一键')
            .fontSize(FontSize.CAPTION)
            .fontColor(this.toggleAll ? '#FFD93D' : '#FFFFFFAA')
            .fontWeight(FontWeight.Medium)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .borderRadius(9999)
            .backgroundColor('#FFFFFF22')
            .margin({ left: Spacing.LG })
            .onClick(() => { this.toggleAllDevices(); })
        }
      }
      .width('100%')
      .height(140)
      .padding({ left: Spacing.XXL, right: Spacing.XXL, top: Spacing.SM })
      .backgroundColor('#1a1a2e')

      Row() {
        Scroll() {
          Row() {
            ForEach(ROOMS, (room: Room) => {
              Column() {
                Text(room.icon)
                  .fontSize(24)
                  .margin({ bottom: 4 })
                Text(room.name)
                  .fontSize(11)
                  .fontColor(this.selectedRoomId === room.id ? Color.White :
                    AppColors.TEXT_SECONDARY)
                  .fontWeight(this.selectedRoomId === room.id ?
                  FontWeight.Medium : FontWeight.Regular)
                Text(`${this.getRoomOnCount(room.id)}/${this.getRoomTotalCount(room.id)}`)
                  .fontSize(9)
                  .fontColor(this.selectedRoomId === room.id ? '#FFFFFFAA' :
                    AppColors.TEXT_DISABLED)
              }
              .width(68)
              .height(76)
              .borderRadius(BorderRadius.MD)
              .backgroundColor(this.selectedRoomId === room.id ?
                room.color : '#F5F6FA')
              .justifyContent(FlexAlign.Center)
              .margin({ right: Spacing.SM })
              .onClick(() => { this.selectedRoomId = room.id; })
            })
          }
          .padding({ left: Spacing.LG, right: Spacing.LG })
        }
        .scrollBar(BarState.Off)
        .width('100%')
      }
      .width('100%')
      .padding({ top: Spacing.MD, bottom: Spacing.MD })
      .backgroundColor(Color.White)
      .border({ width: { bottom: 1 }, color: '#F0F0F0' })

      Scroll() {
        Column() {
          ForEach(this.getRoomDevices(), (device: Device) => {
            Column() {
              Row() {
                Column() {
                  Text(device.icon)
                    .fontSize(32)
                }
                .width(56).height(56)
                .borderRadius(BorderRadius.MD)
                .backgroundColor(device.isOn ?
                  this.getSliderColor(device) + '20' : '#F0F0F0')
                .justifyContent(FlexAlign.Center)
                .margin({ right: Spacing.LG })

                Column() {
                  Text(device.name)
                    .fontSize(FontSize.MEDIUM)
                    .fontColor(device.isOn ?
                    AppColors.TEXT_PRIMARY : AppColors.TEXT_TERTIARY)
                    .fontWeight(FontWeight.Medium)

                  Text(this.getDeviceStatusText(device))
                    .fontSize(FontSize.CAPTION)
                    .fontColor(device.isOn ?
                    this.getSliderColor(device) : AppColors.TEXT_DISABLED)
                    .margin({ top: 2 })
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Start)

                Toggle({ type: ToggleType.Switch, isOn: device.isOn })
                  .selectedColor(this.getSliderColor(device))
                  .onChange((value: boolean) => {
                    this.toggleDevice(device.id);
                  })
              }
              .width('100%')

              if (device.isOn && device.hasSlider) {
                Column() {
                  Row() {
                    Text(device.sliderLabel)
                      .fontSize(10)
                      .fontColor(AppColors.TEXT_TERTIARY)
                    Blank()
                    Text(`${device.sliderValue}${device.sliderUnit}`)
                      .fontSize(12)
                      .fontColor(this.getSliderColor(device))
                      .fontWeight(FontWeight.Bold)
                  }
                  .width('100%')
                  .margin({ bottom: Spacing.SM })

                  Slider({
                    value: device.sliderValue,
                    min: device.sliderMin,
                    max: device.sliderMax,
                    step: device.sliderStep,
                    style: SliderStyle.InSet
                  })
                    .blockColor(this.getSliderColor(device))
                    .trackColor(this.getSliderColor(device) + '30')
                    .selectedColor(this.getSliderColor(device))
                    .trackThickness(6)
                    .onChange((value: number) => {
                      this.updateSlider(device.id, value);
                    })
                }
                .width('100%')
                .margin({ top: Spacing.MD })
                .padding({ left: Spacing.SM, right: Spacing.SM })
              }
            }
            .width('100%')
            .padding(Spacing.LG)
            .backgroundColor(Color.White)
            .borderRadius(BorderRadius.MD)
            .margin({ left: Spacing.LG, right: Spacing.LG, bottom: Spacing.SM })
          })
        }
        .width('100%')
        .padding({ top: Spacing.LG, bottom: Spacing.XXL })
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)
      .backgroundColor('#F5F6FA')
    }
    .width('100%')
    .height('100%')
  }
}

五、Toggle 与 Slider 的设计模式

5.1 主开关 + 从属调节

这是最常见的模式——Toggle 控制"是否启用",Slider 控制"启用后的程度":

┌──────────────────────────────┐
│  💡 主灯           [Toggle]  │
│  亮度  80%  ───●───────────  │  ← 设备开启时才显示
└──────────────────────────────┘

代码模式:

// 卡片顶部:Toggle 控制开关
Toggle({ type: ToggleType.Switch, isOn: device.isOn })
  .onChange((value: boolean) => { ... })

// 卡片底部:Slider 条件显示
if (device.isOn && device.hasSlider) {
  Slider({ value: device.sliderValue, ... })
}

这个模式在 IoT App 中几乎无处不在——灯、空调、风扇、加湿器全部遵循"开关→参数"的两级控制。

5.2 颜色 = 功能语义

为不同的设备类型分配不同颜色,是 IoT UI 设计的常见实践:

颜色 含义 适用设备
蓝色 通用/灯光 灯、插座
红色 温度/热量 空调、热水器
青色 水/湿度 加湿器、饮水机
金色 风力/速度 风扇、油烟机

这个颜色映射在代码层面通过 getSliderColor() 方法集中管理,Toggle 的 selectedColor 和 Slider 的 blockColor/trackColor/selectedColor 全部使用同一个颜色值,确保视觉一致性。

5.3 状态驱动的 UI 变化

设备卡片的视觉效果应根据开关状态变化:

// 图标背景 — 开:彩色透明底,关:灰色底
.backgroundColor(device.isOn ? color + '20' : '#F0F0F0')

// 设备名 — 开:深色,关:浅灰
.fontColor(device.isOn ? AppColors.TEXT_PRIMARY : AppColors.TEXT_TERTIARY)

// 状态文字 — 开:彩色数字(80%),关:灰色"已关闭"
.fontColor(device.isOn ? color : AppColors.TEXT_DISABLED)

这些变化让用户不依赖 Toggle 的位置就能快速扫视判断设备状态——开启的设备"亮",关闭的设备"暗"。


六、常见面试题 / 踩坑点

6.1 Toggle 的 onChange 和 isOn 绑定有什么区别?

isOn 是数据→UI 的单向绑定(数据变化 → 开关位置变化)。onChange 是 UI→数据的单向回调(用户操作 → 更新数据)。

// 错误:忘记 onChange,isOn 虽然变了但数据没同步
Toggle({ type: ToggleType.Switch, isOn: device.isOn })
  // 缺少 .onChange((v) => { this.updateDevice(v); })

// 正确:onChange 中更新 @State 数据
Toggle({ type: ToggleType.Switch, isOn: device.isOn })
  .onChange((v) => { this.toggleDevice(device.id); })

如果你发现 Toggle 滑不动或滑动后没有效果,99% 是因为缺少 onChange

6.2 Slider 的 onChange 触发频率?

onChange 在用户拖动滑块时实时触发——手指微动 1px,回调就触发一次。对于需要发送网络请求的场景(如调节智能灯亮度),不要在 onChange 中直接发请求——应该用防抖:

.onChange((value: number) => {
  this.localValue = value;  // 立即更新本地状态
  this.debounceSubmit(value);  // 防抖 300ms 后发送请求
})

如果不是网络请求(如本文 Demo 的纯 UI 更新),直接更新即可,不需要防抖。

6.3 ToggleType.Switch vs Checkbox vs Button 如何选择?

类型 适用场景 视觉特征
Switch 即时生效的开关(灯、通知、WiFi) 滑动开关,iOS 风格
Checkbox 需确认的选择(同意协议、多选删除) 勾选框,提交时生效
Button 排他性单选(支付方式、性别) 圆形按钮,选中一个取消其他

关键区别:Switch 是即时生效的"开关",Checkbox 是延迟生效的"勾选",Button 是排他性的"单选"。

6.4 如何实现"关闭设备后隐藏 Slider"?

条件渲染——这是 ArkUI 声明式 UI 的优势:

if (device.isOn && device.hasSlider) {
  // Slider 组件
}

设备关闭时(isOn = false),Slider 相关的 Column 不会渲染,卡片高度自动收缩。这是 React/Vue 开发者在 ArkUI 中最熟悉的模式——不需要手动设置 display:nonevisibility

6.5 如何在 Slider 上显示刻度标记?

Slider 组件内置 step 参数即可实现"刻度感"——用户拖动时会"吸附"到最近的步长值。如果需要在视觉上显示刻度线(如温度计 16/18/20/…/30),可以用 Mark 装饰,或手动在 Slider 上下画一组等距的刻度点:

Slider({ value: 24, min: 16, max: 30, step: 1 })
  .showSteps(true)    // 显示刻度点
  .showTips(true)     // 拖动时显示数值气泡

两个属性一起使用,用户在拖动温度 Slider 时能看到刻度点,手指附近弹出当前值的提示气泡。


七、总结

Toggle 和 Slider 是"麻雀虽小,五脏俱全"的组件代表——API 简洁,但每个参数都直指真实使用场景的核心需求。ToggleType 的三选一、SliderStyle 的四选一、颜色的语义化设计、滑块条件的展开——这些不是"组件文档能教给你的",而是在实际项目中迭代出来的模式。

1. Toggle 的三个类型解决三个问题。 Switch = 即时开关,Checkbox = 确认勾选,Button = 排他单选。看起来只是"换个样式",实际上定义了三种截然不同的交互意图——选择正确的类型是第一步。

2. Slider 的语义化颜色比功能更重要。 用户不会仔细看"温度 24℃"的文字——他们先看到红色滑块,大脑已经知道"这是在调温度"。颜色是一种不用思考就能理解的信息通道。

3. 条件渲染是 Toggle+Slider 组合的最佳拍档。 设备关闭时隐藏 Slider,卡片更紧凑;设备开启时展开 Slider,操作更直观。这种"开→展开"的动画节奏,是所有 IoT App 的交互基模。

4. 数组不可变更新是 @State 的必备模式。 this.devices = this.devices.map(...) 创建新数组 + 新对象——让框架能精确检测到哪个设备的哪个字段发生了变化。违反这个模式 = UI 不更新。

Toggle 和 Slider 最适用的场景:

  • 智能家居 / IoT 设备控制(开关 + 参数调节)
  • 设置页面(通知开关、音量调节、亮度控制)
  • 媒体播放器(播放/暂停 Toggle + 音量/进度 Slider)
  • 游戏内 HUD(音效开关 + 灵敏度调节)
  • 健康/运动 App(目标开关 + 目标值调节)

这两个组件虽然"小",但在真实 App 中的调用频率远超大部分高级组件——一个典型页面可能有 5~10 个 Toggle 和 2~3 个 Slider。把它们的 API 用对、用熟,是每个 ArkUI 开发者的基本功。

Logo

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

更多推荐