什么是单选组

熟悉web的小伙伴一定知道,在html中如果我们给radio表单设置相同的name,会视为一个单选组。

  1. <input type="radio" name="group" value="1"> 我是成员1
  2. <input type="radio" name="group" value="2"> 我是成员2
  3. <input type="radio" name="group" value="3"> 我是成员3

对于一个单选组,我们只需要知道这个组里,我们选中的表单值就好了。

  1. document.getElementsByName("group").forEach((v) => {
  2. v.addEventListener("change", function (e) {
  3. let value = e.currentTarget.value
  4. document.getElementById("result").innerText = "您选中了成员" + value
  5. }, false)
  6. })

单选组效果:

1.gif

在OpenHarmony应用开发中,我们可以使用多个Toggle组件来实现。

OpenHarmony Toggle组件请参考: 【甜甜酱OH文档补充】OpenHarmony Toggle组件

来看看我们想要的一个效果吧。

2.gif

现在先定义出一个新的组件RadioGroup

  1. @Component
  2. struct RadioGroup {
  3. build(){}
  4. }

数据

要实现一个单选组组件,首先要知道我们需要什么。

  • 单选项

    一个单选项应该由两种数据构成。

    • 单选项的对应值

    • 单选项要显示文字

    那么我们先定义一个接口叫Option

    1. interface Option {
    2. displayValue: string, //显示文字
    3. value: string | number //对应值
    4. }
  • 单选组

    一个单选组一定是由多个单选项组成的。那么RadioGroup组件一定是需要获取一个单选项数组Option[]

    private list: Option[]  //一个Option数组
  • 单选组值

    对于一个单选组,永远会是有值的,因为总有一个选项是被选中的。

    单选组值我们进行双向绑定。

    @Link value: string | number //类型与Option的value保持一致

    如果没有给定单选组值,那么默认单选组值为第一个选项的对应值。

    1. aboutToAppear() {
    2. if (!this.value) {
    3. this.value = this.list[0].value
    4. }
    5. }

现在我们先渲染一个基本的页面来看看效果吧。

  1. interface Option {
  2. displayValue: string,
  3. value: string | number
  4. }
  5. @Component
  6. struct RadioGroup {
  7. private list: Option[]
  8. @Link value: string | number
  9. aboutToAppear() {
  10. if (!this.value) {
  11. this.value = this.list[0].value
  12. }
  13. }
  14. build() {
  15. Flex({
  16. wrap: FlexWrap.Wrap
  17. }) {
  18. ForEach(this.list, (option: Option, index: number) => {
  19. Toggle({
  20. type: ToggleType.Button,
  21. isOn: false
  22. }) {
  23. Text(option.displayValue).fontSize(24).fontColor(Color.White)
  24. }
  25. .width(150)
  26. .height(80)
  27. .selectedColor(Color.Blue)
  28. })
  29. }
  30. }
  31. }
  32. @Entry
  33. @Component
  34. struct Index {
  35. private list: Option[] = [
  36. {
  37. displayValue: "这是选项A",
  38. value: "A"
  39. },
  40. {
  41. displayValue: "这是选项B",
  42. value: "B"
  43. },
  44. {
  45. displayValue: "这是选项C",
  46. value: "C"
  47. },
  48. {
  49. displayValue: "这是选项D",
  50. value: "D"
  51. }
  52. ]
  53. @State value: string | number = ''
  54. build() {
  55. Column() {
  56. RadioGroup({
  57. list: this.list,
  58. value: $value
  59. })
  60. Column(){
  61. Text(`您选择了${this.value}`).fontSize(30)
  62. }
  63. .height(100)
  64. .margin({ top: 40 })
  65. }
  66. .height('100%')
  67. .width('100%')
  68. .padding(20)
  69. }
  70. }

但是现在每个Toggle组件控制都是独立的。

3.gif

状态管理

我们知道Toggle组件是靠isOn的值来渲染自己当前的状态,现在我们为单选组建立一个状态管理数组。

@State _state: boolean[] = []  //状态管理数组

现在按照索引为每个选项组件建立好自己的状态数据。

  • 选项默认状态为关闭false
  • 单选组值对应的选项状态为开启true
  1. if (!this.value) {
  2. this.value = this.list[0].value
  3. }
  4. this.list.forEach((option, index) => {
  5. let state = false
  6. if (option.value == this.value) { //单选组值对应选项
  7. state = true
  8. }
  9. this._state[index] = state
  10. })

