实现组件拖拽不超出限定区域
=============

#### 问题现象

*   **场景一**：
    
    使用offset属性定位和PanGesture手势实现组件拖拽，如何限制组件拖拽范围？目前按照如下代码实现，被拖拽组件会超出限定区域，期望组件在拖拽过程中不会超出限定区域。
    
    问题代码示例参考如下：
    
    @Entry
    @Component
    struct DraggableDialog {
      @State offsetX: number = 0
      @State offsetY: number = 0
      @State startX: number = 0
      @State startY: number = 0
    
      build() {
        Stack() {
       _   // 弹框主体_
          Column() {
            Text('可拖拽弹窗')
              .fontSize(20)
              .fontColor(Color.White)
              .margin({ bottom: 10 })
    
            Text('拖拽我移动位置')
              .fontSize(16)
              .fontColor(Color.White)
          }
          .width(200)
          .height(150)
          .backgroundColor(Color.Black)
          .borderRadius(10)
          .shadow({
            radius: 8,
            color: Color.Gray,
            offsetX: 5,
            offsetY: 5
          })
          .padding(20)
          .offset({ x: this.offsetX, y: this.offsetY })
          .gesture(
            PanGesture()
              .onActionStart(() => {
                this.startX = this.offsetX
                this.startY = this.offsetY
              })
              .onActionUpdate((event: GestureEvent) => {
                this.offsetX = this.startX + event.offsetX
                this.offsetY = this.startY + event.offsetY
              })
          )
    
       _   // 页面背景文字_
          Text('页面背景')
            .fontSize(30)
            .margin({ top: 300 })
        }
        .width('100%')
        .height('100%')
      }
    }
    
    问题效果预览：
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/45/v3/HiBz3ttfSWOJb2fE1zsVRQ/zh-cn_image_0000002658917663.png?HW-CC-KV=V1&HW-CC-Date=20260709T133920Z&HW-CC-Expire=86400&HW-CC-Sign=5C374FD31D96982EFAA9FC28EF382B88A7A87E4BAE96B1AE0A76019AB276772E "点击放大")
    
