Как реализовать обычные анимации в React

React.js анимация
Как реализовать обычные анимации в React

Теперь требования пользователей к фронтенд-страницам уже не удовлетворяются реализацией функций, а также должны быть красивыми и интересными. Помимо красоты общего пользовательского интерфейса, добавление правильных анимационных эффектов в нужных местах часто более выразительно, чем статические страницы, и позволяет добиться более естественного эффекта. Например, простая анимация загрузки или эффект переключения страниц могут не только облегчить пользователю настроение ожидания, но даже незаметно добиться эффекта продвижения бренда за счет использования логотипов брендов и других форм.

React, как популярный в последние годы фреймворк для фронтенд-разработки, предложил концепцию виртуального DOM.Все изменения DOM сначала происходили в виртуальном DOM.Фактические изменения веб-страницы анализировались через DOM diff, а затем отражались на реальном DOM, что значительно улучшило производительность веб-страницы. Однако, с точки зрения реализации анимации, React как фреймворк не обеспечивает напрямую эффекты анимации для компонентов и требует, чтобы разработчики реализовывали их самостоятельно, в то время как большинство традиционных веб-анимаций реализуются путем прямого манипулирования фактическими элементами DOM, что, очевидно, не используется в Реагировать. Итак, как анимация реализована в React?

Суть всех анимаций заключается в постоянном изменении одного или нескольких свойств элемента DOM для создания согласованных изменений в анимации форм. Реализация анимации в React по сути такая же, как и традиционная веб-анимация, но двумя способами: с помощью реализации анимации css3 и с помощью js для изменения свойств элемента. Однако в конкретной реализации, чтобы больше соответствовать характеристикам фреймворка React, его можно разделить на несколько категорий:

  1. Интервальная анимация на основе таймера или запроса ананиацииСрамена (RAF);
  2. Простая анимация на основе css3;
  3. Плагин анимации ReactCssTransitionGroup;
  4. Комбинируйте хук, чтобы реализовать сложную анимацию;
  5. Другие сторонние библиотеки анимации.

1. Интервальная анимация по таймеру или RAF

В самом начале реализация анимации зависит от таймеров.setInterval,setTimeoutилиrequestAnimationFrame(RAF) Напрямую изменять свойства элементов DOM. Разработчики, незнакомые с функциями React, могут по привычке пройти мимоrefилиfindDOMNode()Получите настоящий узел DOM и измените его стиль напрямую. Однако, поrefНе рекомендуется напрямую получать реальный DOM и работать с ним, и этого следует избегать, насколько это возможно.

Следовательно, нам нужно передавать такие методы, как таймеры или RAF, с атрибутами узла DOM.stateСвязаться. Во-первых, вам нужно извлечь атрибуты, связанные с изменяющимся стилем, и заменить их наstate, затем добавьте таймер в соответствующую функцию жизненного цикла илиrequestAnimationFrameПостоянно изменятьstate, запустите обновление компонента для достижения эффекта анимации.

Пример

Возьмите индикатор выполнения в качестве примера, код выглядит следующим образом:

// 使用requestAnimationFrame改变state
import React, { Component } from 'react';

export default class Progress extends Component {
    constructor(props) {
        super(props);
        this.state = {
            percent: 10
        };
    }

    increase = () => {
        const percent = this.state.percent;
        const targetPercent = percent >= 90 ? 100 : percent + 10;
        const speed = (targetPercent - percent) / 400;
        let start = null;
        const animate = timestamp => {
            if (!start) start = timestamp;
            const progress = timestamp - start;
            const currentProgress = Math.min(parseInt(speed * progress + percent, 10), targetPercent);
            this.setState({
                percent: currentProgress
            });
            if (currentProgress < targetPercent) {
                window.requestAnimationFrame(animate);
            }
        };
        window.requestAnimationFrame(animate);
    }

    decrease = () => {
        const percent = this.state.percent;
        const targetPercent = percent < 10 ? 0 : percent - 10;
        const speed = (percent - targetPercent) / 400;
        let start = null;
        const animate = timestamp => {
            if (!start) start = timestamp;
            const progress = timestamp - start;
            const currentProgress = Math.max(parseInt(percent - speed * progress, 10), targetPercent);
            this.setState({
                    percent: currentProgress
                });
            if (currentProgress > targetPercent) {
                window.requestAnimationFrame(animate);
            }
        };
        window.requestAnimationFrame(animate);
    }

