Skip to content

Commit

Permalink
feat: 进入视图才执行动画 (#11342)
Browse files Browse the repository at this point in the history
* feat: 进入视图才执行动画

* 入场动画支持配置重复

* 优化

* bugfxi

* 样式微调
  • Loading branch information
qkiroc authored Dec 10, 2024
1 parent ed7d39c commit a50fdf9
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 88 deletions.
90 changes: 8 additions & 82 deletions packages/amis-core/src/SchemaRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ import {buildStyle} from './utils/style';
import {isExpression} from './utils/formula';
import {StatusScopedProps} from './StatusScoped';
import {evalExpression, filter} from './utils/tpl';
import {CSSTransition} from 'react-transition-group';
import {createAnimationStyle} from './utils/animations';
import styleManager from './StyleManager';
import Animations from './components/Animations';

interface SchemaRendererProps
extends Partial<Omit<RendererProps, 'statusStore'>>,
Expand Down Expand Up @@ -103,49 +101,19 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
schema: any;
path: string;

animationTimeout: {
enter?: number;
exit?: number;
} = {};
animationClassNames: {
appear?: string;
enter?: string;
exit?: string;
} = {};

toDispose: Array<() => any> = [];
unbindEvent: (() => void) | undefined = undefined;
unbindGlobalEvent: (() => void) | undefined = undefined;
isStatic: any = undefined;

constructor(props: SchemaRendererProps) {
super(props);
const animations = props?.schema?.animations;
if (animations) {
let id = props?.schema.id;
id = formateId(id);
if (animations.enter) {
this.animationTimeout.enter =
((animations.enter.duration || 1) + (animations.enter.delay || 0)) *
1000;
this.animationClassNames.enter = `${animations.enter.type}-${id}-enter`;
this.animationClassNames.appear = this.animationClassNames.enter;
}
if (animations.exit) {
this.animationTimeout.exit =
((animations.exit.duration || 1) + (animations.exit.delay || 0)) *
1000;
this.animationClassNames.exit = `${animations.exit.type}-${id}-exit`;
}
}

this.refFn = this.refFn.bind(this);
this.renderChild = this.renderChild.bind(this);
this.reRender = this.reRender.bind(this);
this.resolveRenderer(this.props);
this.dispatchEvent = this.dispatchEvent.bind(this);
this.addAnimationAttention = this.addAnimationAttention.bind(this);
this.removeAnimationAttention = this.removeAnimationAttention.bind(this);

// 监听statusStore更新
this.toDispose.push(
Expand All @@ -169,20 +137,11 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
);
}

componentDidMount(): void {
if (this.props.schema.animations) {
let {animations, id} = this.props.schema;
id = formateId(id);
createAnimationStyle(id, animations);
}
}

componentWillUnmount() {
this.toDispose.forEach(fn => fn());
this.toDispose = [];
this.unbindEvent?.();
this.unbindGlobalEvent?.();
this.removeAnimationStyle();
}

// 限制:只有 schema 除外的 props 变化,或者 schema 里面的某个成员值发生变化才更新。
Expand Down Expand Up @@ -213,14 +172,6 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
return false;
}

removeAnimationStyle() {
if (this.props.schema.animations) {
let {id} = this.props.schema;
id = formateId(id);
styleManager.removeStyles(id);
}
}

resolveRenderer(props: SchemaRendererProps, force = false): any {
let schema = props.schema;
let path = props.$path;
Expand Down Expand Up @@ -367,25 +318,6 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
this.forceUpdate();
}

addAnimationAttention(node: HTMLElement) {
const {schema} = this.props || {};
const {attention} = schema?.animations || {};
if (attention) {
let {id} = schema;
id = formateId(id);
node.classList.add(`${attention.type}-${id}-attention`);
}
}
removeAnimationAttention(node: HTMLElement) {
const {schema} = this.props || {};
const {attention} = schema?.animations || {};
if (attention) {
let {id} = schema;
id = formateId(id);
node.classList.remove(`${attention.type}-${id}-attention`);
}
}