*   **场景二**：
    
    可拖拽组件的初始位置(Stack({ alignContent: Alignment.End }))不在屏幕左上角的场景时，如何限制组件拖拽范围？在代码中给Stack组件添加了拖动事件，设置了任意方向可拖动，当前发现组件会拖动到屏幕以外的区域，期望能够实现组件只在屏幕内拖动的效果。
    
    问题代码示例参考如下：
    
    @Entry
    @Component
    struct Index {
      private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All })
      @State offsetX: number = 0
      @State offsetY: number = 0
      @State positionX: number = 0
      @State positionY: number = 0
    
      build() {
        Column() {
          Text(\`PanGesture offset: \\nX: ${this.offsetX} \\nY: ${this.offsetY}\`).height(100)
          Stack({ alignContent: Alignment.End }) {
            Column()
              .width(100)
              .height(100)
              .backgroundColor(Color.Pink)
              .margin({ top: '200lpx' })
              .align(Alignment.End)
              .translate({ x: this.offsetX, y: this.offsetY, z: 0 })
              .gesture(
                PanGesture(this.panOption)
                  .onActionStart((event: GestureEvent) => {
                    console.info('Pan start')
                    this.panOption.setDirection(PanDirection.All) _// 往任意方向拖动_
                  })
                  .onActionUpdate((event: GestureEvent) => {
                    if (event) {
                      this.offsetX = this.positionX + event.offsetX
                      this.offsetY = this.positionY + event.offsetY
                    }
                  })
                  .onActionEnd((event: GestureEvent) => {
                    this.positionX = this.offsetX
                    this.positionY = this.offsetY
                    console.info('Pan end')
                  })
              )
          }.height(200)
          .width('100%')
        }.height('100%')
        .width('100%')
      }
    }
    
    问题效果预览：
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/a1/v3/SPLOsxk-TfOBUpZdNTLWRQ/zh-cn_image_0000002628398436.png?HW-CC-KV=V1&HW-CC-Date=20260709T133920Z&HW-CC-Expire=86400&HW-CC-Sign=F963A741318E2F6704DE52F620151244D46BB55CD67E5F9EDD525E753E8BEBDB "点击放大")
    

#### 背景知识

*   [PanGesture](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-gestures-pangesture)手势事件通过设置distance属性定义触发阈值，当滑动距离达到阈值时，通过事件回调参数[GestureEvent](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-gesture-common#gestureevent对象说明)可获取X/Y轴偏移量。
*   [onAreaChange](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-component-area-change-event)是组件区域变化回调，当组件尺寸或位置发生改变时触发，可通过事件参数获取组件宽高、坐标等布局信息。对于指定位置的组件拖拽，需要调整拖拽区域位置。

#### 解决方案

*   **场景一**：
    
    问题代码没有对坐标的取值作限制，导致组件可以被拖动到屏幕外。可以考虑通过onAreaChange回调获取容器尺寸参数，结合被拖动组件的尺寸限制计算位移边界阈值，在PanGesture的[onActionUpdate](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-gestures-pangesture#onactionupdate)回调中实时计算并约束组件位置偏移量。
    
    @Entry
    @Component
    struct DraggableDialog1 {
      @State offsetX: number = 0;
      @State offsetY: number = 0;
      @State startX: number = 0;
      @State startY: number = 0;
      @State outerContainerWidth: number = 0;
      @State outerContainerHeight: number = 0;
      private dialogWidth = 200;
      private dialogHeight = 150;
      private minX: number = 0;
      private maxX: number = 0;
      private minY: number = 0;
      private maxY: number = 0;
    
      build() {
        Stack() {
       _   // 弹框主体_
          Column() {
            Text('可拖拽弹窗')
              .fontSize(20)
              .fontColor(Color.White)
              .margin({ bottom: 10 });
    
            Text('拖拽我移动位置')
              .fontSize(16)
              .fontColor(Color.White);
          }
          .width(this.dialogWidth)
          .height(this.dialogHeight)
          .backgroundColor(Color.Black)
          .borderRadius(10)
          .shadow({
            radius: 8,
            color: Color.Gray,
            offsetX: 5,
            offsetY: 5
          })
          .padding(20)
          .position({ x: this.offsetX, y: this.offsetY })
          .gesture(
            PanGesture()
              .onActionStart(() => {
                this.startX = this.offsetX;
                this.startY = this.offsetY;
              })
              .onActionUpdate((event: GestureEvent) => {
                this.offsetX = this.startX + event.offsetX;
                this.offsetY = this.startY + event.offsetY;
            _    // 判断坐标是否超出区域限定，超出后重新赋值_
                if (this.offsetX <= this.minX) {
                  this.offsetX = this.minX;
                }
                if (this.offsetX >= this.maxX) {
                  this.offsetX = this.maxX;
                }
                if (this.offsetY <= this.minY) {
                  this.offsetY = this.minY;
                }
                if (this.offsetY >= this.maxY) {
                  this.offsetY = this.maxY;
                }
              })
          );
    
       _   // 页面背景文字_
          Text(\`页面背景\\nX：${this.offsetX}\\nY：${this.offsetY}\`)
            .fontSize(30)
            .margin({ top: 300 });
        }
        .width('100%')
        .height('100%')
        .onAreaChange((oldvalue: Area, newValue: Area) => {
          console.info(\` ${oldvalue}\`);
       _   // 限定区域宽高_
          this.outerContainerWidth = Number(newValue.width);
          this.outerContainerHeight = Number(newValue.height);
        _  // 最大横纵坐标_
          this.maxX = this.outerContainerWidth - this.dialogWidth;
          this.maxY = this.outerContainerHeight - this.dialogHeight;
        });
      }
    }
    
    效果预览：
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/74/v3/2DBu4d7dR5KG0MQ0laGPXQ/zh-cn_image_0000002658797719.png?HW-CC-KV=V1&HW-CC-Date=20260709T133920Z&HW-CC-Expire=86400&HW-CC-Sign=51CEF2447F82591C0AEDB874D67D81519D9E0E1BC97940035C3F233E1B7485B9 "点击放大")
    
*   **场景二**：
    
    设置Stack({ alignContent: Alignment.End })后，布局为尾端纵向居中，此时，坐标(0，0)不在屏幕的左上角，而是在方块右上角向左平移100vp，向下平移(xx)vp的位置。此时，整个屏幕被划分成了四个区域，左上角为(x<0,y<0)，右上角为(x>0,y<0)，左下角为(x<0,y>0)，右下角为(x>0,y>0)。
    
    import { window } from '@kit.ArkUI';
    
    @Entry
    @Component
    struct DraggableDialog2 {
      private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All });
      @State offsetX: number = 0;
      @State offsetY: number = 0;
      @State positionX: number = 0;
      @State positionY: number = 0;
      @State windowRectWidth: number = 0;
      @State windowRectHeight: number = 0;
    
      aboutToAppear(): void {
       _ // 获取窗口尺寸_
        window.getLastWindow(this.getUIContext().getHostContext()).then((win) => {
          const properties = win.getWindowProperties();
          this.windowRectWidth = this.getUIContext().px2vp(properties.windowRect.width);
          this.windowRectHeight = this.getUIContext().px2vp(properties.windowRect.height);
    
          const avoidArea = win.getWindowAvoidArea(window.AvoidAreaType.TYPE\_SYSTEM);
       _   // 获取顶部状态栏高度_
          const topRectHeight = this.getUIContext().px2vp(avoidArea.topRect.height);
         _ // 获取底部导航栏高度_
          const bottomRectHeight = this.getUIContext().px2vp(avoidArea.bottomRect.height);
          this.windowRectHeight = this.windowRectHeight - topRectHeight - bottomRectHeight;
    
          console.info(\`window width: ${this.windowRectWidth}\`);
          console.info(\`window height: ${this.windowRectHeight}\`);
        });
      }
    
      build() {
        Column() {
          Text(\`PanGesture offset: \\nX: ${this.offsetX} \\nY: ${this.offsetY}\`).height(100);
          Stack({ alignContent: Alignment.End }) {
            Column()
              .width(100)
              .height(100)
              .backgroundColor(Color.Pink)
              .margin({ top: '200lpx' })
              .align(Alignment.End)
              .translate({ x: this.offsetX, y: this.offsetY, z: 0 })
              .gesture(
                PanGesture(this.panOption)
                  .onActionStart(() => {
                    console.info('Pan start');
                    this.panOption.setDirection(PanDirection.All); _// 往任意方向拖动_
                  })
                  .onActionUpdate((event: GestureEvent) => {
                    if (event) {
                      this.offsetX = this.positionX + event.offsetX;
                      this.offsetY = this.positionY + event.offsetY;
                      if (this.offsetX > 0) {
                        this.offsetX = 0;
                      } else if (this.offsetX < -this.windowRectWidth + 100) {
                        this.offsetX = -this.windowRectWidth + 100;
                      }
                      if (this.offsetY < -200) {
                        this.offsetY = -200;
                      } else if (this.offsetY > this.windowRectHeight - 300) {
                        this.offsetY = this.windowRectHeight - 300;
                      }
                    }
                  })
                  .onActionEnd(() => {
                    this.positionX = this.offsetX;
                    this.positionY = this.offsetY;
                    console.info('Pan end');
                  })
              );
          }.height(200)
          .width('100%');
        }.height('100%')
        .width('100%');
      }
    }
    
    效果预览：
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/d0/v3/Hpc-v290Tv-ySEqdAXbPTg/zh-cn_image_0000002628558354.png?HW-CC-KV=V1&HW-CC-Date=20260709T133920Z&HW-CC-Expire=86400&HW-CC-Sign=5CDEC1B4690FA16F11EA242F6EA7EE3B73BF1F43C0AE36B6C1C82715D98B4FF9 "点击放大")