    render() {
        const { percent } = this.state;

        return (
            <div>
                <div className="progress">
                    <div className="progress-wrapper" >
                        <div className="progress-inner" style = {{width: `${percent}%`}} ></div>
                    </div>
                    <div className="progress-info" >{percent}%</div>
                </div>
                <div className="btns">
                    <button onClick={this.decrease}>-</button>
                    <button onClick={this.increase}>+</button>
                </div>
            </div>
        );
    }
}

В примере мыincreaseа такжеdecreaseПостроить линейную переходную функцию в функцииanimation,requestAnimationFrameФункция перехода выполняется до того, как браузер каждый раз перерисовывает для расчета текущего индикатора выполнения.widthсвойства и обновитьstate, что вызывает повторную визуализацию индикатора выполнения. Эффект этого примера следующий:

RAF实现进度条效果

Эта реализация используетrequestAnimationFrameПроизводительность хорошая, полностью реализован на чистом js, не зависит от css, при использовании таймера возможны выпадения кадров и зависания. Кроме того, разработчикам необходимо самостоятельно рассчитывать состояние по функции скорости, что является более сложным.

2. Простая анимация на основе css3

когда в css3animationа такжеtransitionПосле появления и популяризации мы можем легко использовать CSS для изменения стиля элементов без расчета стилей в реальном времени вручную.

Пример

Мы по-прежнему берем приведенный выше график в качестве примера, используя CSS3 для достижения прогрессивных динамических эффектов, код выглядит следующим образом:

import React, { Component } from 'react';

export default class Progress extends Component {
    constructor(props) {
        super(props);
        this.state = {
            percent: 10
        };
    }

    increase = () => {
        const percent = this.state.percent + 10;
        this.setState({
            percent: percent > 100 ? 100 : percent,
        })
    }

    decrease = () => {
        const percent = this.state.percent - 10;
        this.setState({
            percent: percent < 0 ? 0 : percent,
        })
    }

    render() {
        // 同上例, 省略
        ....
    }
}
.progress-inner {
  transition: width 400ms cubic-bezier(0.08, 0.82, 0.17, 1);
  // 其他样式同上,省略
  ...
}

В примереincreaseа такжеdecreaseфункция больше не рассчитываетсяwidth, но напрямую задайте увеличенную или уменьшенную ширину. Следует отметить, что стиль css установленtransitionАтрибут, когда изменяется атрибут ширины, автоматически реализуется эффект динамического изменения стиля, и можно установить кривую скорости для различных эффектов скорости. Результат этого примера показан на рисунке ниже.Можно обнаружить, что, в отличие от предыдущего примера, данные о ходе выполнения в правой части напрямую изменяются на целевое число, и нет конкретного процесса изменения, а динамический эффект индикатора выполнения больше не является линейным изменением. , эффект более яркий.

进度条效果

Реализация на основе css3 имеет высокую производительность и небольшое количество кода, но она может полагаться только на эффекты css, и сложно реализовать сложные анимации. Кроме того, модифицируяstateЭффект анимации можно применить только к узлам, которые уже существуют в дереве DOM. Если вы хотите добавить анимацию входа и выхода к компонентам таким образом, вам нужно поддерживать по крайней мере дваstateреализовать анимацию входа и выхода, один изstateИспользуется для управления отображением элемента, другойstateИспользуется для управления изменением свойств элемента во время анимации. В этом случае разработчикам нужно тратить много сил на поддержание анимационной логики компонента, что очень сложно и громоздко.

3. Плагин анимации ReactCssTransitionGroup

Реагировать, используемый для обеспечения анимационных плагинов для разработчиковreact-addons-css-transition-group, а затем переданы сообществу на обслуживание, формируя текущуюreact-transition-group, этот плагин может легко реализовать анимацию входа и выхода компонентов и требует дополнительной установки разработчиком при его использовании.react-transition-groupВключатьCSSTransitionGroupа такжеTransitionGroupДва плагина анимации, последний является базовым API, а первый является дальнейшей инкапсуляцией последнего, который может более удобно реализовывать анимацию CSS.