1.png

当我们每次修改单选组值value后,应该刷新状态管理数组_state

我们定义一个刷新状态的方法refresh

  1. private refresh(){
  2. this.list.forEach((option, index) => {
  3. let state = false
  4. if (option.value == this.value) {
  5. state = true
  6. }
  7. this._state[index] = state
  8. })
  9. }

Toggle组件每次切换状态会执行onChange方法。

当我们发现组件发生变化时,就去更新单选组值,并刷新整个状态数组。

  1. ForEach(this.list, (option: Option, index: number) => {
  2. Toggle({
  3. type: ToggleType.Button,
  4. isOn: this._state[index]
  5. })
  6. .onChange((isOn: boolean) => {
  7. this.value = option.value
  8. this.refresh()
  9. })
  10. })

现在还差最重要的一步,就是保证选中的单选项再次被点击后不会切换状态。

这里就要用到enabled属性了,只要我们发现这个选项被选中就让这个选项不可响应。

  1. ForEach(this.list, (option: Option, index: number) => {
  2. Toggle({
  3. type: ToggleType.Button,
  4. isOn: this._state[index]
  5. })
  6. .enabled(!this._state[index]) //是否响应状态 正好与 是否选中状态值相反
  7. })

自定义选项

虽然实现了一个单选组组件,但是我们发现每个选项的样式都被固定在RadioGroup组件内,我们如果想修改样式就必须修改RadioGroup组件。那么有没有什么办法,可以让我们使用单选组逻辑的同时又能够自定义Toggle组件的样式呢。

这里要感谢社区小伙伴提供的ets插槽思路,使用ForEach配合@Builder装饰器来实现。

选项构造器

首先我们为RadioGroup组件添加一个选项构造器itemBuilder

  1. type Update = () => void
  2. type ItemBuilder = (item: Option, index: number, state: boolean, update: Update) => void
  3. struct RadioGroup {
  4. private itemBuilder: ItemBuilder
  5. ...
  6. }

这个构造器会为父级组件提供RadioGroup选项信息。

  • item: Option 当前选项信息
  • index:number 当前选项索引
  • state:boolean 当前选项状态
  • update:Update 刷新RadioGroup状态方法

有了构造器,我们就可以利用构造器返回的信息渲染单个选项。

我们来看看构造器是如何使用的。

@Builder装饰器定义了一个如何渲染自定义组件的方法。此装饰器提供了一个修饰方法,其目的是和build函数一致。

@Builder装饰器装饰的方法的语法规范与build函数也保持一致。

通过@Builder装饰器可以在一个自定义组件内快速生成多个布局内容。

  1. @Entry
  2. @Component
  3. struct Index {
  4. @State value: string = ''
  5. // 渲染选项
  6. @Builder OptionItem(option: Option, index: number, state: boolean, update: Update) {
  7. Toggle({
  8. type: ToggleType.Button,
  9. isOn: state //Toggle组件状态
  10. }) {
  11. // 选项显示文字
  12. Text(`${option.displayValue}`).fontSize(24).fontColor(Color.White)
  13. }
  14. .onChange((isOn)=>{
  15. // 发生变化时,执行刷新方法
  16. update()
  17. })
  18. .enabled(!state) //Toggle组件可响应状态
  19. .selectedColor(Color.Blue)
  20. .size({
  21. width: 150,
  22. height: 80
  23. })
  24. }
  25. build() {
  26. Column() {
  27. RadioGroup({
  28. value: $value, // 单选组值是双向绑定的
  29. list: this.list,
  30. itemBuilder: (option: Option, index: number, state: boolean, update: Update) => {
  31. // 在itemBuilder内调用@Builder装饰器方法
  32. this.OptionItem(option, index, state, update)
  33. }
  34. })
  35. }
  36. .height('100%')
  37. .width('100%')
  38. }
  39. }

实现构造器

那么这个构造器怎么实现呢?

我们要知道build方法内仅支持:

  • 组合组件
  • 使用渲染控制语法
  • 调用@Builder装饰的方法

要让itemBuilder构造器方法在build中被调用,我们就需要让itemBuilder成为一个组件。

这就要提起我们的ForEach循环渲染了。

当我们使用ForEach只执行一次循环时,就相当于是一个组件在被渲染。那么只要我们将itemBuilder构造器方法重新定义在ForEach生成子组件的lambda函数上,就可以让itemBuilder构造器成为一个可以被渲染的组件了。

