# 如何实现带有滚动动画效果的数字显示组件

#### 问题现象

如何实现一个带有滚动动画效果的数字显示组件，可以通过单击按钮来增加数字，数字将以动画效果过渡至新值。

#### 效果预览

#### 背景知识

*   显式动画接口animateTo能够指定由于闭包代码导致的状态变化插入过渡动效。
*   通用属性translate能够设置组件平移，可使组件在以组件左上角为坐标原点的坐标系中进行移动。margin属性能够设置组件的外边距属性，保持组件的相对位置。

#### 解决方案

1.  定义状态变量：状态变量scrollNumber通过@Prop和@Watch装饰器绑定到父组件的num状态，当num变化时，调用changeNumber方法。其余状态变量如下：
    
    ```
    @Prop @Watch('changeNumber') scrollNumber: number = 0;
    @State bottomNum: number = 0;
    @State showNumber: string = '0';
    @State cacheNumber: string = '';
    changeNumberFinal: string = '';
    isScrolling: boolean = false;
    isInit: boolean = false;
    
    changeNumber() {
      if (this.isInit) {
        this.animateToScroll(this.scrollNumber);
      }
    }
    ```
    
2.  定义方法initAnimateToScroll，用来初始化动画，设置在50毫秒后开始动画的定时器。在方法animateToScroll中执行滚动动画，根据传入的newNum更新bottomNum和isScrolling，并在动画完成后更新showNumber和cacheNumber。
    
    ```
    initAnimateToScroll() {
      setTimeout(() => {
        this.getUIContext().animateTo({
          duration: 600,
          onFinish: () => {
            this.isInit = true;
          }
        }, () => {
          this.bottomNum = -50;
        });
      }, 50);
    }
    
    animateToScroll(newNum: number) {
      this.changeNumberFinal = String(newNum);
      if (this.isScrolling) {
        return;
      }
      this.cacheNumber = this.changeNumberFinal;
      this.getUIContext().animateTo({
        duration: 600,
        onFinish: () => {
          this.bottomNum = -50;
          this.showNumber = this.cacheNumber;
          this.cacheNumber = '';
          this.isScrolling = false;
          if (this.changeNumberFinal !== this.showNumber) {
            this.animateToScroll(Number(this.changeNumberFinal));
          }
        }
      }, () => {
        this.bottomNum = 0;
        this.isScrolling = true;
      });
    }
    ```
    
3.  通过translate和margin属性控制数字的垂直位置，配合动画实现滚动效果。
    
    ```
    Row() {
      Text(this.showNumber)
        .fontSize(35)
        .fontColor('#0A59F7');
    }
    .margin({ bottom: this.bottomNum })
    .height(50)
    .translate({ y: this.bottomNum });
    ```
    

完整示例参考如下：

```
@Entry
@Component
struct ScrollNum {
  @State num: number = 0;
  @State isEnabled: boolean = true;

  build() {
    Column({ space: 50 }) {
      ScrollNumComponent({ scrollNumber: this.num });

      Button('点击添加')
        .enabled(this.isEnabled)
        .onClick(() => {
          this.isEnabled = !this.isEnabled;
          setTimeout(() => {
            this.num += 50;
            this.isEnabled = !this.isEnabled;
          }, 600);
        });
    }
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%');
  }
}

@Component
export struct ScrollNumComponent {
  @Prop @Watch('changeNumber') scrollNumber: number = 0;
  @State bottomNum: number = 0;
  @State showNumber: string = '0';
  @State cacheNumber: string = '';
  changeNumberFinal: string = '';
  isScrolling: boolean = false;
  isInit: boolean = false;

  changeNumber() {
    if (this.isInit) {
      this.animateToScroll(this.scrollNumber);
    }
  }

  aboutToAppear(): void {
    this.initAnimateToScroll();
  }

  initAnimateToScroll() {
    setTimeout(() => {
      this.getUIContext().animateTo({
        duration: 600,
        onFinish: () => {
          this.isInit = true;
        }
      }, () => {
        this.bottomNum = -50;
      });
    }, 50);
  }

  animateToScroll(newNum: number) {
    this.changeNumberFinal = String(newNum);
    if (this.isScrolling) {
      return;
    }
    this.cacheNumber = this.changeNumberFinal;
    this.getUIContext().animateTo({
      duration: 600,
      onFinish: () => {
        this.bottomNum = -50;
        this.showNumber = this.cacheNumber;
        this.cacheNumber = '';
        this.isScrolling = false;
        if (this.changeNumberFinal !== this.showNumber) {
          this.animateToScroll(Number(this.changeNumberFinal));
        }
      }
    }, () => {
      this.bottomNum = 0;
      this.isScrolling = true;
    });
  }

  build() {
    Column() {
      Row() {
        Text(this.cacheNumber)
          .fontSize(35)
          .fontColor('#0A59F7');
      }
      .height(50)
      .translate({ y: this.bottomNum });

      Row() {
        Text(this.showNumber)
          .fontSize(35)
          .fontColor('#0A59F7');
      }
      .margin({ bottom: this.bottomNum })
      .height(50)
      .translate({ y: this.bottomNum });
    }
    .clip(true)
    .height(50);
  }
}
```