Пример

В качестве примера возьмем динамически увеличивающуюся вкладку, код выглядит следующим образом:

import React, { Component } from 'react';
import { CSSTransitionGroup } from 'react-transition-group';

let uid = 2;
export default class Tabs extends Component {
    constructor(props) {
        super(props);
        this.state = {
            activeId: 1,
            tabData: [{
                id: 1,
                panel: '选项1'
            }, {
                id: 2,
                panel: '选项2'
            }]
        };
    }

    addTab = () => {
        // 添加tab代码
        ...
    }

    deleteTab = (id) => {
        // 删除tab代码
        ...
    }

    render() {
        const { tabData, activeId } = this.state;

        const renderTabs = () => {
            return tabData.map((item, index) => {
                return (
                    <div
                        className={`tab-item${item.id === activeId ? ' tab-item-active' : ''}`}
                        key={`tab${item.id}`}
                    >
                        {item.panel}
                        <span className="btns btn-delete" onClick={() => this.deleteTab(item.id)}>✕</span>
                    </div>
                );
            })
        }

        return (
            <div>
                <div className="tabs" >
                    <CSSTransitionGroup
                      transitionName="tabs-wrap"
                      transitionEnterTimeout={500}
                      transitionLeaveTimeout={500}
                    >
                      {renderTabs()}
                    </CSSTransitionGroup>
                    <span className="btns btn-add" onClick={this.addTab}>+</span>
                </div>
                <div className="tab-cont">
                    cont
                </div>
            </div>
        );
    }
}
/* tab动态增加动画 */
.tabs-wrap-enter {
  opacity: 0.01;
}

.tabs-wrap-enter.tabs-wrap-enter-active {
  opacity: 1;
  transition: all 500ms ease-in;
}

.tabs-wrap-leave {
  opacity: 1;
}

.tabs-wrap-leave.tabs-wrap-leave-active {
  opacity: 0.01;
  transition: all 500ms ease-in;
}

CSSTransitionGroupВы можете добавить дополнительные классы CSS к его дочерним узлам, а затем анимировать анимацию входа и выхода с помощью анимации CSS. Чтобы анимировать каждый узел вкладки, оберните ихCSSTransitionGroupв компоненте. когда установленоtransitionNameсобственность'tabs-wrapper',transitionEnterTimeoutЧерез 400 мс один разCSSTransitionGroupДобавьте новый узел в новый узел, новый узел будет добавлен с классом css, когда он появится.'tabs-wrapper-enter', который затем добавляется в класс css в следующем кадре'tabs-wrapper-enter-active'. Из-за разных свойств прозрачности и перехода css3, заданных в этих двух классах css, узел реализует входной эффект прозрачности от малого к большому. класс css через 400 мс'tabs-wrapper-enter'а также'tabs-wrapper-enter-active'будет одновременно удален, и узел завершит весь процесс анимации входа. Реализация анимации выхода похожа на анимацию входа, за исключением того, что добавленный класс css называется'tabs-wrapper-leave'а также'tabs-wrapper-leave-active'. Эффект этого примера показан на следующем рисунке:

动态增加tab效果

CSSTransitionGroupПоддерживаются следующие 7 свойств:

Среди них анимация входа и выхода включена по умолчанию и должна быть установлена ​​при использованииtransitionEnterTimeoutа такжеtransitionLeaveTimeout. Примечательно,CSSTransitionGroupОн также обеспечивает анимацию (появление), которую необходимо установить при использованииtransitionAppearTimeout. Итак, в чем разница между анимацией появления и анимацией входа? когда установленоtransitionAppearдляtrueчас,CSSTransitionGroupсуществуетпервый рендер, добавлен этап появления. На этом этапеCSSTransitionGroupСуществующие дочерние узлы класса css будут добавлены последовательно'tabs-wrapper-appear'а также'tabs-wrapper-appear-active', чтобы добиться эффекта анимации. следовательно,Появляться анимация только дляCSSTransitionGroupДочерние узлы, существующие при первом рендеринге,однаждыCSSTransitionGroupПосле завершения рендеринга его дочерние узлы могут иметь только анимацию входа (вход), а анимация (появление) невозможна.

