• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 动画衔接
2
3
4UI界面除了运行动画之外,还承载着与用户进行实时交互的功能。当用户行为根据意图变化发生改变时,UI界面应做到即时响应。例如用户在应用启动过程中,上滑退出,那么启动动画应该立即过渡到退出动画,而不应该等启动动画完成后再退出,从而减少用户等待时间。对于桌面翻页类从跟手到离手触发动画的场景,离手后动画的初始速度应承继手势速度,避免由于速度不接续导致停顿感的产生。针对以上场景,系统已提供动画与动画、手势与动画之间的衔接能力,保证各类场景下动画平稳光滑地过渡的同时,尽可能降低开发难度。
5
6假设对于某一可动画属性,存在正在运行的动画。当UI侧行为改变该属性终点值时,开发者仅需在[animateTo](../reference/apis-arkui/arkui-ts/ts-explicit-animation.md)动画闭包中改变属性值或者改变[animation](../reference/apis-arkui/arkui-ts/ts-animatorproperty.md)接口作用的属性值,即可产生动画。系统会自动衔接之前的动画和当前的动画,开发者仅需要关注当前单次动画的实现。
7
8
9```ts
10import curves from '@ohos.curves'
11class SetSlt{
12  scaleToggle:boolean = true
13  set():void{
14    this.scaleToggle = !this.scaleToggle;
15  }
16}
17let CurAn:Record<string,curves> = {'curve':curves.springMotion()}
18// 第一步:声明相关状态变量
19@state scaleToggle: boolean = true;
20
21...
22Column() {
23  Button()
24    // 第二步:将状态变量设置到相关可动画属性接口
25    .scale(this.scaleToggle ? 1 : 0.5)
26    // 第三步:通过点击事件改变状态变量值,影响可动画属性值
27    .onclick(() => {
28      let sets = new SetSlt()
29      sets.set()
30    })
31    // 第四步:通过隐式动画接口开启隐式动画,动画终点值改变时,系统自动添加衔接动画
32    .animation(CurAn)
33}
34...
35```
36
37完整示例如下。通过点击click,红色方块的缩放属性会发生变化。当连续快速点击click时,缩放属性的终点值连续发生变化,当前动画也会平滑过渡到朝着新的缩放属性终点值运动。
38
39
40```ts
41import curves from '@ohos.curves';
42class SetSlt{
43  isAnimation:boolean = true
44  set():void{
45    this.isAnimation = !this.isAnimation;
46  }
47}
48@Entry
49@Component
50struct AnimationToAnimationDemo {
51  @State SetAnimation: SetSlt = new SetSlt();
52
53  build() {
54    Column() {
55      Text('ArkUI')
56        .fontWeight(FontWeight.Bold)
57        .fontSize(12)
58        .fontColor(Color.White)
59        .textAlign(TextAlign.Center)
60        .borderRadius(10)
61        .backgroundColor(0xf56c6c)
62        .width(100)
63        .height(100)
64        .scale({ x: this.SetAnimation.isAnimation ? 2 : 1, y: this.SetAnimation.isAnimation ? 2 : 1 })
65        .animation({ curve: curves.springMotion(0.4, 0.8) })
66
67      Button('Click')
68        .margin({ top: 200 })
69        .onClick(() => {
70          this.SetAnimation.set()
71        })
72    }
73    .width('100%')
74    .height('100%')
75    .justifyContent(FlexAlign.Center)
76  }
77}
78```
79
80![zh-cn_image_0000001599971890](figures/zh-cn_image_0000001599971890.gif)
81
82
83
84## 手势与动画的衔接
85
86使用滑动、捏合、旋转等手势的场景中,跟手过程中一般会触发属性的改变。离手后,这部分属性往往会继续发生变化,直到到达属性终点值。
87
88离手阶段的属性变化初始速度应与离手前一刻的属性改变速度保持一致。如果离手后属性变化速度从0开始,就好像正在运行的汽车紧急刹车,造成观感上的骤变是用户和开发者都不希望看到的。
89
90针对在手势和动画之间进行衔接的场景(如列表滑动),可以在跟手阶段每一次更改组件属性时,都做成使用跟手弹簧曲线的属性动画。离手时再用离手弹簧曲线产生离手阶段的属性动画。对于采用[springMotion](../reference/apis-arkui/js-apis-curve.md#curvesspringmotion9)曲线的动画,离手阶段动画将自动继承跟手阶段动画的速度,并以跟手动画当前位置为起点,运动到指定的属性终点。
91
92
93```ts
94import curves from '@ohos.curves'
95class SetOffset{
96  offsetX:number = 0;
97  offsetY:number = 0;
98  set(x:number,y:number):void{
99    this.offsetX = x;
100    this.offsetY = y;
101  }
102}
103// 第一步:声明相关状态变量
104@state offsetX: number = 0;
105@State offsetY: number = 0;
106targetOffsetX: number = 100;
107targetOffsetY: number = 100;
108...
109Column()
110  // 第二步:将状态变量设置到相关可动画属性接口
111  .translate({ x: this.offsetX, y: this.offsetY})
112  .gesture(
113    PanGesture({})
114      .onActionUpdate((event?: GestureEvent) => {
115        // 第三步:在跟手过程改变状态变量值,并且采用reponsiveSpringMotion动画运动到新的值
116        animateTo({
117          curve: curves.responsiveSpringMotion()
118        }, () => {
119          if(event){
120            let setxy = new SetOffset();
121            setxy.set(event.offsetX,event.offsetY)
122          }
123        })
124      })
125      .onActionEnd(() => {
126        // 第四步:在离手过程设定状态变量终点值,并且用springMotion动画运动到新的值,springMotion动画将继承跟手阶段的动画速度
127        animateTo({
128          curve: curves.SpringMotion()
129        }, () => {
130          let setxy = new SetOffset();
131          setxy.set(targetOffsetX,targetOffsetY)
132        })
133      })
134  )
135...
136```
137
138完整的示例和效果如下。
139
140
141```ts
142import curves from '@ohos.curves';
143
144@Entry
145@Component
146struct SpringMotionDemo {
147  @State positionX: number = 100;
148  @State positionY: number = 100;
149  diameter: number = 50;
150
151  build() {
152    Column() {
153      Row() {
154        Circle({ width: this.diameter, height: this.diameter })
155          .fill(Color.Blue)
156          .position({ x: this.positionX, y: this.positionY })
157          .onTouch((event?: TouchEvent) => {
158            if(event){
159              if (event.type === TouchType.Move) {
160                // 跟手过程,使用responsiveSpringMotion曲线
161                animateTo({ curve: curves.responsiveSpringMotion() }, () => {
162                  // 减去半径,以使球的中心运动到手指位置
163                  this.positionX = event.touches[0].windowX - this.diameter / 2;
164                  this.positionY = event.touches[0].windowY - this.diameter / 2;
165                  console.info(`move, animateTo x:${this.positionX}, y:${this.positionY}`);
166                })
167              } else if (event.type === TouchType.Up) {
168                // 离手时,使用springMotion曲线
169                animateTo({ curve: curves.springMotion() }, () => {
170                  this.positionX = 100;
171                  this.positionY = 100;
172                  console.info(`touchUp, animateTo x:100, y:100`);
173                })
174              }
175            }
176          })
177      }
178      .width("100%").height("80%")
179      .clip(true) // 如果球超出父组件范围,使球不可见
180      .backgroundColor(Color.Orange)
181
182      Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Start, justifyContent: FlexAlign.Center }) {
183        Text("拖动小球").fontSize(16)
184      }
185      .width("100%")
186
187      Row() {
188        Text('点击位置: [x: ' + Math.round(this.positionX) + ', y:' + Math.round(this.positionY) + ']').fontSize(16)
189      }
190      .padding(10)
191      .width("100%")
192    }.height('100%').width('100%')
193  }
194}
195```
196
197![zh-cn_image_0000001647027001](figures/zh-cn_image_0000001647027001.gif)
198