bind()函数创建了一个新函数(原函数的拷贝),这个函数接受一个提供新的this上下文的参数,以及之后任意可选的其他参数。当这个新函数被调用时,它的this关键字指向第一个参数的新上下文。而第二个之后的参数会与原函数的参数组成新参数(原函数的参数在后),传递给函数。

另外使用call()apply()都是可以的,我们要的就是重新定义ForEach的生成子组件方法。

  1. private _once = Array(1)
  2. @Builder RadioGroupItem(option: Option, index: number) {
  3. ForEach(this._once, this.itemBuilder.bind(this, option, index, this._state[index], () => {
  4. this.value = option.value
  5. this.refresh()
  6. }))
  7. }

循环渲染选项组件

  1. build() {
  2. Flex({
  3. wrap: FlexWrap.Wrap
  4. }) {
  5. ForEach(this.list, (option: Option, index: number) => {
  6. this.RadioGroupItem(option, index)
  7. })
  8. }
  9. }

完整代码

ui/RadioGroup.ets

  1. export interface Option {
  2. displayValue: string,
  3. value: string | number
  4. }
  5. export type Update = () => void
  6. export type ItemBuilder = (item: Option, index: number, state: boolean, update: Update) => void
  7. // 单选组
  8. @Component
  9. export struct RadioGroup {
  10. private list: Option[]
  11. private itemBuilder: ItemBuilder
  12. @State _state: boolean[] = []
  13. @Link value: string | number
  14. private _once: number[]
  15. private refresh() {
  16. this.list.forEach((option, index) => {
  17. let state = false
  18. if (option.value == this.value) {
  19. state = true
  20. }
  21. this._state[index] = state
  22. })
  23. }
  24. aboutToAppear() {
  25. this._once = [1]
  26. if (!this.value) {
  27. this.value = this.list[0].value
  28. }
  29. this.refresh()
  30. }
  31. @Builder RadioGroupItem(option: Option, index: number) {
  32. ForEach(this._once, this.itemBuilder.bind(this, option, index, this._state[index], () => {
  33. this.value = option.value
  34. this.refresh()
  35. }))
  36. }
  37. build() {
  38. Flex({
  39. wrap: FlexWrap.Wrap
  40. }) {
  41. ForEach(this.list, (option: Option, index: number) => {
  42. this.RadioGroupItem(option, index)
  43. })
  44. }
  45. }
  46. }

index.ets

  1. import {Option, RadioGroup, Update} from '../ui/RadioGroup.ets';
  2. @Entry
  3. @Component
  4. struct Index {
  5. @State value: string = ''
  6. private list: Option[] = [
  7. {
  8. displayValue: "这是选项A",
  9. value: "A"
  10. },
  11. {
  12. displayValue: "这是选项B",
  13. value: "B"
  14. },
  15. {
  16. displayValue: "这是选项C",
  17. value: "C"
  18. },
  19. {
  20. displayValue: "这是选项D",
  21. value: "D"
  22. }
  23. ]
  24. @Builder OptionItem(option: Option, index: number, state: boolean, update: Update) {
  25. Flex({
  26. alignItems: ItemAlign.Center,
  27. justifyContent: FlexAlign.Center
  28. }) {
  29. Toggle({
  30. type: ToggleType.Button,
  31. isOn: state
  32. }) {
  33. Text(`${option.displayValue}`).flexGrow(1).fontSize(24).fontColor(Color.White)
  34. }
  35. .onChange((isOn) => {
  36. update()
  37. })
  38. .enabled(!state)
  39. .selectedColor(Color.Blue)
  40. .borderRadius(150)
  41. .size({
  42. width: 150,
  43. height: 80
  44. })
  45. }
  46. .width('25%')
  47. .margin({
  48. top: 10,
  49. bottom: 10
  50. })
  51. }
  52. build() {
  53. Column() {
  54. RadioGroup({
  55. value: $value,
  56. list: this.list,
  57. itemBuilder: (option: Option, index: number, state: boolean, update: Update) => {
  58. this.OptionItem(option, index, state, update)
  59. }
  60. })
  61. Column() {
  62. Text(`您选择了${this.value}`).fontSize(30)
  63. }.margin(40)
  64. }
  65. .height('100%')
  66. .width('100%')
  67. }
  68. }

这是我自己的一个思路,自定义组件在实际应用开发中应该要考虑到更多的情况,大家有更好的思路或者实现方式希望可以在社区里一起交流。