Кроме того, используйтеCSSTransitionGroupНеобходимо отметить следующие моменты:

  • CSSTransitionGroupГенерирует значение по умолчанию в дереве DOMspanтег оборачивает свои дочерние узлы, если вы хотите использовать другие теги html, вы можете установитьCSSTransitionGroupизcomponentАтрибуты;
  • CSSTransitionGroupнеобходимо добавить дочерние элементыkeyКогда значение узла изменяется, он может точно вычислить, какие узлы должны добавить анимацию входа и какие узлы должны добавить анимацию выхода;
  • CSSTransitionGroupЭффект анимации действует только на прямые дочерние узлы, а не на внучатые узлы;
  • Время окончания анимации не зависит от продолжительности перехода в css, а зависит отtransitionEnterTimeout,transitionLeaveTimeout,TransitionAppearTimeoutпревалируют, потому что в некоторых случаях событие transitionend не будет запущено.MDN transitionend.

CSSTransitionGroupПреимущества реализации анимации:

  • Простой и удобный в использовании, вы можете легко и быстро реализовать анимацию входа и выхода элементов;
  • В сочетании с React производительность выше.

CSSTransitionGroupНедостатки тоже очень очевидны:

  • Ограничен анимацией внешнего вида, анимации входа и анимации выхода;
  • Из-за необходимости сформулироватьtransitionName, недостаточно гибкий;
  • Простую анимацию можно получить, только полагаясь на css.

В-четвертых, объедините хук для реализации сложной анимации.

В реальных проектах могут потребоваться еще какие-то крутые анимационные эффекты, которых часто сложно добиться, полагаясь только на css3. На этом этапе мы могли бы также использовать некоторые зрелые сторонние библиотеки, такие как jQuery или GASP, для объединения методов ловушек жизненного цикла и функций ловушек в компонентах React для достижения сложных эффектов анимации. В дополнение к обычному жизненному циклу компонентов React,CSSTransitionGroupбазовый APITransitonGroupОн также предоставляет ряд специальных функций-ловушек жизненного цикла для своих дочерних элементов.Объединение сторонних анимационных библиотек в этих функциях-ловушках может реализовать богатые эффекты анимации входа и выхода.

TransisitonGroupПредоставьте следующие шесть функций ловушек жизненного цикла:

  • componentWillAppear(callback)
  • componentDidAppear()
  • componentWillEnter(callback)
  • componentDidEnter()
  • componentWillLeave(callback)
  • componentDidLeave()

Их синхронизация показана на рисунке ниже:

TransitionGroup组件生命周期与自组件生命周期的关系

Пример

GASPЭто анимационная библиотека, разработанная в эпоху флэш-памяти, основанная на концепции видеокадров и особенно подходящая для долговременных анимационных эффектов. В этой статье мы используемTransitonGroupа такжеreact-gsap-enhancer(библиотека улучшений, которая может применять GSAP к React) для завершения галереи изображений, код выглядит следующим образом:

import React, { Component } from 'react';
import { TransitionGroup } from 'react-transition-group';
import GSAP from 'react-gsap-enhancer'
import { TimelineMax, Back, Sine } from 'gsap';

class Photo extends Component {
    constructor(props) {
        super(props);
    }

    componentWillEnter(callback) {
        this.addAnimation(this.enterAnim, {callback: callback})
    }

    componentWillLeave(callback) {
        this.addAnimation(this.leaveAnim, {callback: callback})
    }

    enterAnim = (utils) => {
        const { id } = this.props;
        return new TimelineMax()
            .from(utils.target, 1, {
                x: `+=${( 4 - id ) * 60}px`,
                autoAlpha: 0,
                onComplete: utils.options.callback,
            }, id * 0.7);
    }

    leaveAnim = (utils) => {
        const { id } = this.props;
        return new TimelineMax()
            .to(utils.target, 0.5, {
                scale: 0,
                ease: Sine.easeOut,
                onComplete: utils.options.callback,
            }, (4 - id) * 0.7);
    }

    render() {
        const { url } = this.props;
        return (
            <div className="photo">
                <img src={url} />
            </div>
        )
    }
}

const WrappedPhoto = GSAP()(Photo);

