实现游戏摇杆控制角色移动的动画效果
=================

#### 问题现象

在使用ArkTS开发的游戏页面中，希望实现一个游戏摇杆（虚拟摇杆），通过摇杆控制物体的移动。当按下摇杆滑动时，物体应该按照摇杆方向开始移动；当松开摇杆时，角色停止移动，并且摇杆小球恢复原位，此场景应如何实现？

#### 效果预览

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/49/v3/k5z-XVtrRv-YaXCMTz6Zrg/zh-cn_image_0000002658793777.gif?HW-CC-KV=V1&HW-CC-Date=20260709T134045Z&HW-CC-Expire=86400&HW-CC-Sign=9FF8D5115665B2DD8C28CB8F864A542A7942F093E6BDCDA70D0F7CCC7CDCD7B6 "点击放大")

#### 背景知识

*   UIContext提供[animateTo](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-uicontext-uicontext#animateto)接口来指定由于闭包代码导致的状态变化插入过渡动效，可在其中设置动画效果相关参数。
*   [transition](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-transition-animation-component#transition)用于设置组件插入显示和删除隐藏的过渡效果。

#### 解决方案

1.  定义虚拟摇杆。
    *   由两个Circle组成，设置大小圆半径以及摇杆中心区域小球位置以及初始位置。
        
        Row() {
          Circle({ width: this.maxRadius \* 2, height: this.maxRadius \* 2 })
            .fill('#20101010')
            .position({ x: this.centerX - this.maxRadius, y: this.centerY - this.maxRadius })
          Circle({ width: this.radius \* 2, height: this.radius \* 2 })
            .fill('#403A3A3A')
            .position({ x: this.positionX - this.radius, y: this.positionY - this.radius })
        }
        .height(240)
        .width(240)
        .justifyContent(FlexAlign.Center)
        .position({ x: 0, y: 120 })
        .onTouch(this.handleTouchEvent.bind(this))
        
    *   通过[onTouch](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-events-touch#ontouch)事件处理触摸操作。TouchType.Move事件中，计算手指相对于摇杆中心的位置，并更新小圆圈的位置。同时，计算出手指与中心点连线的角度和距离，用于控制物体的移动方向和速度。TouchType.Up事件中，停止物体的移动，并将小圆圈复位到摇杆的中心。
        
        _//_ _处理手指移动的事件_
        handleTouchEvent(event: TouchEvent) {
          switch (event.type) {
            case TouchType.Down:
            _  // 开始定时任务_
              this.taskId = setInterval(() => {
                this.personX += this.speed \* this.cos;
                this.personY += this.speed \* this.sin;
              }, 40);
              break;
            case TouchType.Move:
             _ // 获取手指位置坐标_
              let x = event.touches\[0\].x;
              let y = event.touches\[0\].y;
             _ // 计算手指与中心点的坐标差值_
              let vx = x - this.centerX;
              let vy = y - this.centerY;
           _   // 计算手指与中心点连线和x轴正半轴的夹角，单位是弧度_
              let angle = Math.atan2(vy, vx);
              this.sin = Math.sin(angle);
              this.cos = Math.cos(angle);
             _ // 计算手指与中心点的距离_
              let distance = this.getDistance(vx, vy);
              this.getUIContext()?.animateTo(
                { curve: curves.responsiveSpringMotion() },
                () => {
                 _ // 计算摇杆小球的坐标_
                  this.positionX = this.centerX + distance \* this.cos;
                  this.positionY = this.centerY + distance \* this.sin;
                  _//_ _弧度换算角度_
                  this.angle = angle \* 180 / Math.PI;
                  this.speed = 5;
                }
              );
              break;
            case TouchType.Up:
             _ // 还原物体速度_
              this.speed = 0;
              _//_ _取消定时任务_
              clearInterval(this.taskId);
             _ // 还原摇杆小球的坐标_
              this.getUIContext()?.animateTo(
                { curve: curves.springMotion() },
                () => {
                  this.positionX = this.centerX;
                  this.positionY = this.centerY;
                }
              );
              break;
          }
        }
        
2.  设置动画效果。
    *   通过animateTo方法来平滑地改变物体的位置和摇杆小圆圈的位置。使用responsiveSpringMotion和springMotion方法来实现弹簧动画效果，使移动更加自然。
        
        this.getUIContext()?.animateTo(
          { curve: curves.responsiveSpringMotion() },
          () => {
           _ // 计算摇杆小球的坐标_
            this.positionX = this.centerX + distance \* this.cos;
            this.positionY = this.centerY + distance \* this.sin;
           _ // 弧度换算角度_
            this.angle = angle \* 180 / Math.PI;
            this.speed = 5;
          }
        );
        
        _//_ _还原摇杆小球的坐标_
        this.getUIContext()?.animateTo(
          { curve: curves.springMotion() },
          () => {
            this.positionX = this.centerX;
            this.positionY = this.centerY;
          }
        );
        
    *   通过[TransitionEffect](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-transition-animation-component#transitioneffect10对象说明)中的combine和animation方法来实现组合动画。
        
        _//_ _物体图片_
        Image(this.src)
          .position({ x: this.personX - 20, y: this.personY - 20 })
          .rotate({ angle: this.angle })
          .transition(TransitionEffect.OPACITY.animation({ duration: 1000 }).combine(
            TransitionEffect.translate({ x: -240 })
          ))
          .width(40)
          .height(40)
        
3.  完整示例参考如下：
    
    import curves from '@ohos.curves';
    
    @Entry
    @Component
    struct AnimationPageDemo {
     _ // 物体坐标_
      @State personX: number = 200;
      @State personY: number = 120;
     _ // 物体角度_
      @State angle: number = 0;
      _// 物体图片_
      @State src: Resource = $r('app.media.startIcon');
      _//_ _是否开始游戏_
      @State isBegin: boolean = false;
      _// 角度正弦和余弦_
      sin: number = 0;
      cos: number = 0;
     _ // 物体移动速度_
      speed: number = 0;
     _ // 任务id_
      taskId: number = -1;
      _// 摇杆中心区域坐标_
      private centerX: number = 120;
     _ // 摇杆小圆球初始位置_
      @State positionX: number = this.centerX;
      private centerY: number = 120;
      @State positionY: number = this.centerY;
      _//_ _大、小圆半径_
      private maxRadius: number = 100;
      private radius: number = 20;
    
      build() {
        Row() {
          Stack() {
            _// 开始按钮和物体_
            if (!this.isBegin) {
            _  // 开始按钮_
              Button('开始游戏')
                .onClick(() => {
                  this.getUIContext()?.animateTo(
                    { duration: 1000 },
                    () => {
                     _ // 点击后显示物体_
                      this.isBegin = true;
                    }
                  );
                });
            } else {
            _  // 物体图片_
              Image(this.src)
                .position({ x: this.personX - 20, y: this.personY - 20 })
                .rotate({ angle: this.angle })
                .transition(TransitionEffect.OPACITY.animation({ duration: 1000 }).combine(
                  TransitionEffect.translate({ x: -240 })
                ))
                .width(40)
                .height(40)
            }
           _ // 摇杆_
            Row() {
              Circle({ width: this.maxRadius \* 2, height: this.maxRadius \* 2 })
                .fill('#20101010')
                .position({ x: this.centerX - this.maxRadius, y: this.centerY - this.maxRadius })
              Circle({ width: this.radius \* 2, height: this.radius \* 2 })
                .fill('#403A3A3A')
                .position({ x: this.positionX - this.radius, y: this.positionY - this.radius })
            }
            .height(240)
            .width(240)
            .justifyContent(FlexAlign.Center)
            .position({ x: 0, y: 120 })
            .onTouch(this.handleTouchEvent.bind(this))
          }
          .height('100%').width('100%')
        }
        .height('100%')
        .width('100%')
      }
     _ // 处理手指移动的事件_
      handleTouchEvent(event: TouchEvent) {
        switch (event.type) {
          case TouchType.Down:
           _ // 开始定时任务_
            this.taskId = setInterval(() => {
              this.personX += this.speed \* this.cos;
              this.personY += this.speed \* this.sin;
            }, 40);
            break;
          case TouchType.Move:
          _  // 获取手指位置坐标_
            let x = event.touches\[0\].x;
            let y = event.touches\[0\].y;
           _ // 计算手指与中心点的坐标差值_
            let vx = x - this.centerX;
            let vy = y - this.centerY;
         _   // 计算手指与中心点连线和x轴正半轴的夹角，单位是弧度_
            let angle = Math.atan2(vy, vx);
            this.sin = Math.sin(angle);
            this.cos = Math.cos(angle);
         _   // 计算手指与中心点的距离_
            let distance = this.getDistance(vx, vy);
            this.getUIContext()?.animateTo(
              { curve: curves.responsiveSpringMotion() },
              () => {
              _  // 计算摇杆小球的坐标_
                this.positionX = this.centerX + distance \* this.cos;
                this.positionY = this.centerY + distance \* this.sin;
              _  // 弧度换算角度_
                this.angle = angle \* 180 / Math.PI;
                this.speed = 5;
              }
            );
            break;
          case TouchType.Up:
          _  // 还原物体速度_
            this.speed = 0;
           _ // 取消定时任务_
            clearInterval(this.taskId);
           _ // 还原摇杆小球的坐标_
            this.getUIContext()?.animateTo(
              { curve: curves.springMotion() },
              () => {
                this.positionX = this.centerX;
                this.positionY = this.centerY;
              }
            );
            break;
        }
      }
      getDistance(x: number, y: number) {
        let d = Math.sqrt(x \* x + y \* y);
        return Math.min(d, this.maxRadius);
      }
    }