v-if 上平滑的 vue 折叠过渡
Smooth vue collapse transition on v-if
我正在努力尝试使用 v-if 顺利显示/隐藏内容的 vue 转换。虽然我了解 css classes 和过渡,但我可以使用不透明度或翻译等方式使内容显示 'smoothly'...但是一旦动画完成(或者更确切地说,因为它开始),下面的任何 html 部分似乎 'jump'。
我正在尝试实现与 Bootstrap 4 'collapse' class 相同的效果 - 单击此处顶部的按钮之一:https://getbootstrap.com/docs/4.0/components/collapse/
随着隐藏部分的出现/消失,所有 html 内容 'slides' 都很好。
对于使用 v-if 显示的内容,是否可以使用 Vue 转换? vue transitions 文档上的所有示例,虽然具有很好的 css 过渡效果,但在过渡开始或完成后具有以下 html 'jump'。
我见过一些使用最大高度的纯 js 解决方案 - https://jsfiddle.net/wideboy32/7ap15qq0/134/
并尝试使用 vue:https://jsfiddle.net/wideboy32/eywraw8t/303737/
.smooth-enter-active, .smooth-leave-active {
transition: max-height .5s;
}
.smooth-enter, .smooth-leave-to {
max-height: 0 .5s;
}
谢谢!
如果要设置最大高度动画,则应为要设置动画的元素输入最大高度的数量,同时更正第二个 class 并输入 's'(或秒)在最大高度定义中:
p{
max-height: 20px;
}
.smooth-enter-active, .smooth-leave-active {
transition: max-height .5s;
}
.smooth-enter, .smooth-leave-to {
max-height: 0;
}
如果你想要 bs4 崩溃之类的东西,那么 vue 网站中的例子就可以了:
.smooth-enter-active, .smooth-leave-active {
transition: opacity .5s;
}
.smooth-enter, .smooth-leave-to {
opacity: 0
}
编辑:
您要做的是首先找出内容的高度,然后将其设置在 .*-enter-to
和 .*-leave
classes 内。下面的 fiddle 演示了一种方法:
https://jsfiddle.net/rezaxdi/sxgyj1f4/3/
您也可以完全忘记 v-if 或 v-show,只使用我认为更平滑的高度值隐藏元素:
我也有类似的任务。
我发现没有 JS 是不可能做到的。
所以我写了自定义转换组件(Reusable Transitions),它对我有用:
Vue.component('transition-collapse-height', {
template: `<transition
enter-active-class="enter-active"
leave-active-class="leave-active"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
>
<slot />
</transition>`,
methods: {
/**
* @param {HTMLElement} element
*/
beforeEnter(element) {
requestAnimationFrame(() => {
if (!element.style.height) {
element.style.height = '0px';
}
element.style.display = null;
});
},
/**
* @param {HTMLElement} element
*/
enter(element) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
element.style.height = `${element.scrollHeight}px`;
});
});
},
/**
* @param {HTMLElement} element
*/
afterEnter(element) {
element.style.height = null;
},
/**
* @param {HTMLElement} element
*/
beforeLeave(element) {
requestAnimationFrame(() => {
if (!element.style.height) {
element.style.height = `${element.offsetHeight}px`;
}
});
},
/**
* @param {HTMLElement} element
*/
leave(element) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
element.style.height = '0px';
});
});
},
/**
* @param {HTMLElement} element
*/
afterLeave(element) {
element.style.height = null;
},
},
});
new Vue({
el: '#app',
data: () => ({
isOpen: true,
}),
methods: {
onClick() {
this.isOpen = !this.isOpen;
}
}
});
.enter-active,
.leave-active {
overflow: hidden;
transition: height 1s linear;
}
.content {
background: grey;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button @click="onClick">
open/hide
</button>
<transition-collapse-height>
<div v-show="isOpen" class="content">
<br/>
<br/>
<br/>
<br/>
</div>
</transition-collapse-height>
</div>
这是我基于 Web Animation 的 Vue3 解决方案 API,请参阅之前在此处发布的 demo It is rather similar to the one by Alexandr Vysotsky,但是这个也会保留块的初始高度。
我从这个 blog post 开始,并以某种方式对其进行了改进(主要是为了在过渡结束后保持内容块的初始样式)。主要变化是切换到 Web 动画 API,它看起来与纯 CSS 动画一样高效,并提供更多控制。这也消除了原始解决方案中的所有性能优化黑客。
<script setup lang="ts">
interface Props {
duration?: number;
easingEnter?: string;
easingLeave?: string;
opacityClosed?: number;
opacityOpened?: number;
}
const props = withDefaults(defineProps<Props>(), {
duration: 250,
easingEnter: "ease-in-out",
easingLeave: "ease-in-out",
opacityClosed: 0,
opacityOpened: 1,
});
const closed = "0px";
interface initialStyle {
height: string;
width: string;
position: string;
visibility: string;
overflow: string;
paddingTop: string;
paddingBottom: string;
borderTopWidth: string;
borderBottomWidth: string;
marginTop: string;
marginBottom: string;
}
function getElementStyle(element: HTMLElement) {
return {
height: element.style.height,
width: element.style.width,
position: element.style.position,
visibility: element.style.visibility,
overflow: element.style.overflow,
paddingTop: element.style.paddingTop,
paddingBottom: element.style.paddingBottom,
borderTopWidth: element.style.borderTopWidth,
borderBottomWidth: element.style.borderBottomWidth,
marginTop: element.style.marginTop,
marginBottom: element.style.marginBottom,
};
}
function prepareElement(element: HTMLElement, initialStyle: initialStyle) {
const { width } = getComputedStyle(element);
element.style.width = width;
element.style.position = "absolute";
element.style.visibility = "hidden";
element.style.height = "";
let { height } = getComputedStyle(element);
element.style.width = initialStyle.width;
element.style.position = initialStyle.position;
element.style.visibility = initialStyle.visibility;
element.style.height = closed;
element.style.overflow = "hidden";
return initialStyle.height && initialStyle.height != closed
? initialStyle.height
: height;
}
function animateTransition(
element: HTMLElement,
initialStyle: initialStyle,
done: () => void,
keyframes: Keyframe[] | PropertyIndexedKeyframes | null,
options?: number | KeyframeAnimationOptions
) {
const animation = element.animate(keyframes, options);
// Set height to 'auto' to restore it after animation
element.style.height = initialStyle.height;
animation.onfinish = () => {
element.style.overflow = initialStyle.overflow;
done();
};
}
function getEnterKeyframes(height: string, initialStyle: initialStyle) {
return [
{
height: closed,
opacity: props.opacityClosed,
paddingTop: closed,
paddingBottom: closed,
borderTopWidth: closed,
borderBottomWidth: closed,
marginTop: closed,
marginBottom: closed,
},
{
height,
opacity: props.opacityOpened,
paddingTop: initialStyle.paddingTop,
paddingBottom: initialStyle.paddingBottom,
borderTopWidth: initialStyle.borderTopWidth,
borderBottomWidth: initialStyle.borderBottomWidth,
marginTop: initialStyle.marginTop,
marginBottom: initialStyle.marginBottom,
},
];
}
function enterTransition(element: Element, done: () => void) {
const HTMLElement = element as HTMLElement;
const initialStyle = getElementStyle(HTMLElement);
const height = prepareElement(HTMLElement, initialStyle);
const keyframes = getEnterKeyframes(height, initialStyle);
const options = { duration: props.duration, easing: props.easingEnter };
animateTransition(HTMLElement, initialStyle, done, keyframes, options);
}
function leaveTransition(element: Element, done: () => void) {
const HTMLElement = element as HTMLElement;
const initialStyle = getElementStyle(HTMLElement);
const { height } = getComputedStyle(HTMLElement);
HTMLElement.style.height = height;
HTMLElement.style.overflow = "hidden";
const keyframes = getEnterKeyframes(height, initialStyle).reverse();
const options = { duration: props.duration, easing: props.easingLeave };
animateTransition(HTMLElement, initialStyle, done, keyframes, options);
}
</script>
<template>
<Transition :css="false" @enter="enterTransition" @leave="leaveTransition">
<slot />
</Transition>
</template>
我正在努力尝试使用 v-if 顺利显示/隐藏内容的 vue 转换。虽然我了解 css classes 和过渡,但我可以使用不透明度或翻译等方式使内容显示 'smoothly'...但是一旦动画完成(或者更确切地说,因为它开始),下面的任何 html 部分似乎 'jump'。
我正在尝试实现与 Bootstrap 4 'collapse' class 相同的效果 - 单击此处顶部的按钮之一:https://getbootstrap.com/docs/4.0/components/collapse/
随着隐藏部分的出现/消失,所有 html 内容 'slides' 都很好。
对于使用 v-if 显示的内容,是否可以使用 Vue 转换? vue transitions 文档上的所有示例,虽然具有很好的 css 过渡效果,但在过渡开始或完成后具有以下 html 'jump'。
我见过一些使用最大高度的纯 js 解决方案 - https://jsfiddle.net/wideboy32/7ap15qq0/134/
并尝试使用 vue:https://jsfiddle.net/wideboy32/eywraw8t/303737/
.smooth-enter-active, .smooth-leave-active {
transition: max-height .5s;
}
.smooth-enter, .smooth-leave-to {
max-height: 0 .5s;
}
谢谢!
如果要设置最大高度动画,则应为要设置动画的元素输入最大高度的数量,同时更正第二个 class 并输入 's'(或秒)在最大高度定义中:
p{
max-height: 20px;
}
.smooth-enter-active, .smooth-leave-active {
transition: max-height .5s;
}
.smooth-enter, .smooth-leave-to {
max-height: 0;
}
如果你想要 bs4 崩溃之类的东西,那么 vue 网站中的例子就可以了:
.smooth-enter-active, .smooth-leave-active {
transition: opacity .5s;
}
.smooth-enter, .smooth-leave-to {
opacity: 0
}
编辑:
您要做的是首先找出内容的高度,然后将其设置在 .*-enter-to
和 .*-leave
classes 内。下面的 fiddle 演示了一种方法:
https://jsfiddle.net/rezaxdi/sxgyj1f4/3/
您也可以完全忘记 v-if 或 v-show,只使用我认为更平滑的高度值隐藏元素:
我也有类似的任务。 我发现没有 JS 是不可能做到的。 所以我写了自定义转换组件(Reusable Transitions),它对我有用:
Vue.component('transition-collapse-height', {
template: `<transition
enter-active-class="enter-active"
leave-active-class="leave-active"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
>
<slot />
</transition>`,
methods: {
/**
* @param {HTMLElement} element
*/
beforeEnter(element) {
requestAnimationFrame(() => {
if (!element.style.height) {
element.style.height = '0px';
}
element.style.display = null;
});
},
/**
* @param {HTMLElement} element
*/
enter(element) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
element.style.height = `${element.scrollHeight}px`;
});
});
},
/**
* @param {HTMLElement} element
*/
afterEnter(element) {
element.style.height = null;
},
/**
* @param {HTMLElement} element
*/
beforeLeave(element) {
requestAnimationFrame(() => {
if (!element.style.height) {
element.style.height = `${element.offsetHeight}px`;
}
});
},
/**
* @param {HTMLElement} element
*/
leave(element) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
element.style.height = '0px';
});
});
},
/**
* @param {HTMLElement} element
*/
afterLeave(element) {
element.style.height = null;
},
},
});
new Vue({
el: '#app',
data: () => ({
isOpen: true,
}),
methods: {
onClick() {
this.isOpen = !this.isOpen;
}
}
});
.enter-active,
.leave-active {
overflow: hidden;
transition: height 1s linear;
}
.content {
background: grey;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button @click="onClick">
open/hide
</button>
<transition-collapse-height>
<div v-show="isOpen" class="content">
<br/>
<br/>
<br/>
<br/>
</div>
</transition-collapse-height>
</div>
这是我基于 Web Animation 的 Vue3 解决方案 API,请参阅之前在此处发布的 demo It is rather similar to the one by Alexandr Vysotsky,但是这个也会保留块的初始高度。
我从这个 blog post 开始,并以某种方式对其进行了改进(主要是为了在过渡结束后保持内容块的初始样式)。主要变化是切换到 Web 动画 API,它看起来与纯 CSS 动画一样高效,并提供更多控制。这也消除了原始解决方案中的所有性能优化黑客。
<script setup lang="ts">
interface Props {
duration?: number;
easingEnter?: string;
easingLeave?: string;
opacityClosed?: number;
opacityOpened?: number;
}
const props = withDefaults(defineProps<Props>(), {
duration: 250,
easingEnter: "ease-in-out",
easingLeave: "ease-in-out",
opacityClosed: 0,
opacityOpened: 1,
});
const closed = "0px";
interface initialStyle {
height: string;
width: string;
position: string;
visibility: string;
overflow: string;
paddingTop: string;
paddingBottom: string;
borderTopWidth: string;
borderBottomWidth: string;
marginTop: string;
marginBottom: string;
}
function getElementStyle(element: HTMLElement) {
return {
height: element.style.height,
width: element.style.width,
position: element.style.position,
visibility: element.style.visibility,
overflow: element.style.overflow,
paddingTop: element.style.paddingTop,
paddingBottom: element.style.paddingBottom,
borderTopWidth: element.style.borderTopWidth,
borderBottomWidth: element.style.borderBottomWidth,
marginTop: element.style.marginTop,
marginBottom: element.style.marginBottom,
};
}
function prepareElement(element: HTMLElement, initialStyle: initialStyle) {
const { width } = getComputedStyle(element);
element.style.width = width;
element.style.position = "absolute";
element.style.visibility = "hidden";
element.style.height = "";
let { height } = getComputedStyle(element);
element.style.width = initialStyle.width;
element.style.position = initialStyle.position;
element.style.visibility = initialStyle.visibility;
element.style.height = closed;
element.style.overflow = "hidden";
return initialStyle.height && initialStyle.height != closed
? initialStyle.height
: height;
}
function animateTransition(
element: HTMLElement,
initialStyle: initialStyle,
done: () => void,
keyframes: Keyframe[] | PropertyIndexedKeyframes | null,
options?: number | KeyframeAnimationOptions
) {
const animation = element.animate(keyframes, options);
// Set height to 'auto' to restore it after animation
element.style.height = initialStyle.height;
animation.onfinish = () => {
element.style.overflow = initialStyle.overflow;
done();
};
}
function getEnterKeyframes(height: string, initialStyle: initialStyle) {
return [
{
height: closed,
opacity: props.opacityClosed,
paddingTop: closed,
paddingBottom: closed,
borderTopWidth: closed,
borderBottomWidth: closed,
marginTop: closed,
marginBottom: closed,
},
{
height,
opacity: props.opacityOpened,
paddingTop: initialStyle.paddingTop,
paddingBottom: initialStyle.paddingBottom,
borderTopWidth: initialStyle.borderTopWidth,
borderBottomWidth: initialStyle.borderBottomWidth,
marginTop: initialStyle.marginTop,
marginBottom: initialStyle.marginBottom,
},
];
}
function enterTransition(element: Element, done: () => void) {
const HTMLElement = element as HTMLElement;
const initialStyle = getElementStyle(HTMLElement);
const height = prepareElement(HTMLElement, initialStyle);
const keyframes = getEnterKeyframes(height, initialStyle);
const options = { duration: props.duration, easing: props.easingEnter };
animateTransition(HTMLElement, initialStyle, done, keyframes, options);
}
function leaveTransition(element: Element, done: () => void) {
const HTMLElement = element as HTMLElement;
const initialStyle = getElementStyle(HTMLElement);
const { height } = getComputedStyle(HTMLElement);
HTMLElement.style.height = height;
HTMLElement.style.overflow = "hidden";
const keyframes = getEnterKeyframes(height, initialStyle).reverse();
const options = { duration: props.duration, easing: props.easingLeave };
animateTransition(HTMLElement, initialStyle, done, keyframes, options);
}
</script>
<template>
<Transition :css="false" @enter="enterTransition" @leave="leaveTransition">
<slot />
</Transition>
</template>