export default class Gallery extends Component {
    constructor(props) {
        super(props);
        this.state = {
            show: false,
            photos: [{
                id: 1,
                url: 'http://img4.imgtn.bdimg.com/it/u=1032683424,3204785822&fm=214&gp=0.jpg'
            }, {
                id: 2,
                url: 'http://imgtu.5011.net/uploads/content/20170323/7488001490262119.jpg'
            }, {
                id: 3,
                url: 'http://tupian.enterdesk.com/2014/lxy/2014/12/03/18/10.jpg'
            }, {
                id: 4,
                url: 'http://img4.imgtn.bdimg.com/it/u=360498760,1598118672&fm=27&gp=0.jpg'
            }]
        };
    }

    toggle = () => {
        this.setState({
            show: !this.state.show
        })
    }

    render() {
        const { show, photos } = this.state;

        const renderPhotos = () => {
            return photos.map((item, index) => {
                return <WrappedPhoto id={item.id} url={item.url} key={`photo${item.id}`} />;
            })
        }

        return (
            <div>
                <button onClick={this.toggle}>toggle</button>
                <TransitionGroup component="div">
                    {show && renderPhotos()}
                </TransitionGroup>
            </div>
        );
    }
}

В этом примере мы находимся в дочернем компонентеPhotoизcomponentWillEnterа такжеcomponentWillLeaveДобавлена ​​анимация записи для каждого дочернего компонента в двух функциях хука.enterAnimи анимация отъездаLeaveAnim. В анимации входа используйтеTimeLineMax.from(target, duration, vars, delay)способ создать анимацию на временной шкале, указав, что расстояние перемещения анимации каждого подкомпонента зависит отidувеличивается и уменьшается, а время задержки увеличивается сidувеличивается и увеличивается, время задержки каждого подкомпонента в анимации отправления увеличивается сidувеличивается и уменьшается, позволяяidРазные имеют разные эффекты анимации. В реальном использовании вы можете добавлять различные эффекты к любому подкомпоненту в соответствии с вашими потребностями. Эффект этого примера показан на следующем рисунке:

图片画廊效果

В использованииTransitionGroupкогда, вcomponentnWillAppear(callback),componentnWillEntercallback),componentnWillLeave(callback)функция должнаВызывается после завершения логики функцииcallback,обещалTransitionGroupМожет правильно поддерживать последовательность состояний дочерних узлов. Подробнее о том, как использовать GASP, см.Официальная документация ГПБПи сообщение в блогеGSAP, профессиональная библиотека веб-анимации, эта статья не будет их повторять.

Объединение хука для реализации анимации может поддерживать различные сложные анимации, такие как анимация временных рядов и т. д. Из-за зависимости от сторонних библиотек эффект анимации часто более плавный, а взаимодействие с пользователем — лучше. Однако внедрение сторонних библиотек требует от разработчиков дополнительного изучения соответствующих API, что также увеличивает сложность кода.

5. Другие сторонние библиотеки анимации

Кроме того, существует множество отличных сторонних анимационных библиотек, таких какreact-motionанимация,velocity-reactПодождите, у этих анимационных библиотек тоже есть свои преимущества и недостатки при их использовании.

Animated

Animated— это кроссплатформенная библиотека анимации, совместимая с React и React Native. Поскольку в процессе анимации мы заботимся только о начальном состоянии, конечном состоянии и функции изменения анимации и не заботимся о конкретном значении атрибута элемента в каждый момент, поэтому Animated использует декларативную анимацию и вычисляет объект css. с помощью определенного метода, который он предоставляет.Animated.divРеализовать анимационные эффекты.

Пример

Мы используем Animated для достижения эффекта переворота изображения, код выглядит следующим образом.

import React, { Component } from 'react';
import Animated from 'animated/lib/targets/react-dom';

export default class PhotoPreview extends Component {
    constructor(props) {
        super(props);
        this.state = {
            anim: new Animated.Value(0)
        };
    }

    handleClick = () => {
        const { anim } = this.state;
        anim.stopAnimation(value => {
            Animated.spring(anim, {
                toValue: Math.round(value) + 1
            }).start();
        });
    }

