如何实现滑动经过复选框时选中/取消的功能
====================

#### 问题现象

列表中的每一个Item都对应一个复选框。当前，CheckBox仅支持逐个选中/取消选中，如何实现手指滑动经过CheckBox的时候，CheckBox能完成选中/取消选中的功能，同时不影响列表滑动。

#### 效果预览

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/f3/v3/hldUFmiMSwONg4wmdvNU3g/zh-cn_image_0000002658968445.gif?HW-CC-KV=V1&HW-CC-Date=20260709T133956Z&HW-CC-Expire=86400&HW-CC-Sign=58DF2E31A94A0D7EA2CB7AC5F158BDE32F8F0C3A58485B2C28EC3D7B55333BE0 "点击放大")

#### 背景知识

*   [CheckBox](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-checkbox)：多选框组件，通常用于某选项的打开或关闭。
*   [onAreaChange](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-component-area-change-event#onareachange)：组件区域变化事件，组件区域变化时触发该回调。仅会响应由布局变化所导致的组件大小、位置发生变化时的回调。
*   [onWillScroll](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-scrollable-common#onwillscroll12)：滚动事件回调，滚动组件滚动前触发。可以获取到相对于上一帧的偏移量、当前滑动状态、当前滑动操作的来源。
*   [onTouch](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-events-touch)：触摸事件，由手指在组件上按下、滑动或抬起时触发。回调中可以获取到触摸事件的类型。

#### 解决方案

CheckBox本身不提供滑动多选的能力，需要自定义选中逻辑：手指在CheckBox选择框区域进行滑动时，完成选中操作；手指在文本区域进行滑动时，进行列表滚动。

1.  首先要明确组件的位置以及当前手指滑动的位置。对于组件的位置，自定义组件位置的数据类ComponentRect，记录当前组件四个边相对于父组件左上角的距离。通过onAreaChange事件，在组件位置发生变化时触发回调，重新计算位置并存储到map中。
    
    _//_ _记录组件的位置_
    interface ComponentRect {
      left: number;
      top: number;
      right: number;
      bottom: number;
    }
    
    _//_ _判断手指当前在哪个组件的矩形区域内_
    detectCurrentComponent(x: number, y: number) {
      let cur \= '';
      this.componentRects.forEach((rect, id) \=\> {
        if (x \>\= rect.left && x <\= rect.right &&
          y \>\= rect.top && y <\= rect.bottom) {
          cur \= id;
        }
      });
      return cur;
    }
    
    _//_ _组件发生变化时，刷新组件的位置信息_
    .onAreaChange((oldVal, newVal) \=\> {
      console.info(\`${oldVal},${newVal}\`);
      let area: ComponentRect \= {
        left: Number(newVal.position.x),
        top: Number(newVal.position.y),
        right: Number(newVal.position.x) \+ Number(newVal.width),
        bottom: Number(newVal.position.y) \+ Number(newVal.height)
      };
      this.componentRects.set(item.id, area); _//_ _记录组件位置_
    })
    
2.  ComponentRect数据类型记录的组件位置是包含了CheckBox和文本的，还需要再次根据组件位置比例判断手指是在CheckBox上还是在文本上，只有在CheckBox选择框上时才触发选中逻辑。
    
    _//_ _获取手指处于组件上的横向比例_
    getRateWidth(x: number, index: string) {
      let item \= this.componentRects.get(index);
      if (!item) {
        return 0;
      }
      return (x \- item.left) / (item.right \- item.left);
    }
    
3.  获取到组件位置和手指位置之后，动态执行CheckBox选中操作，当检测到触摸类型为TouchType.Move且在组件范围，说明手指滑过了选中框，就将对应CheckBox的状态值置反，同时设置列表的friction和onWillScroll用于禁止列表滑动。
    
    .onTouch((event) \=\> {
      _//_ _获取当前手指的坐标（相对于父容器，与组件rect的坐标系一致）_
      const fingerX \= event.touches\[0\].x;
      const fingerY \= event.touches\[0\].y;
     _ // 判断手指当前在哪个组件的矩形区域内_
      let cur \= this.detectCurrentComponent(fingerX, fingerY);
     _ // 获取手指处于组件上的横向比例_
      let rate \= this.getRateWidth(fingerX, cur);
      if (event.type \=== TouchType.Move && 0 < rate && rate < this.widthCheckRate) {
        this.isInCheckBox \= true;
      _  // 更新CheckBox状态_
        if (cur !== this.curId) {
          this.curId \= cur;
          let item \= this.itemsMap.get(cur);
          if (item !== undefined) {
            item.checked \= !item.checked;
            this.itemsMap.set(cur, item);
          }
        }
      }
      if (event.type \=== TouchType.Up) {
        setTimeout(() \=\> {
          this.isInCheckBox \= false;
        }, 100);
        this.curId \= '';
      }
    })
    

完整示例参考如下：

@Entry
@Component
struct CheckBoxTest {
  @State items: Array<CheckBoxStatus\> = \[\];
  _// 记录组件的位置信息_
  @State itemsMap: Map<string, CheckBoxStatus\> = new Map<string, CheckBoxStatus\>();
  @State isInCheckBox: boolean \= false;
  private componentRects: Map<string, ComponentRect\> = new Map(); _// 存储组件的位置信息_
  private widthCheckRate: number \= 0.2; _//_ _选择框所占比例_
  private curId: string \= ''; _//_ _当前手指所在的组件_

  aboutToAppear(): void {
    for (let index \= 0; index < 20; index++) {
      this.itemsMap.set(String(index), { id: String(index), name: '选项序号 - ' \+ index, checked: false });
    }
    this.items \= Array.from(this.itemsMap.values());
  }

  build() {
    Column() {
      List({ space: 10 }) {
        ForEach(this.items, (item: CheckBoxStatus) \=\> {
          ListItem() {
            Row() {
              Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
                Checkbox()
                  .enabled(false)    _// 禁用CheckBox自己的选中逻辑_
                  .select(this.itemsMap.get(item.id)?.checked)
              }.width(\`${100 \* this.widthCheckRate}%\`)
              .hitTestBehavior(HitTestMode.Transparent)

              Flex({ alignItems: ItemAlign.Center }) {
                Text(item.name)
                  .fontSize(18)
              }.width(\`${100 \- 100 \* this.widthCheckRate}%\`)
            }
            .width('100%')
            .height(50)
          }
          .backgroundColor('#fff1f3f5')
          .borderRadius(10)
          .margin({ left: 16, right: 16 })
  _        // 组件发生变化时，刷新组件的位置信息_
          .onAreaChange((oldVal, newVal) \=\> {
            console.info(\`${oldVal},${newVal}\`);
            let area: ComponentRect \= {
              left: Number(newVal.position.x),
              top: Number(newVal.position.y),
              right: Number(newVal.position.x) \+ Number(newVal.width),
              bottom: Number(newVal.position.y) \+ Number(newVal.height)
            };
            this.componentRects.set(item.id, area); _//_ _记录组件位置_
          })
        })
      }
      .friction(this.isInCheckBox ? 1000 : 0)
      .onWillScroll(() \=\> {
     _   // 手指不在CheckBox框位置时，禁止列表滑动_
        if (this.isInCheckBox) {
          return { offsetRemain: 0 };
        }
        return;
      })
      .width('100%')
      .height('100%')
    }
    .padding({ top: 10 })
    .width('100%')
    .height('95%')
    .backgroundColor('#ffffffff')
    .onTouch((event) \=\> {
    _  // 获取当前手指的坐标（相对于父容器，与组件rect的坐标系一致）_
      const fingerX \= event.touches\[0\].x;
      const fingerY \= event.touches\[0\].y;
   _   // 判断手指当前在哪个组件的矩形区域内_
      let cur \= this.detectCurrentComponent(fingerX, fingerY);
      _// 获取手指处于组件上的横向比例_
      let rate \= this.getRateWidth(fingerX, cur);
      if (event.type \=== TouchType.Move && 0 < rate && rate < this.widthCheckRate) {
        this.isInCheckBox \= true;
       _ // 更新CheckBox状态_
        if (cur !== this.curId) {
          this.curId \= cur;
          let item \= this.itemsMap.get(cur);
          if (item !== undefined) {
            item.checked \= !item.checked;
            this.itemsMap.set(cur, item);
          }
        }
      }
      if (event.type \=== TouchType.Up) {
        setTimeout(() \=\> {
          this.isInCheckBox \= false;
        }, 100);
        this.curId \= '';
      }
    })
  }

 _ // 判断手指当前在哪个组件的矩形区域内_
  detectCurrentComponent(x: number, y: number) {
    let cur \= '';
    this.componentRects.forEach((rect, id) \=\> {
      if (x \>\= rect.left && x <\= rect.right &&
        y \>\= rect.top && y <\= rect.bottom) {
        cur \= id;
      }
    });
    return cur;
  }

  _// 获取手指处于组件上的横向比例_
  getRateWidth(x: number, index: string) {
    let item \= this.componentRects.get(index);
    if (!item) {
      return 0;
    }
    return (x \- item.left) / (item.right \- item.left);
  }
}

_//_ _记录CheckBox的状态_
interface CheckBoxStatus {
  id: string;
  name: string;
  checked: boolean;
}

_//_ _记录组件的位置_
interface ComponentRect {
  left: number;
  top: number;
  right: number;
  bottom: number;
}