render(): JSX.Element | null {
let {
$path: _,
Expand Down Expand Up @@ -536,7 +468,7 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
} = schema;
const Component = renderer.component!;

let animationIn = true;
let animationShow = true;

// 原来表单项的 visible: false 和 hidden: true 表单项的值和验证是有效的
// 而 visibleOn 和 hiddenOn 是无效的,
Expand All @@ -550,7 +482,7 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
(schema.visible !== false && !schema.hidden))
) {
if (schema.animations) {
animationIn = false;
animationShow = false;
} else {
return null;
}
Expand Down Expand Up @@ -629,17 +561,11 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {

if (schema.animations) {
component = (
<CSSTransition
in={animationIn}
timeout={this.animationTimeout}
classNames={this.animationClassNames}
onEntered={this.addAnimationAttention}
onExit={this.removeAnimationAttention}
appear
unmountOnExit
>
{component}
</CSSTransition>
<Animations
schema={schema}
component={component}
show={animationShow}
/>
);
}

Expand Down
158 changes: 158 additions & 0 deletions packages/amis-core/src/components/Animations.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import React, {useEffect, useMemo, useState, useCallback} from 'react';
import {CSSTransition} from 'react-transition-group';
import {Schema} from '../types';
import {formateId} from '../utils';
import {createAnimationStyle} from '../utils/animations';
import styleManager from '../StyleManager';

function Animations({
schema,
component,
show
}: {
schema: Schema;
component: any;
show: boolean;
}) {
const {enter} = schema.animations || {};
const [animationShow, setAnimationShow] = useState(!enter?.inView);
const [placeholderShow, setPlaceholderShow] = useState(!!enter?.inView);

const id = useMemo(() => formateId(schema.id), []);
const observer = useMemo(newObserver, []);
const animationClassNames = useMemo(initAnimationClassNames, []);
const animationTimeout = useMemo(initAnimationTimeout, []);

useEffect(() => {
createAnimationStyle(id, schema.animations!);
return () => {
if (schema.animations) {
styleManager.removeStyles(id);
}
observer.disconnect();
};
}, []);

function newObserver() {
return new IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
if (
entry.target.getAttribute('data-role') === 'animation-placeholder'
) {
if (entry.isIntersecting) {
setAnimationShow(true);
setPlaceholderShow(false);
observer.unobserve(entry.target);
}
} else {
if (!entry.isIntersecting) {
setAnimationShow(false);
observer.unobserve(entry.target);
}
}
});
},
{
root: null,
rootMargin: '0px',
threshold: 0.1
}
);
}

function initAnimationClassNames() {
const animations = schema?.animations;
const animationClassNames = {
appear: '',
enter: '',
exit: ''
};
if (animations) {
if (animations.enter) {
animationClassNames.enter = `${animations.enter.type}-${id}-enter`;
animationClassNames.appear = animationClassNames.enter;
}
if (animations.exit) {
animationClassNames.exit = `${animations.exit.type}-${id}-exit`;
}
}
return animationClassNames;
}

function initAnimationTimeout() {
const animations = schema?.animations;
const animationTimeout = {
enter: 0,
exit: 0
};
if (animations) {
if (animations.enter) {
animationTimeout.enter =
((animations.enter.duration || 1) + (animations.enter.delay || 0)) *
1000;
}
if (animations.exit) {
animationTimeout.exit =
((animations.exit.duration || 1) + (animations.exit.delay || 0)) *
1000;
}
}
return animationTimeout;
}

function refFn(ref: HTMLElement | null) {
if (ref) {
observer.observe(ref);
}
}

const handleEntered = useCallback((node: HTMLElement) => {
const {attention, exit, enter} = schema.animations || {};
if (attention) {
node.classList.add(`${attention.type}-${id}-attention`);
}
if (exit?.outView || enter?.repeat) {
observer.observe(node);
}
}, []);

const handleExit = useCallback((node: HTMLElement) => {
const {attention} = schema.animations || {};
if (attention) {
node.classList.remove(`${attention.type}-${id}-attention`);
}
}, []);

const handleExited = useCallback(() => {
setPlaceholderShow(true);
}, []);

return (
<>
{!animationShow && show && placeholderShow && (
<div
ref={refFn}
className="amis-animation-placeholder"
data-role="animation-placeholder"
>
{component}
</div>
)}
<CSSTransition
in={animationShow && show}
timeout={animationTimeout}
classNames={animationClassNames}
onEntered={handleEntered}
onExit={handleExit}
onExited={handleExited}
appear
unmountOnExit
>
{component}
</CSSTransition>
</>
);
}

export default Animations;
4 changes: 4 additions & 0 deletions packages/amis-core/src/utils/animations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export interface AnimationsProps {
type: string;
duration?: number;
delay?: number;
repeat?: boolean;
inView?: boolean;
};
attention?: {
type: string;
Expand All @@ -16,6 +18,8 @@ export interface AnimationsProps {
type: string;
duration?: number;
delay?: number;
repeat?: boolean;
outView?: boolean;
};
}

Expand Down
3 changes: 3 additions & 0 deletions packages/amis-core/src/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2437,6 +2437,9 @@ export function supportsMjs() {
}

export function formateId(id: string) {
if (!id) {
return guid();
}
// 将className非法字符替换为短横线
id = id.replace(/[^a-zA-Z0-9-]/g, '-');
// 将连续的-替换为单个-
Expand Down
3 changes: 0 additions & 3 deletions packages/amis-editor-core/scss/control/_event-action.scss
Original file line number Diff line number Diff line change
Expand Up @@ -446,9 +446,6 @@
overflow-y: auto;
height: 100%;
padding: 0 8px 0 0;
.event-action-radio {
padding-top: 5px;
}
.action-desc {
margin-left: 16px;
color: var(--Form-item-color);
Expand Down
Loading

0 comments on commit a50fdf9

Please sign in to comment.