    render() {
        const { anim } = this.state;

        const rotateDegree = anim.interpolate({
            inputRange: [0, 4],
            outputRange: ['0deg', '360deg']
        });

        return (
            <div>
                <button onClick={this.handleClick}>向右翻转</button>
                <Animated.div
                    style={{
                        transform: [{
                            rotate: rotateDegree
                        }]
                    }}
                    className="preivew-wrapper"
                >
                    <img
                        alt="img"
                        src="http://img4.imgtn.bdimg.com/it/u=1032683424,3204785822&fm=214&gp=0.jpg"
                    />
                </Animated.div>
            </div>
        );
    }
}

В этом примере мы хотим поворачивать изображение на 90° вправо при каждом нажатии кнопки. Когда компонент инициализируется, создается новый с начальным значением 0Animatedобъектthis.state.anim.AnimatedОбъект имеет функцию интерполяцииinterpolate, при установке интервала вводаinputRangeи выходной интервалoutputRangeПосле этого интерполяционная функция может быть основана наAnimatedТекущее значение объекта линейно интерполируется, и вычисляется соответствующее значение отображения.

В этом примере мы предполагаем, что каждый раз, когда нажимается кнопка,this.state.animДобавьте 1 к значению, и изображение нужно повернуть на 90°. В функции рендера мы устанавливаем функцию интерполяцииthis.state.anim.interpolateДиапазон ввода — [0, 4], а диапазон вывода — ['0deg', '360deg']. Когда анимация выполняется,this.state.animизменяет значение , функция интерполяции основана наthis.state.animТекущее значение, угол поворота рассчитываетсяrotateDegree, который запускает повторный рендеринг компонента. Следовательно, еслиAnimatedТекущее значение объекта равно 2, а соответствующий угол поворота равен 180 градусов. В структуре рендеринга компонента вам нужно использоватьAnimated.divоберните узел анимации и поместитеrotateDegreeИнкапсулирован как объект css, переданный как stlyeAnimated.divВ реализации изменения атрибута CSS узла.

В событии клика, учитывая, что кнопка может быть нажата несколько раз подряд, мы сначала используемstopAnimationОстановите текущую анимацию, функция вернет объект {value : number} в функции обратного вызова, значение соответствует последнему значению свойства анимации. Согласно полученномуvalueзначение, затем используйтеAnimated.springФункция запускает новый процесс анимации пружины для достижения плавного эффекта анимации. Поскольку каждый раз, когда вращение останавливается, мы хотим, чтобы угол поворота изображения был целым числом, кратным 90°, поэтому нам нужно исправитьAnimated.springЗначение завершения округляется в большую сторону. В итоге мы добились следующих результатов:

image

Есть несколько вещей, чтобы обратить внимание на использование:

  • AnimatedИ результаты его ценностей могут действовать только на объектахAnimated.divузел;
  • interpolateПо умолчанию линейная интерполяция будет выполняться в соответствии с входным интервалом и выходным интервалом.Если входное значение превышает входной интервал, это не будет затронуто.Результат интерполяции расширит интерполяцию в соответствии с выходным интервалом по умолчанию, который может быть установить, установивextrapolateСвойство ограничивает интервал результата интерполяции.

Animated не изменяет компонент напрямую во время анимации.state, но напрямую изменять свойства элемента через компоненты и методы его вновь созданного объекта, без многократного запуска функции рендеринга, которая является очень стабильной библиотекой анимации в React Native. Однако в React существует проблема совместимости браузеров с низкими версиями, и есть определенные затраты на обучение.

Эпилог

Когда мы реализуем анимацию в React, мы должны в первую очередь учитывать простоту анимации и сценарии использования.Для простых анимаций предпочтительнее реализация css3, а затем анимация временных интервалов на основе js. Если это анимация входа в элемент и анимация выхода, рекомендуется комбинироватьCSSTransitionGroupилиTransitionGroupвыполнить. Когда анимационные эффекты будут достигнуты более сложными, вы можете попробовать несколько отличных сторонних библиотек, чтобы открыть дверь в замечательные анимационные эффекты.

Ps. Все примеры кодов в этой статье доступныgithubПроверить

Использованная литература:

react-transition-group

react-gsap-enhancer

A Comparison of Animation Technologies

React Animations in Depth

Эта статья была впервые опубликована вБлог о технологиях Youzan.