アニメーションのトランジション効果をどのように実現しますか?
17730 ワード
概要
アニメーションという概念は非常に広く、各分野に及んでいます.ここでは、ゲーム分野のアニメーションはもちろん、最も簡単なことから始めます.
現在、ほとんどのWebアプリケーションはフレームワークに基づいて開発されています.例えば、Vue、Reactなど、データ駆動ビューに基づいています.では、これらのフレームワークがない場合、アニメーションや移行効果をどのように実現し、データ駆動を使用してどのように実現するかを比較してみましょう.
従来のトランジションアニメーション
アニメーション効果は体験にとって非常に重要な効果がありますが、多くの開発者にとって、非常に弱い部分かもしれません.css 3が現れた後、多くの初心者が最もよく使うアニメーションの移行はcss 3の能力かもしれません.
css遷移アニメーション
cssは移行アニメーションを起動するのはとても簡単で、transition属性を書くだけでいいです.次はdemoを書きます.
効果はやはりとても良くて、css 3のtransitionは基本的に大部分のアニメーションの需要を満たして、もし満足しなければ本当のcss 3 animationがあります.animate-cssの有名なcssアニメーションライブラリは、誰が知っていますか.
css 3 transitionでもcss 3 animationでも簡単にclassクラス名を切り替えることで、コールバック処理を行う場合はブラウザにontransitionnd、onanimationendなどのアニメーションフレームイベントが提供され、jsインタフェースで傍受すればよい.
OK、これがcssアニメーションの基礎であり、jsパッケージによっても大部分のアニメーション遷移需要を実現することができるが、限界はcssがサポートする属性アニメーションを制御するしかないのに対し、制御力はまだ少し弱い.
jsアニメーション
jsはあくまでもカスタムエンコーディングプログラムであり、アニメーションに対する制御力が強く、様々なcssがサポートしていない効果を実現することができます.ではjsがアニメーションを実現する基礎は何ですか?簡単に言えば、アニメーションとは、時間軸上で要素のプロパティを更新し続け、ブラウザに渡して再描画し、視覚的にアニメーションになります.くだらないことは言わないで、まず栗をください.
時間軸上で属性を更新し続け、settimoutまたはrequestAnimationで実現できます.Tween緩動関数については、補間のような概念で、一連の変数を与え、区間セグメントで任意の時刻の値を取得することができ、純粋な数学式では、ほとんどのアニメーションフレームワークが使用され、知りたいことは張鑫旭のTween.jsを参照してください.
OK、この極めて簡単なdemoもjsがアニメーションを実現する核心の基礎で、私たちがプログラムを通じて移行値の生成過程を完璧に制御したことを見ることができて、その他の複雑なアニメーションのメカニズムはすべてこのモードです.
従来とVue/Reactフレームワークの比較
前の例では、css遷移でもjs遷移でもdom要素を直接取得し、dom要素を属性操作します.Vue/Reactはすべて仮想domの概念を導入して、データはビューを駆動して、私達はできるだけdomを操作しないで、データだけを制御して、それでは私達はどのようにデータの方面でアニメーションを駆動しますか?
Vueフレームの下での遷移アニメーション
まず、ドキュメントVue遷移アニメーションを参照してください.Vueが提供するtransitionコンポーネントがアニメーション移行サポートをどのように実現しているかを分析します.
Transitionコンポーネント
まずtransitionコンポーネントコードを参照してください.パス「src/platforms/web/runtime/components/transition.js」のコアコードは次のとおりです.
このコンポーネント自体の機能は簡単で、slotsを通じてレンダリングする要素childrenを手に入れ、transitionのprops属性データcopyをdataのtranstion属性に送り、後続の注入ライフサイクルに使用し、mergeVNodeHookはライフサイクル管理を行うことがわかります.
modules/transition
次にライフサイクル関連を下に見ます.経路:src/platforms/web/runtime/modules/transition.jsはまずデフォルトのエクスポートを参照します.
ここでinBrowserはtrueとして扱われます.ブラウザ環境を分析しているからです.次にenterとleave関数を見て、まずenterを見ます.
Enterでは、遷移またはアニメーション終了のイベントをリスニングする関数whenTransitionEndsが使用されています.
OK、ここに着いたら、上のソースコードの注釈分析に基づいて、私たちは発見することができます: Vueは、まずいくつかのカラム操作dom classNameの補助方法addClass/removeClassなどをカプセル化しています. ライフサイクルenterHookの後、startClass、すなわちenterClassのデフォルトの初期スタイルがすぐに設定され、activeClass もあります.ブラウザnextFrameの次のフレームに続いてstartClassが除去され、toClassが追加され、遷移アニメーションが追加されたendイベント傍受処理 が追加される. endイベントを傍受した後、cbを異動し、toClassとactiveClass を除去した.
leaveのプロシージャはenterのプロシージャと同じですが、classNameを逆方向に追加して削除するだけです.
結論:Vueのアニメーション遷移処理方式は従来のdomと本質的に同じであり、Vueの各ライフサイクルに溶け込んで処理するにすぎず、本質的にはdomに削除を追加するタイミングで処理する
Reactの遷移アニメーション
ああ、Reactのドキュメントをめくっても、移行アニメーションの処理は見つかりませんでした.ねえ、公式の不原生支持のようです.
しかし、私たちは自分で実現することができます.例えば、useStateでステータスを維持し、renderでステータスに応じてclassNameの切り替えを行うことができますが、複雑な場合はどうすればいいですか.
幸いにもコミュニティでホイールプラグインreact-transition-groupを見つけました.ええ、ソースコードを直接貼って、前のVueの分析があります.これはとても理解しやすくて、かえって簡単です.
Vueと非常に似ていることがわかるが,ここではReactの各ライフサイクル関数で処理するようになった.
ここまで来ると、VueのtransitonコンポーネントでもReactというtransiton-groupコンポーネントでも、css属性のアニメーションに重点を置いていることがわかります.
データ駆動のアニメーション
実際のシーンではcssでは処理できないアニメーションが常に発生します.この場合、2つの解決策があります.はrefによってdomを取得し、従来のjsスキームを採用する. state状態メンテナンスにより描画domのデータを維持し、setState更新stateクラス駆動ビューにより を自動的にリフレッシュする.
アニメーションという概念は非常に広く、各分野に及んでいます.ここでは、ゲーム分野のアニメーションはもちろん、最も簡単なことから始めます.
現在、ほとんどのWebアプリケーションはフレームワークに基づいて開発されています.例えば、Vue、Reactなど、データ駆動ビューに基づいています.では、これらのフレームワークがない場合、アニメーションや移行効果をどのように実現し、データ駆動を使用してどのように実現するかを比較してみましょう.
従来のトランジションアニメーション
アニメーション効果は体験にとって非常に重要な効果がありますが、多くの開発者にとって、非常に弱い部分かもしれません.css 3が現れた後、多くの初心者が最もよく使うアニメーションの移行はcss 3の能力かもしれません.
css遷移アニメーション
cssは移行アニメーションを起動するのはとても簡単で、transition属性を書くだけでいいです.次はdemoを書きます.
.normal {
width: 100px;
height: 100px;
background-color: red;
transition: all 0.3s;
}
.normal:hover {
background-color: yellow;
width: 200px;
height: 200px;
}
効果はやはりとても良くて、css 3のtransitionは基本的に大部分のアニメーションの需要を満たして、もし満足しなければ本当のcss 3 animationがあります.animate-cssの有名なcssアニメーションライブラリは、誰が知っていますか.
css 3 transitionでもcss 3 animationでも簡単にclassクラス名を切り替えることで、コールバック処理を行う場合はブラウザにontransitionnd、onanimationendなどのアニメーションフレームイベントが提供され、jsインタフェースで傍受すればよい.
var el = document.querySelector('#app')
el.addEventListener('transitionstart', () => {
console.log('transition start')
})
el.addEventListener('transitionend', () => {
console.log('transition end')
})
OK、これがcssアニメーションの基礎であり、jsパッケージによっても大部分のアニメーション遷移需要を実現することができるが、限界はcssがサポートする属性アニメーションを制御するしかないのに対し、制御力はまだ少し弱い.
jsアニメーション
jsはあくまでもカスタムエンコーディングプログラムであり、アニメーションに対する制御力が強く、様々なcssがサポートしていない効果を実現することができます.ではjsがアニメーションを実現する基礎は何ですか?簡単に言えば、アニメーションとは、時間軸上で要素のプロパティを更新し続け、ブラウザに渡して再描画し、視覚的にアニメーションになります.くだらないことは言わないで、まず栗をください.
#app {
width: 100px;
height: 100px;
background-color: red;
border-radius: 50%;
}
// Tween
var el = document.querySelector('#app')
var time = 0, begin = 0, change = 500, duration = 1000, fps = 1000 / 60;
function startSport() {
var val = Tween.Elastic.easeInOut(time, begin, change, duration);
el.style.transform = 'translateX(' + val + 'px)';
if (time <= duration) {
time += fps
} else {
console.log(' ')
time = 0;
}
setTimeout(() => {
startSport()
}, fps)
}
startSport()
時間軸上で属性を更新し続け、settimoutまたはrequestAnimationで実現できます.Tween緩動関数については、補間のような概念で、一連の変数を与え、区間セグメントで任意の時刻の値を取得することができ、純粋な数学式では、ほとんどのアニメーションフレームワークが使用され、知りたいことは張鑫旭のTween.jsを参照してください.
OK、この極めて簡単なdemoもjsがアニメーションを実現する核心の基礎で、私たちがプログラムを通じて移行値の生成過程を完璧に制御したことを見ることができて、その他の複雑なアニメーションのメカニズムはすべてこのモードです.
従来とVue/Reactフレームワークの比較
前の例では、css遷移でもjs遷移でもdom要素を直接取得し、dom要素を属性操作します.Vue/Reactはすべて仮想domの概念を導入して、データはビューを駆動して、私達はできるだけdomを操作しないで、データだけを制御して、それでは私達はどのようにデータの方面でアニメーションを駆動しますか?
Vueフレームの下での遷移アニメーション
まず、ドキュメントVue遷移アニメーションを参照してください.Vueが提供するtransitionコンポーネントがアニメーション移行サポートをどのように実現しているかを分析します.
Transitionコンポーネント
まずtransitionコンポーネントコードを参照してください.パス「src/platforms/web/runtime/components/transition.js」のコアコードは次のとおりです.
// , props
export function extractTransitionData (comp: Component): Object {
const data = {}
const options: ComponentOptions = comp.$options
// props
for (const key in options.propsData) {
data[key] = comp[key]
}
// events.
const listeners: ?Object = options._parentListeners
for (const key in listeners) {
data[camelize(key)] = listeners[key]
}
return data
}
export default {
name: 'transition',
props: transitionProps,
abstract: true, // , dom,
render (h: Function) {
// slots children
let children: any = this.$slots.default
const mode: string = this.mode
const rawChild: VNode = children[0]
// key
// component instance. This key will be used to remove pending leaving nodes
// during entering.
const id: string = `__transition-${this._uid}-`
child.key = getKey(id)
: child.key
// data transition , props
const data: Object = (child.data || (child.data = {})).transition = extractTransitionData(this)
const oldRawChild: VNode = this._vnode
const oldChild: VNode = getRealChild(oldRawChild)
// important for dynamic transitions!
const oldData: Object = oldChild.data.transition = extend({}, data)
// handle transition mode
if (mode === 'out-in') {
// return placeholder node and queue update when leave finishes
this._leaving = true
mergeVNodeHook(oldData, 'afterLeave', () => {
this._leaving = false
this.$forceUpdate()
})
return placeholder(h, rawChild)
} else if (mode === 'in-out') {
let delayedLeave
const performLeave = () => { delayedLeave() }
mergeVNodeHook(data, 'afterEnter', performLeave)
mergeVNodeHook(data, 'enterCancelled', performLeave)
mergeVNodeHook(oldData, 'delayLeave', leave => { delayedLeave = leave })
}
return rawChild
}
}
このコンポーネント自体の機能は簡単で、slotsを通じてレンダリングする要素childrenを手に入れ、transitionのprops属性データcopyをdataのtranstion属性に送り、後続の注入ライフサイクルに使用し、mergeVNodeHookはライフサイクル管理を行うことがわかります.
modules/transition
次にライフサイクル関連を下に見ます.経路:src/platforms/web/runtime/modules/transition.jsはまずデフォルトのエクスポートを参照します.
function _enter (_: any, vnode: VNodeWithData) {
if (vnode.data.show !== true) {
enter(vnode)
}
}
export default inBrowser ? {
create: _enter,
activate: _enter,
remove (vnode: VNode, rm: Function) {
if (vnode.data.show !== true) {
leave(vnode, rm)
}
}
} : {}
ここでinBrowserはtrueとして扱われます.ブラウザ環境を分析しているからです.次にenterとleave関数を見て、まずenterを見ます.
export function addTransitionClass (el: any, cls: string) {
const transitionClasses = el._transitionClasses || (el._transitionClasses = [])
if (transitionClasses.indexOf(cls) < 0) {
transitionClasses.push(cls)
addClass(el, cls)
}
}
export function removeTransitionClass (el: any, cls: string) {
if (el._transitionClasses) {
remove(el._transitionClasses, cls)
}
removeClass(el, cls)
}
export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
const el: any = vnode.elm
// call leave callback now
if (isDef(el._leaveCb)) {
el._leaveCb.cancelled = true
el._leaveCb()
}
// data transition
const data = resolveTransition(vnode.data.transition)
if (isUndef(data)) {
return
}
/* istanbul ignore if */
if (isDef(el._enterCb) || el.nodeType !== 1) {
return
}
const {
css,
type,
enterClass,
enterToClass,
enterActiveClass,
appearClass,
appearToClass,
appearActiveClass,
beforeEnter,
enter,
afterEnter,
enterCancelled,
beforeAppear,
appear,
afterAppear,
appearCancelled,
duration
} = data
let context = activeInstance
let transitionNode = activeInstance.$vnode
const isAppear = !context._isMounted || !vnode.isRootInsert
if (isAppear && !appear && appear !== '') {
return
}
// className
const startClass = isAppear && appearClass
? appearClass
: enterClass
const activeClass = isAppear && appearActiveClass
? appearActiveClass
: enterActiveClass
const toClass = isAppear && appearToClass
? appearToClass
: enterToClass
const beforeEnterHook = isAppear
? (beforeAppear || beforeEnter)
: beforeEnter
const enterHook = isAppear
? (typeof appear === 'function' ? appear : enter)
: enter
const afterEnterHook = isAppear
? (afterAppear || afterEnter)
: afterEnter
const enterCancelledHook = isAppear
? (appearCancelled || enterCancelled)
: enterCancelled
const explicitEnterDuration: any = toNumber(
isObject(duration)
? duration.enter
: duration
)
const expectsCSS = css !== false && !isIE9
const userWantsControl = getHookArgumentsLength(enterHook)
// , class
const cb = el._enterCb = once(() => {
if (expectsCSS) {
removeTransitionClass(el, toClass)
removeTransitionClass(el, activeClass)
}
if (cb.cancelled) {
if (expectsCSS) {
removeTransitionClass(el, startClass)
}
enterCancelledHook && enterCancelledHook(el)
} else {
afterEnterHook && afterEnterHook(el)
}
el._enterCb = null
})
// dom , start class
beforeEnterHook && beforeEnterHook(el)
if (expectsCSS) {
//
addTransitionClass(el, startClass)
addTransitionClass(el, activeClass)
// , toClass
// end , cb
nextFrame(() => {
removeTransitionClass(el, startClass)
if (!cb.cancelled) {
addTransitionClass(el, toClass)
if (!userWantsControl) {
if (isValidDuration(explicitEnterDuration)) {
setTimeout(cb, explicitEnterDuration)
} else {
whenTransitionEnds(el, type, cb)
}
}
}
})
}
if (vnode.data.show) {
toggleDisplay && toggleDisplay()
enterHook && enterHook(el, cb)
}
if (!expectsCSS && !userWantsControl) {
cb()
}
}
Enterでは、遷移またはアニメーション終了のイベントをリスニングする関数whenTransitionEndsが使用されています.
export let transitionEndEvent = 'transitionend'
export let animationEndEvent = 'animationend'
export function whenTransitionEnds (
el: Element,
expectedType: ?string,
cb: Function
) {
const { type, timeout, propCount } = getTransitionInfo(el, expectedType)
if (!type) return cb()
const event: string = type === TRANSITION ? transitionEndEvent : animationEndEvent
let ended = 0
const end = () => {
el.removeEventListener(event, onEnd)
cb()
}
const onEnd = e => {
if (e.target === el) {
if (++ended >= propCount) {
end()
}
}
}
setTimeout(() => {
if (ended < propCount) {
end()
}
}, timeout + 1)
el.addEventListener(event, onEnd)
}
OK、ここに着いたら、上のソースコードの注釈分析に基づいて、私たちは発見することができます:
leaveのプロシージャはenterのプロシージャと同じですが、classNameを逆方向に追加して削除するだけです.
結論:Vueのアニメーション遷移処理方式は従来のdomと本質的に同じであり、Vueの各ライフサイクルに溶け込んで処理するにすぎず、本質的にはdomに削除を追加するタイミングで処理する
Reactの遷移アニメーション
ああ、Reactのドキュメントをめくっても、移行アニメーションの処理は見つかりませんでした.ねえ、公式の不原生支持のようです.
しかし、私たちは自分で実現することができます.例えば、useStateでステータスを維持し、renderでステータスに応じてclassNameの切り替えを行うことができますが、複雑な場合はどうすればいいですか.
幸いにもコミュニティでホイールプラグインreact-transition-groupを見つけました.ええ、ソースコードを直接貼って、前のVueの分析があります.これはとても理解しやすくて、かえって簡単です.
class Transition extends React.Component {
static contextType = TransitionGroupContext
constructor(props, context) {
super(props, context)
let parentGroup = context
let appear =
parentGroup && !parentGroup.isMounting ? props.enter : props.appear
let initialStatus
this.appearStatus = null
if (props.in) {
if (appear) {
initialStatus = EXITED
this.appearStatus = ENTERING
} else {
initialStatus = ENTERED
}
} else {
if (props.unmountOnExit || props.mountOnEnter) {
initialStatus = UNMOUNTED
} else {
initialStatus = EXITED
}
}
this.state = { status: initialStatus }
this.nextCallback = null
}
// dom ,
componentDidMount() {
this.updateStatus(true, this.appearStatus)
}
// data ,
componentDidUpdate(prevProps) {
let nextStatus = null
if (prevProps !== this.props) {
const { status } = this.state
if (this.props.in) {
if (status !== ENTERING && status !== ENTERED) {
nextStatus = ENTERING
}
} else {
if (status === ENTERING || status === ENTERED) {
nextStatus = EXITING
}
}
}
this.updateStatus(false, nextStatus)
}
updateStatus(mounting = false, nextStatus) {
if (nextStatus !== null) {
// nextStatus will always be ENTERING or EXITING.
this.cancelNextCallback()
if (nextStatus === ENTERING) {
this.performEnter(mounting)
} else {
this.performExit()
}
} else if (this.props.unmountOnExit && this.state.status === EXITED) {
this.setState({ status: UNMOUNTED })
}
}
performEnter(mounting) {
const { enter } = this.props
const appearing = this.context ? this.context.isMounting : mounting
const [maybeNode, maybeAppearing] = this.props.nodeRef
? [appearing]
: [ReactDOM.findDOMNode(this), appearing]
const timeouts = this.getTimeouts()
const enterTimeout = appearing ? timeouts.appear : timeouts.enter
// no enter animation skip right to ENTERED
// if we are mounting and running this it means appear _must_ be set
if ((!mounting && !enter) || config.disabled) {
this.safeSetState({ status: ENTERED }, () => {
this.props.onEntered(maybeNode)
})
return
}
this.props.onEnter(maybeNode, maybeAppearing)
this.safeSetState({ status: ENTERING }, () => {
this.props.onEntering(maybeNode, maybeAppearing)
this.onTransitionEnd(enterTimeout, () => {
this.safeSetState({ status: ENTERED }, () => {
this.props.onEntered(maybeNode, maybeAppearing)
})
})
})
}
performExit() {
const { exit } = this.props
const timeouts = this.getTimeouts()
const maybeNode = this.props.nodeRef
? undefined
: ReactDOM.findDOMNode(this)
// no exit animation skip right to EXITED
if (!exit || config.disabled) {
this.safeSetState({ status: EXITED }, () => {
this.props.onExited(maybeNode)
})
return
}
this.props.onExit(maybeNode)
this.safeSetState({ status: EXITING }, () => {
this.props.onExiting(maybeNode)
this.onTransitionEnd(timeouts.exit, () => {
this.safeSetState({ status: EXITED }, () => {
this.props.onExited(maybeNode)
})
})
})
}
cancelNextCallback() {
if (this.nextCallback !== null) {
this.nextCallback.cancel()
this.nextCallback = null
}
}
safeSetState(nextState, callback) {
// This shouldn't be necessary, but there are weird race conditions with
// setState callbacks and unmounting in testing, so always make sure that
// we can cancel any pending setState callbacks after we unmount.
callback = this.setNextCallback(callback)
this.setState(nextState, callback)
}
setNextCallback(callback) {
let active = true
this.nextCallback = event => {
if (active) {
active = false
this.nextCallback = null
callback(event)
}
}
this.nextCallback.cancel = () => {
active = false
}
return this.nextCallback
}
// end
onTransitionEnd(timeout, handler) {
this.setNextCallback(handler)
const node = this.props.nodeRef
? this.props.nodeRef.current
: ReactDOM.findDOMNode(this)
const doesNotHaveTimeoutOrListener =
timeout == null && !this.props.addEndListener
if (!node || doesNotHaveTimeoutOrListener) {
setTimeout(this.nextCallback, 0)
return
}
if (this.props.addEndListener) {
const [maybeNode, maybeNextCallback] = this.props.nodeRef
? [this.nextCallback]
: [node, this.nextCallback]
this.props.addEndListener(maybeNode, maybeNextCallback)
}
if (timeout != null) {
setTimeout(this.nextCallback, timeout)
}
}
render() {
const status = this.state.status
if (status === UNMOUNTED) {
return null
}
const {
children,
// filter props for `Transition`
in: _in,
mountOnEnter: _mountOnEnter,
unmountOnExit: _unmountOnExit,
appear: _appear,
enter: _enter,
exit: _exit,
timeout: _timeout,
addEndListener: _addEndListener,
onEnter: _onEnter,
onEntering: _onEntering,
onEntered: _onEntered,
onExit: _onExit,
onExiting: _onExiting,
onExited: _onExited,
nodeRef: _nodeRef,
...childProps
} = this.props
return (
// allows for nested Transitions
{typeof children === 'function'
? children(status, childProps)
: React.cloneElement(React.Children.only(children), childProps)}
)
}
}
Vueと非常に似ていることがわかるが,ここではReactの各ライフサイクル関数で処理するようになった.
ここまで来ると、VueのtransitonコンポーネントでもReactというtransiton-groupコンポーネントでも、css属性のアニメーションに重点を置いていることがわかります.
データ駆動のアニメーション
実際のシーンではcssでは処理できないアニメーションが常に発生します.この場合、2つの解決策があります.