动画
概述
Ionic Animations 是一个工具,使开发者能够以平台无关的方式创建复杂动画,无需特定的框架或 Ionic 应用。
创建高效的动画可能具有挑战性,因为它受到可用库和设备硬件资源的限制。此外,许多动画库使用 JavaScript 驱动的方法,这可能会降低动画的可扩展性并占用 CPU 时间。
相比之下,Ionic Animations 使用 Web Animations API,将所有动画的计算和运行卸载到浏览器。这种方法允许浏览器优化动画并确保其平滑执行。在不支持 Web Animations 的情况下,Ionic Animations 将回退到 CSS Animations,性能差异可以忽略不计。
安装
- JavaScript
- TypeScript
- Angular
- React
- Vue
使用 Ionic Core 和 JavaScript 的开发者应安装最新版本的 @ionic/core。
import { createAnimation } from 'https://cdn.jsdelivr.net/npm/@ionic/core@latest/dist/esm/index.mjs';
...
const animation = createAnimation()
.addElement(myElementRef)
.duration(1000)
.fromTo('opacity', '1', '0.5');
}
使用 Ionic Core 和 TypeScript 的开发者应安装最新版本的 @ionic/core。
import { createAnimation, Animation } from '@ionic/core';
...
const animation: Animation = createAnimation('')
.addElement(myElementRef)
.duration(1000)
.fromTo('opacity', '1', '0.5');
}
使用 Angular 的开发者应安装最新版本的 @ionic/angular。动画可以通过 AnimationController 依赖注入来创建。
import { Animation, AnimationController } from '@ionic/angular';
...
constructor(private animationCtrl: AnimationController) {
const animation: Animation = this.animationCtrl.create()
.addElement(myElementRef)
.duration(1000)
.fromTo('opacity', '1', '0.5');
}
使用 React 的开发者应安装最新版本 的 @ionic/react。React 包装器处于测试阶段。请在 GitHub 上报告任何问题!
import { CreateAnimation, Animation } from '@ionic/react';
...
<CreateAnimation
duration={1000}
fromTo={{
property: 'opacity',
fromValue: '1',
toValue: '0.5'
}}
>
...
</CreateAnimation>
使用 Ionic Vue 的开发者应安装最新版本的 @ionic/vue。
import { createAnimation } from '@ionic/vue';
import { ref } from 'vue';
...
const myElementRef = ref();
...
const animation = createAnimation()
.addElement(myElementRef.value)
.duration(1000)
.fromTo('opacity', '1', '0.5');
}
基本动画
用法
- JavaScript
- Angular
- React
- Vue
createAnimation()
.addElement(document.querySelector('.square'))
.duration(1500)
.iterations(Infinity)
.fromTo('transform', 'translateX(0px)', 'translateX(100px)')
.fromTo('opacity', '1', '0.2');
this.animationCtrl.create()
.addElement(document.querySelector('.square'))
.duration(1500)
.iterations(Infinity)
.fromTo('transform', 'translateX(0px)', 'translateX(100px)')
.fromTo('opacity', '1', '0.2');
<CreateAnimation
duration={1500}
iterations={Infinity}
fromTo={[
{ property: 'transform', fromValue: 'translateX(0px)', toValue: 'translateX(100px)' },
{ property: 'opacity', fromValue: '1', toValue: '0.2' }
]}
>
...
</CreateAnimation>
import { createAnimation } from '@ionic/vue';
import { ref } from 'vue';
...
const elementRef = ref();
...
createAnimation()
.addElement(elementRef.value)
.duration(1500)
.iterations(Infinity)
.fromTo('transform', 'translateX(0px)', 'translateX(100px)')
.fromTo('opacity', '1', '0.2');
在上面的示例中,创建了一个动画,该动画会改变 .square 元素的不透明度并使其沿 X 轴从左向右移动。此动画将无限次运行,每次迭代持续 1500ms。
默认情况下,所有 Ionic Animations 都处于暂停状态,直到调用 play 方法。
关键帧动画
Ionic Animations 允许您使用关键帧控制动画中的中间步骤。这里可以使用任何有效的 CSS 属性,您甚至可以使用 CSS 变量作为值。
在编写关键帧时,连字符形式的 CSS 属性应使用驼峰形式书写。例如,border-radius 应写为 borderRadius。这也适用于 fromTo()、from() 和 to() 方法。
用法
- JavaScript
- Angular
- React
- Vue
createAnimation()
.addElement(document.querySelector('.square'))
.duration(3000)
.iterations(Infinity)
.keyframes([
{ offset: 0, background: 'red' },
{ offset: 0.72, background: 'var(--background)' },
{ offset: 1, background: 'green' }
]);
this.animationCtrl.create()
.addElement(this.square.nativeElement)
.duration(3000)
.iterations(Infinity)
.keyframes([
{ offset: 0, background: 'red' },
{ offset: 0.72, background: 'var(--background)' },
{ offset: 1, background: 'green' }
]);
<CreateAnimation
duration={3000}
iterations={Infinity}
keyframes={[
{ offset: 0, background: 'red' },
{ offset: 0.72, background: 'var(--background)' },
{ offset: 1, background: 'green' }
]}
>
...
</CreateAnimation>
import { createAnimation } from '@ionic/vue';
import { ref } from 'vue';
...
const squareRef = ref();
...
createAnimation()
.addElement(squareRef.value)
.duration(3000)
.iterations(Infinity)
.keyframes([
{ offset: 0, background: 'red' },
{ offset: 0.72, background: 'var(--background)' },
{ offset: 1, background: 'green' }
]);
在上面的示例中,.square 元素将从红色背景过渡到由 --background 变量定义的背景色,然后过 渡到绿色背景。
每个关键帧对象都包含一个 offset 属性。offset 是 0 到 1 之间的值,用于定义关键帧步骤。偏移值必须按升序排列且不能重复。
分组动画
多个元素可以同时进行动画,并通过单个父动画对象进行控制。除非另有指定,子动画会继承持续时间、缓动和迭代次数等属性。父动画的 onFinish 回调在所有子动画完成之前不会被调用。
用法
- JavaScript
- Angular
- React
- Vue
const squareA = createAnimation()
.addElement(document.querySelector('.square-a'))
.keyframes([
{ offset: 0, transform: 'scale(1) rotate(0)' },
{ offset: 0.5, transform: 'scale(1.2) rotate(45deg)' },
{ offset: 1, transform: 'scale(1) rotate(45deg)' }
]);
const squareB = createAnimation()
.addElement(document.querySelector('.square-b'))
.keyframes([
{ offset: 0, transform: 'scale(1))', opacity: '1' },
{ offset: 0.5, transform: 'scale(1.2)', opacity: '0.3' },
{ offset: 1, transform: 'scale(1)', opacity: '1' }
]);
const squareC = createAnimation()
.addElement(document.querySelector('.square-c'))
.duration(5000)
.keyframes([
{ offset: 0, transform: 'scale(1))', opacity: '0.5' },
{ offset: 0.5, transform: 'scale(0.8)', opacity: '1' },
{ offset: 1, transform: 'scale(1)', opacity: '0.5' }
]);
const parent = createAnimation()
.duration(2000)
.iterations(Infinity)
.addAnimation([squareA, squareB, squareC]);
const squareA = this.animationCtrl.create()
.addElement(this.squareA.nativeElement)
.keyframes([
{ offset: 0, transform: 'scale(1) rotate(0)' },
{ offset: 0.5, transform: 'scale(1.2) rotate(45deg)' },
{ offset: 1, transform: 'scale(1) rotate(45deg)' }
]);
const squareB = this.animationCtrl.create()
.addElement(this.squareB.nativeElement)
.keyframes([
{ offset: 0, transform: 'scale(1))', opacity: '1' },
{ offset: 0.5, transform: 'scale(1.2)', opacity: '0.3' },
{ offset: 1, transform: 'scale(1)', opacity: '1' }
]);
const squareC = this.animationCtrl.create()
.addElement(this.squareC.nativeElement)
.duration(5000)
.keyframes([
{ offset: 0, transform: 'scale(1))', opacity: '0.5' },
{ offset: 0.5, transform: 'scale(0.8)', opacity: '1' },
{ offset: 1, transform: 'scale(1)', opacity: '0.5' }
]);
const parent = this.animationCtrl.create()
.duration(2000)
.iterations(Infinity)
.addAnimation([squareA, squareB, squareC]);
private parentRef: React.RefObject<CreateAnimation> = React.createRef();
private squareARef: React.RefObject<CreateAnimation> = React.createRef();
private squareBRef: React.RefObject<CreateAnimation> = React.createRef();
private squareCRef: React.RefObject<CreateAnimation> = React.createRef();
...
componentDidMount() {
const parent = this.parentRef.current!.animation;
const squareA = this.squareARef.current!.animation;
const squareB = this.squareBRef.current!.animation;
const squareC = this.squareCRef.current!.animation;
parent.addAnimation([squareA, squareB, squareC]);
}
render() {
return (
<>
<CreateAnimation
ref={this.parentRef}
duration={2000}
iterations={Infinity}
></CreateAnimation>
<CreateAnimation
ref={this.squareARef}
keyframes={[
{ offset: 0, transform: 'scale(1) rotate(0)' },
{ offset: 0.5, transform: 'scale(1.2) rotate(45deg)' },
{ offset: 1, transform: 'scale(1) rotate(0deg)' }
]}
>
<div className="square"></div>
</CreateAnimation>
<CreateAnimation
ref={this.squareBRef}
keyframes={[
{ offset: 0, transform: 'scale(1)', opacity: '1' },
{ offset: 0.5, transform: 'scale(1.2)', opacity: '0.3' },
{ offset: 1, transform: 'scale(1)', opacity: '1' }
]}
>
<div className="square"></div>
</CreateAnimation>
<CreateAnimation
ref={this.squareCRef}
duration={5000}
keyframes={[
{ offset: 0, transform: 'scale(1)', opacity: '0.5' },
{ offset: 0.5, transform: 'scale(0.8)', opacity: '1' },
{ offset: 1, transform: 'scale(1)', opacity: '0.5' }
]}
>
<div className="square"></div>
</CreateAnimation>
</>
)
}
import { createAnimation } from '@ionic/vue';
import { ref } from 'vue';
...
const squareARef = ref();
const squareBRef = ref();
const squareCRef = ref();
...
const squareA = createAnimation()
.addElement(squareARef.value)
.keyframes([
{ offset: 0, transform: 'scale(1) rotate(0)' },
{ offset: 0.5, transform: 'scale(1.2) rotate(45deg)' },
{ offset: 1, transform: 'scale(1) rotate(45deg)' }
]);
const squareB = createAnimation()
.addElement(squareBRef.value)
.keyframes([
{ offset: 0, transform: 'scale(1))', opacity: '1' },
{ offset: 0.5, transform: 'scale(1.2)', opacity: '0.3' },
{ offset: 1, transform: 'scale(1)', opacity: '1' }
]);
const squareC = createAnimation()
.addElement(squareCRef.value)
.duration(5000)
.keyframes([
{ offset: 0, transform: 'scale(1))', opacity: '0.5' },
{ offset: 0.5, transform: 'scale(0.8)', opacity: '1' },
{ offset: 1, transform: 'scale(1)', opacity: '0.5' }
]);
const parent = createAnimation()
.duration(2000)
.iterations(Infinity)
.addAnimation([squareA, squareB, squareC]);
此示例展示了由单个父动画控制的 3 个子动画。动画 squareA 和 squareB 继承了父动画的 2000ms 持续时间,但动画 squareC 由于显式设置了持续时间,所以为 5000ms。
前后钩子
Ionic Animations 提供了钩子,让您可以在动画运行之前和动画完成之后改变元素。这些钩子可用于执行 DOM 读写操作,以 及添加或删除类和内联样式。
用法
- JavaScript
- Angular
- React
- Vue
createAnimation()
.addElement(document.querySelector('.square'))
.duration(2000)
.beforeStyles({
opacity: 0.2
})
.afterStyles({
background: 'rgba(0, 255, 0, 0.5)'
})
.afterClearStyles(['opacity'])
.keyframes([
{ offset: 0, transform: 'scale(1)' },
{ offset: 0.5, transform: 'scale(1.5)' },
{ offset: 1, transform: 'scale(1)' }
])
this.animationCtrl.create()
.addElement(this.square.nativeElement)
.duration(2000)
.beforeStyles({
opacity: 0.2
})
.afterStyles({
background: 'rgba(0, 255, 0, 0.5)'
})
.afterClearStyles(['opacity'])
.keyframes([
{ offset: 0, transform: 'scale(1)' },
{ offset: 0.5, transform: 'scale(1.5)' },
{ offset: 1, transform: 'scale(1)' }
])
<CreateAnimation
duration={2000}
beforeStyles={{
opacity: 0.2
}}
afterStyles={{
background: 'rgba(0, 255, 0, 0.5)'
}}
afterClearStyles={['opacity']}
keyframes={[
{ offset: 0, transform: 'scale(1)' },
{ offset: 0.5, transform: 'scale(1.5)' },
{ offset: 1, transform: 'scale(1)' }
]}
>
...
</CreateAnimation>
import { createAnimation } from '@ionic/vue';
import { ref } from 'vue';
...
const squareRef = ref();
...
createAnimation()
.addElement(squareRef.value)
.duration(2000)
.beforeStyles({
opacity: 0.2
})
.afterStyles({
background: 'rgba(0, 255, 0, 0.5)'
})
.afterClearStyles(['opacity'])
.keyframes([
{ offset: 0, transform: 'scale(1)' },
{ offset: 0.5, transform: 'scale(1.5)' },
{ offset: 1, transform: 'scale(1)' }
])
在此示例中,在动画开始之前,为 .square 元素设置了 0.2 的内联不透明度。动画完成后,元素的背景色被设置为 rgba(0, 255, 0, 0.5),并且内联不透明度被清除。
参见方法部分获取完整的钩子列表。
链式动画
动画可以链接起来依次运行。play 方法返回一个 Promise,该 Promise 在动画完成时解析。
用法
- JavaScript
- Angular
- React
- Vue
const squareA = createAnimation()
.addElement(document.querySelector('.square-a'))
.fill('none')
.duration(1000)
.keyframes([
{ offset: 0, transform: 'scale(1) rotate(0)' },
{ offset: 0.5, transform: 'scale(1.2) rotate(45deg)' },
{ offset: 1, transform: 'scale(1) rotate(0)' }
]);
const squareB = createAnimation()
.addElement(document.querySelector('.square-b'))
.fill('none')
.duration(1000)
.keyframes([
{ offset: 0, transform: 'scale(1)', opacity: '1' },
{ offset: 0.5, transform: 'scale(1.2)', opacity: '0.3' },
{ offset: 1, transform: 'scale(1)', opacity: '1' }
]);
const squareC = createAnimation()
.addElement(document.querySelector('.square-c'))
.fill('none')
.duration(1000)
.keyframes([
{ offset: 0, transform: 'scale(1)', opacity: '0.5' },
{ offset: 0.5, transform: 'scale(0.8)', opacity: '1' },
{ offset: 1, transform: 'scale(1)', opacity: '0.5' }
]);
await squareA.play();
await squareB.play();
await squareC.play();
const squareA = this.animationCtrl.create()
.addElement(this.squareA.nativeElement)
.fill('none')
.duration(1000)
.keyframes([
{ offset: 0, transform: 'scale(1) rotate(0)' },
{ offset: 0.5, transform: 'scale(1.2) rotate(45deg)' },
{ offset: 1, transform: 'scale(1) rotate(0)' }
]);
const squareB = this.animationCtrl.create()
.addElement(this.squareB.nativeElement)
.fill('none')
.duration(1000)
.keyframes([
{ offset: 0, transform: 'scale(1)', opacity: '1' },
{ offset: 0.5, transform: 'scale(1.2)', opacity: '0.3' },
{ offset: 1, transform: 'scale(1)', opacity: '1' }
]);
const squareC = this.animationCtrl.create()
.addElement(this.squareC.nativeElement)
.fill('none')
.duration(1000)
.keyframes([
{ offset: 0, transform: 'scale(1)', opacity: '0.5' },
{ offset: 0.5, transform: 'scale(0.8)', opacity: '1' },
{ offset: 1, transform: 'scale(1)', opacity: '0.5' }
]);
await squareA.play();
await squareB.play();
await squareC.play();
private squareARef: React.RefObject<CreateAnimation> = React.createRef();
private squareBRef: React.RefObject<CreateAnimation> = React.createRef();
private squareCRef: React.RefObject<CreateAnimation> = React.createRef();
...
async componentDidMount() {
const squareA = this.squareARef.current!.animation;
const squareB = this.squareBRef.current!.animation;
const squareC = this.squareCRef.current!.animation;
await squareA.play();
await squareB.play();
await squareC.play();
}
render() {
return (
<>
<CreateAnimation
ref={this.squareARef}
fill="none"
duration={1000}
keyframes={[
{ offset: 0, transform: 'scale(1) rotate(0)' },
{ offset: 0.5, transform: 'scale(1.2) rotate(45deg)' },
{ offset: 1, transform: 'scale(1) rotate(0deg)' }
]}
>
<div className="square"></div>
</CreateAnimation>
<CreateAnimation
ref={this.squareBRef}
fill="none"
duration={1000}
keyframes={[
{ offset: 0, transform: 'scale(1)', opacity: '1' },
{ offset: 0.5, transform: 'scale(1.2)', opacity: '0.3' },
{ offset: 1, transform: 'scale(1)', opacity: '1' }
]}
>
<div className="square"></div>
</CreateAnimation>
<CreateAnimation
ref={this.squareCRef}
fill="none"
duration={1000}
keyframes={[
{ offset: 0, transform: 'scale(1)', opacity: '0.5' },
{ offset: 0.5, transform: 'scale(0.8)', opacity: '1' },
{ offset: 1, transform: 'scale(1)', opacity: '0.5' }
]}
>
<div className="square"></div>
</CreateAnimation>
</>
)
}
import { createAnimation } from '@ionic/vue';
import { ref } from 'vue';
...
const squareARef = ref();
const squareBRef = ref();
const squareCRef = ref();
...
const squareA = createAnimation()
.addElement(squareARef.value)
.fill('none')
.duration(1000)
.keyframes([
{ offset: 0, transform: 'scale(1) rotate(0)' },
{ offset: 0.5, transform: 'scale(1.2) rotate(45deg)' },
{ offset: 1, transform: 'scale(1) rotate(0)' }
]);
const squareB = createAnimation()
.addElement(squareBRef.value)
.fill('none')
.duration(1000)
.keyframes([
{ offset: 0, transform: 'scale(1)', opacity: '1' },
{ offset: 0.5, transform: 'scale(1.2)', opacity: '0.3' },
{ offset: 1, transform: 'scale(1)', opacity: '1' }
]);
const squareC = createAnimation()
.addElement(squareCRef.value)
.fill('none')
.duration(1000)
.keyframes([
{ offset: 0, transform: 'scale(1)', opacity: '0.5' },
{ offset: 0.5, transform: 'scale(0.8)', opacity: '1' },
{ offset: 1, transform: 'scale(1)', opacity: '0.5' }
]);
await squareA.play();
await squareB.play();
await squareC.play();
手势动画
Ionic Animations 使开发者能够通过与 Ionic Gestures 无缝集成来创建强大的基于手势的动画。
用法
- JavaScript
- Angular
- React
- Vue
let initialStep = 0;
let started = false;
const square = document.querySelector('.square');
const MAX_TRANSLATE = 400;
const animation = createAnimation()
.addElement(square)
.duration(1000)
.fromTo('transform', 'translateX(0)', `translateX(${MAX_TRANSLATE}px)`);
const gesture = createGesture({
el: square,
threshold: 0,
gestureName: 'square-drag',
onMove: ev => onMove(ev),
onEnd: ev => onEnd(ev)
})
gesture.enable(true);
const onMove = (ev): {
if (!started) {
animation.progressStart();
started = true;
}
animation.progressStep(getStep(ev));
}
const onEnd = (ev): {
if (!started) { return; }
gesture.enable(false);
const step = getStep(ev);
const shouldComplete = step > 0.5;
animation
.progressEnd((shouldComplete) ? 1 : 0, step)
.onFinish((): { gesture.enable(true); });
initialStep = (shouldComplete) ? MAX_TRANSLATE : 0;
started = false;
}
const clamp = (min, n, max): {
return Math.max(min, Math.min(n, max));
};
const getStep = (ev): {
const delta = initialStep + ev.deltaX;
return clamp(0, delta / MAX_TRANSLATE, 1);
}
private animation?: Animation;
private gesture?: Gesture;
private started: boolean = false;
private initialStep: number = 0;
private MAX_TRANSLATE: number = 400;
ngOnInit() {
this.animation = this.animationCtrl.create()
.addElement(this.square.nativeElement)
.duration(1000)
.fromTo('transform', 'translateX(0)', `translateX(${this.MAX_TRANSLATE}px)`);
this.gesture = this.gestureCtrl.create({
el: this.square.nativeElement,
threshold: 0,
gestureName: 'square-drag',
onMove: ev => this.onMove(ev),
onEnd: ev => this.onEnd(ev)
})
this.gesture.enable(true);
}
private onMove(ev) {
if (!started) {
this.animation.progressStart();
this.started = true;
}
this.animation.progressStep(this.getStep(ev));
}
private onEnd(ev) {
if (!this.started) { return; }
this.gesture.enable(false);
const step = this.getStep(ev);
const shouldComplete = step > 0.5;
this.animation
.progressEnd((shouldComplete) ? 1 : 0, step)
.onFinish((): { this.gesture.enable(true); });
this.initialStep = (shouldComplete) ? this.MAX_TRANSLATE : 0;
this.started = false;
}
private clamp(min, n, max) {
return Math.max(min, Math.min(n, max));
}
private getStep(ev) {
const delta = this.initialStep + ev.deltaX;
return this.clamp(0, delta / this.MAX_TRANSLATE, 1);
}
import { createGesture, CreateAnimation, Gesture, GestureDetail } from '@ionic/react';
import React from 'react';
const MAX_TRANSLATE = 400;
class MyComponent extends React.Component<{}, any> {
private animation: React.RefObject<CreateAnimation> = React.createRef();
private gesture?: Gesture;
private started: boolean = false;
private initialStep: number = 0;
constructor(props: any) {
super(props);
this.state = {
progressStart: undefined,
progressStep: undefined,
progressEnd: undefined,
onFinish: undefined
};
}
componentDidMount() {
const square = Array.from(this.animation.current!.nodes.values())[0];
this.gesture = createGesture({
el: square,
gestureName: 'square-drag',
threshold: 0,
onMove: ev => this.onMove(ev),
onEnd: ev => this.onEnd(ev)
});
this.gesture.enable(true);
}
private onMove(ev: GestureDetail) {
if (!this.started) {
this.setState({
...this.state,
progressStart: { forceLinearEasing: true }
});
this.started = true;
}
this.setState({
...this.state,
progressStep: { step: this.getStep(ev) }
});
}
private onEnd(ev: GestureDetail) {
if (!this.started) { return; }
this.gesture!.enable(false);
const step = this.getStep(ev);
const shouldComplete = step > 0.5;
this.setState({
...this.state,
progressEnd: { playTo: (shouldComplete) ? 1 : 0, step },
onFinish: { callback: () => {
this.gesture!.enable(true);
this.setState({
progressStart: undefined,
progressStep: undefined,
progressEnd: undefined
})
}, opts: { oneTimeCallback: true }}
});
this.initialStep = (shouldComplete) ? MAX_TRANSLATE : 0;
this.started = false;
}
private getStep(ev: GestureDetail) {
const delta = this.initialStep + ev.deltaX;
return this.clamp(0, delta / MAX_TRANSLATE, 1);
}
private clamp(min: number, n: number, max: number) {
return Math.max(min, Math.min(n, max));
}
render() {
return (
<>
<div className="track">
<CreateAnimation
ref={this.animation}
duration={1000}
progressStart={this.state.progressStart}
progressStep={this.state.progressStep}
progressEnd={this.state.progressEnd}
onFinish={this.state.onFinish}
fromTo={{
property: 'transform',
fromValue: 'translateX(0)',
toValue: `translateX(${MAX_TRANSLATE}px)`
}}>
<div className="square"></div>
</CreateAnimation>
</div>
</>
);
}
}
import { createAnimation, createGesture } from '@ionic/vue';
import { ref } from 'vue';
...
let initialStep = 0;
let started = false;
const squareRef = ref();
const MAX_TRANSLATE = 400;
const animation = createAnimation()
.addElement(squareRef.value)
.duration(1000)
.fromTo('transform', 'translateX(0)', `translateX(${MAX_TRANSLATE}px)`);
const gesture = createGesture({
el: squareRef.value,
threshold: 0,
gestureName: 'square-drag',
onMove: ev => onMove(ev),
onEnd: ev => onEnd(ev)
})
gesture.enable(true);
const onMove = (ev): {
if (!started) {
animation.progressStart();
started = true;
}
animation.progressStep(getStep(ev));
}
const onEnd = (ev): {
if (!started) { return; }
gesture.enable(false);
const step = getStep(ev);
const shouldComplete = step > 0.5;
animation
.progressEnd((shouldComplete) ? 1 : 0, step)
.onFinish((): { gesture.enable(true); });
initialStep = (shouldComplete) ? MAX_TRANSLATE : 0;
started = false;
}
const clamp = (min, n, max): {
return Math.max(min, Math.min(n, max));
};
const getStep = (ev): {
const delta = initialStep + ev.deltaX;
return clamp(0, delta / MAX_TRANSLATE, 1);
}
在此示例中,我们创建了一个轨道,可以沿着它拖动 .square 元素。我们的 animation 对象将负责向左或向右移动 .square 元素,而 gesture 对象将指示 animation 对象向哪个方向移动。
基于偏好的动画
开发者还可以使用 CSS 变量根据用户偏好(如 prefers-reduced-motion 和 prefers-color-scheme)来定制动画。
用法
.square {
width: 100px;
height: 100px;
opacity: 0.5;
background: blue;
margin: 10px;
--background: red;
}
@media (prefers-color-scheme: dark) {
.square {
--background: green;
}
}
- JavaScript
- Angular
- React
- Vue
createAnimation()
.addElement(document.querySelector('.square'))
.duration(1500)
.iterations(Infinity)
.direction('alternate')
.fromTo('background', 'blue', 'var(--background)');
this.animationCtrl.create()
.addElement(this.square.nativeElement)
.duration(1500)
.iterations(Infinity)
.direction('alternate')
.fromTo('background', 'blue', 'var(--background)');
<CreateAnimation
duration={1500}
iterations={Infinity}
direction='alternate'
fromTo={{
property: 'background',
fromValue: 'blue',
toValue: 'var(--background)'
}}
>
<div className="square"></div>
</CreateAnimation>
import { createAnimation } from '@ionic/vue';
import { ref } from 'vue';
...
const squareRef = ref();
...
createAnimation()
.addElement(squareRef.value)
.duration(1500)
.iterations(Infinity)
.direction('alternate')
.fromTo('background', 'blue', 'var(--background)');
此方法在首次创建动画时适用于所有支持的浏览器。大多数浏览器还能够随着 CSS 变量的变化动态更新关键帧动画。
Safari 目前不支持动态更新关键帧动画。对于需要在 Safari 中获得此类支持的开发者,可以使用 MediaQueryList.addListener()。
覆盖 Ionic 组件动画
某些 Ionic 组件允许开发者提供自定义动画。所有动画都作为组件上的属性提供或通过全局配置设置。
模态框
- JavaScript
- Angular
- React
- Vue
customElements.define('modal-page', class extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<ion-header>
<ion-toolbar>
<ion-title>Modal Header</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
Modal Content
</ion-content>
`;
}
});
function presentModal() {
const enterAnimation = (baseEl: any) => {
const root = baseEl.shadowRoot;
const backdropAnimation = createAnimation()
.addElement(root.querySelector('ion-backdrop')!)
.fromTo('opacity', '0.01', 'var(--backdrop-opacity)');
const wrapperAnimation = createAnimation()
.addElement(root.querySelector('.modal-wrapper')!)
.keyframes([
{ offset: 0, opacity: '0', transform: 'scale(0)' },
{ offset: 1, opacity: '0.99', transform: 'scale(1)' }
]);
return createAnimation()
.addElement(baseEl)
.easing('ease-out')
.duration(500)
.addAnimation([backdropAnimation, wrapperAnimation]);
}
const leaveAnimation = (baseEl: any) => {
return enterAnimation(baseEl).direction('reverse');
}
// 使用 `modal-page` 组件创建模态框
const modalElement = document.createElement('ion-modal');
modalElement.component = 'modal-page';
modalElement.enterAnimation = enterAnimation;
modalElement.leaveAnimation = leaveAnimation;
// 呈现模态框
document.body.appendChild(modalElement);
return modalElement.present();
}
import { Component } from '@angular/core';
import { ModalController, AnimationController } from '@ionic/angular';
import { ModalPage } from '../modal/modal.page';
@Component({
selector: 'modal-example',
templateUrl: 'modal-example.html',
styleUrls: ['./modal-example.css']
})
export class ModalExample {
constructor(public modalController: ModalController,
public animationCtrl: AnimationController) { }
async presentModal() {
const enterAnimation = (baseEl: any) => {
const root = baseEl.shadowRoot;
const backdropAnimation = this.animationCtrl.create()
.addElement(root.querySelector('ion-backdrop')!)
.fromTo('opacity', '0.01', 'var(--backdrop-opacity)');
const wrapperAnimation = this.animationCtrl.create()
.addElement(root.querySelector('.modal-wrapper')!)
.keyframes([
{ offset: 0, opacity: '0', transform: 'scale(0)' },
{ offset: 1, opacity: '0.99', transform: 'scale(1)' }
]);
return this.animationCtrl.create()
.addElement(baseEl)
.easing('ease-out')
.duration(500)
.addAnimation([backdropAnimation, wrapperAnimation]);
}
const leaveAnimation = (baseEl: any) => {
return enterAnimation(baseEl).direction('reverse');
}
const modal = await this.modalController.create({
component: ModalPage,
enterAnimation,
leaveAnimation
});
return await modal.present();
}
}
import React, { useState } from 'react';
import { createAnimation, IonModal, IonButton, IonContent } from '@ionic/react';
export const ModalExample: React.FC = () => {
const [showModal, setShowModal] = useState(false);
const enterAnimation = (baseEl: any) => {
const root = baseEl.shadowRoot;
const backdropAnimation = createAnimation()
.addElement(root.querySelector('ion-backdrop')!)
.fromTo('opacity', '0.01', 'var(--backdrop-opacity)');
const wrapperAnimation = createAnimation()
.addElement(root.querySelector('.modal-wrapper')!)
.keyframes([
{ offset: 0, opacity: '0', transform: 'scale(0)' },
{ offset: 1, opacity: '0.99', transform: 'scale(1)' }
]);
return createAnimation()
.addElement(baseEl)
.easing('ease-out')
.duration(500)
.addAnimation([backdropAnimation, wrapperAnimation]);
}
const leaveAnimation = (baseEl: any) => {
return enterAnimation(baseEl).direction('reverse');
}
return (
<IonContent>
<IonModal isOpen={showModal} enterAnimation={enterAnimation} leaveAnimation={leaveAnimation}>
<p>This is modal content</p>
<IonButton onClick={() => setShowModal(false)}>Close Modal</IonButton>
</IonModal>
<IonButton onClick={() => setShowModal(true)}>Show Modal</IonButton>
</IonContent>
);
};
<template>
<ion-page>
<ion-content>
<ion-modal
:is-open="isModalOpen"
:enter-animation="enterAnimation"
:leave-animation="leaveAnimation"
@didDismiss="setModalOpen(false)"
>
Modal content goes here.
</ion-modal>
<ion-button @click="setModalOpen(true)">Show Modal</ion-button>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { createAnimation, IonButton, IonContent, IonModal, IonPage } from '@ionic/vue';
import { defineComponent, ref } from 'vue';
export default defineComponent({
components: { IonButton, IonContent, IonModal, IonPage },
setup() {
const isModalOpen = ref(false);
const setModalOpen = (state) => isModalOpen.value = state;
const enterAnimation = (baseEl: any) => {
const root = baseEl.shadowRoot;
const backdropAnimation = createAnimation()
.addElement(root.querySelector('ion-backdrop')!)
.fromTo('opacity', '0.01', 'var(--backdrop-opacity)');
const wrapperAnimation = createAnimation()
.addElement(root.querySelector('.modal-wrapper')!)
.keyframes([
{ offset: 0, opacity: '0', transform: 'scale(0)' },
{ offset: 1, opacity: '0.99', transform: 'scale(1)' }
]);
return createAnimation()
.addElement(baseEl)
.easing('ease-out')
.duration(500)
.addAnimation([backdropAnimation, wrapperAnimation]);
}
const leaveAnimation = (baseEl: any) => {
return enterAnimation(baseEl).direction('reverse');
}
return { isModalOpen, setModalOpen, enterAnimation, leaveAnimation }
}
})
</script>
性能考虑
CSS 和 Web 动画通常在合成线程上处理。这不同于执行布局、绘制、样式和 JavaScript 的主 线程。建议优先使用可以在合成线程上处理的属性,以获得最佳动画性能。
对诸如 height 和 width 等属性进行动画会导致额外的布局和绘制,这可能导致卡顿并降低动画性能。另一方面,对诸如 transform 和 opacity 等属性进行动画可以由浏览器高度优化,通常不会引起太多卡顿。
有关哪些 CSS 属性会导致布局或绘制的信息,请参阅 CSS Triggers。
调试
要在 Chrome 中调试动画,有一篇关于使用 Chrome DevTools 检查动画的优秀博客文章:https://developers.google.com/web/tools/chrome-devtools/inspect-styles/animations。
还建议为您的动画分配唯一的标识符。这些标识符将显示在 Chrome 的动画检查器中,并使调试更容易:
/**
* .square 元素的动画应在 Chrome DevTools 中
* 显示 "my-animation-identifier"。
*/
const animation = createAnimation('my-animation-identifier')
.addElement(document.querySelector('.square'))
.duration(1000)
.fromTo('opacity', '1', '0');
API
本节提供了 Animation 类上可用的所有方法和属性的列表。
接口
AnimationDirection
type AnimationDirection = 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
AnimationFill
type AnimationFill = 'auto' | 'none' | 'forwards' | 'backwards' | 'both';
AnimationBuilder
type AnimationBuilder = (baseEl: any, opts?: any) => Animation;
opts 是特定于自定义动画的附加选项。例如,sheet 模态框的进入动画包含当前断点的信息。
AnimationCallbackOptions
interface AnimationCallbackOptions {
/**
* 如果为 true,相关回调将只触发一次。
*/
oneTimeCallback: boolean;
}
AnimationPlayOptions
interface AnimationPlayOptions {
/**
* 如果为 true,动画将同步播放。
* 这相当于以 0ms 的持续时间运行动画。
*/
sync: boolean;
}
属性
| 名称 | 描述 |
|---|---|
childAnimations: Animation[] | 给定父动画的所有子动画。 |
elements: HTMLElement[] | 附加到动画的所有元素。 |
parentAnimation?: Animation | 给定动画对象的父动画。 |
方法
| 名称 | 描述 |
|---|---|
addAnimation(animationToAdd: Animation | Animation[]): Animation | 将一个或多个动画组合在一起,由父动画控制。 |
addElement(el: Element | Element[] | Node | Node[] | NodeList): Animation | 向动画添加一个或多个元素。 |
afterAddClass(className: string | string[]): Animation | 添加一个类或类数组,在动画结束后添加到动画中的所有元素。 |
afterAddRead(readFn: (): void): Animation | 添加一个执行 DOM 读取的函数,在动画结束后运行。 |
afterAddWrite(writeFn: (): void): Animation | 添加一个执行 DOM 写入的函数,在动画结束后运行。 |
afterClearStyles(propertyNames: string[]): Animation | 添加属性名称数组,在动画结束后从动画中所有元素的内联样式中清除。 |
afterRemoveClass(className: string | string[]): Animation | 添加一个类或类数组,在动画结束后从动画中的所有元素中移除。 |
afterStyles(styles: { [property: string]: any }): Animation | 添加样式对象,在动画结束后应用于动画中的所有元素。 |
beforeAddClass(className: string | string[]): Animation | 添加一个类或类数组,在动画开始前添加到动画中的所有元素。 |
beforeAddRead(readFn: (): void): Animation | 添加一个执行 DOM 读取的函数,在动画开始前运行。 |
beforeAddWrite(writeFn: (): void): Animation | 添加一个执行 DOM 写入的函数,在动画开始前运行。 |
beforeClearStyles(propertyNames: string[]): Animation | 添加属性名称数组,在动画开始前从动画中所有元素的内联样式中清除。 |
beforeRemoveClass(className: string | string[]): Animation | 添加一个类或类数组,在动画开始前从动画中的所有元素中移除。 |
beforeStyles(styles: { [property: string]: any }): Animation | 添加样式对象,在动画开始前应用于动画中的所有元素。 |
direction(direction?: AnimationDirection): Animation | 设置动画播放的方向。 |
delay(delay?: number): Animation | 设置动画开始的延迟时间(毫秒)。 |
destroy(clearStyleSheets?: boolean): Animation | 销毁动画并清除所有元素、子动画和关键帧。 |
duration(duration?: number): Animation | 设置动画的持续时间(毫秒)。 |
easing(easing?: string): Animation | 设置动画的缓动效果。参见 Easing Effects 获取可接受的缓动值列表。 |
from(property: string, value: any): Animation | 设置动画的起始样式。 |
fromTo(property: string, fromValue: any, toValue: any): Animation | 设置动画的起始和结束样式。 |
fill(fill?: AnimationFill): Animation | 设置动画在执行前后如何将样式应用于其元素。 |
iterations(iterations: number): Animation | 设置动画循环在停止前应播放的次数。 |
keyframes(keyframes: any[]): Animation | 设置动画的关键帧。 |
onFinish(callback: (didComplete: boolean, animation: Animation): void, opts?: AnimationCallbackOptions): Animation | 添加动画结束时运行的回调。 |
pause(): Animation | 暂停动画。 |
play(opts?: AnimationPlayOptions): Promise<void> | 播放动画。 |
progressEnd(playTo?: 0 | 1, step: number, dur?: number): Animation | 停止在动画中搜索。 |
progressStart(forceLinearEasing?: boolean, step?: number): Animation | 开始在动画中搜索。 |
progressStep(step: number): Animation | 在动画中搜索。 |
stop(): Animation | 停止动画并将所有元素重置为初始状态。 |
to(property: string, value: any): Animation | 设置动画的结束样式。 |