如何在 v-if 上添加平滑的 Vue 折叠过渡

问题描述 投票:0回答:4

我正在努力解决 Vue 转换问题,试图使用 v-if 顺利地显示/隐藏内容。虽然我了解 CSS 类和过渡,但我可以使用不透明度或翻译等方式使内容“平滑”地显示...但是一旦动画完成(或者更确切地说,当它开始时),下面的任何 HTML 部分似乎都会“跳'。

我正在尝试实现与 Bootstrap 4“折叠”类相同的效果 - 单击此处的顶部按钮之一:https://getbootstrap.com/docs/4.0/components/collapse/

随着隐藏部分的出现/消失,所有 HTML 内容都会随之很好地“滑动”。

是否可以使用 Vue 转换来显示使用 v-if 的内容? Vue 过渡文档中的所有示例虽然具有出色的 CSS 过渡效果,但一旦过渡开始或完成,就会出现以下 HTML“跳转”。

我见过一些使用 max-height 的纯 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;
}
css vue.js vuejs2 transition
4个回答
27
投票

我也有类似的任务。 我发现没有JS是不可能做到的。 所以我编写了自定义过渡组件(可重用过渡),它对我有用:

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>


19
投票

如果您想要设置最大高度动画,那么您应该输入要设置动画的元素的最大高度量,同时在最大高度定义中输入“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
类中来实现。下面的 JSFiddle 演示了一种实现方法:

https://jsfiddle.net/rezaxdi/sxgyj1f4/3/

您也可以完全忘记 v-if 或 v-show 并使用高度值隐藏元素,我认为这更平滑:

https://jsfiddle.net/rezaxdi/tgfabw65/9/


9
投票

这是我基于 Web Animation API 的 Vue3 解决方案,请参阅 demo 它与 Alexandr Vysotsky 之前发布的解决方案非常相似,但这个解决方案也会保留块的初始高度。

我从这篇博客文章开始,并以某种方式改进了它(主要是为了在过渡结束后保持内容块的初始样式)。主要的变化是切换到 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>

0
投票

感谢 @kostyfisik 他优雅的解决方案。通过添加宽度变化稍微改变了它,这样您就可以选择使用 mode 属性来制作动画的测量。

<script setup lang="ts">
interface IExpandAnimationProps {
  duration?: number;
  easingEnter?: string;
  easingLeave?: string;
  opacityClosed?: number;
  opacityOpened?: number;
  mode?: 'width' | 'height';
}

interface initialStyle {
  height: string;
  width: string;
  position: string;
  visibility: string;
  overflow: string;
  paddingTop: string;
  paddingBottom: string;
  paddingLeft: string;
  paddingRight: string;
  borderTopWidth: string;
  borderBottomWidth: string;
  borderLeftWidth: string;
  borderRightWidth: string;
  marginTop: string;
  marginBottom: string;
  marginLeft: string;
  marginRight: string;
}

const props = withDefaults(defineProps<IExpandAnimationProps>(), {
  duration: 300,
  easingEnter: 'ease-in-out',
  easingLeave: 'ease-in-out',
  opacityClosed: 0,
  opacityOpened: 1,
  mode: 'height',
});

const closed = '0px';

function getElementStyle(element: HTMLElement): initialStyle {
  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,
    paddingLeft: element.style.paddingLeft,
    paddingRight: element.style.paddingRight,
    borderTopWidth: element.style.borderTopWidth,
    borderBottomWidth: element.style.borderBottomWidth,
    borderLeftWidth: element.style.borderLeftWidth,
    borderRightWidth: element.style.borderRightWidth,
    marginTop: element.style.marginTop,
    marginBottom: element.style.marginBottom,
    marginLeft: element.style.marginLeft,
    marginRight: element.style.marginRight,
  };
}

function prepareElement(element: HTMLElement, initialStyle: initialStyle): string {
  let width, height;
  if (props.mode === 'height') {
    element.style.width = getComputedStyle(element).width;
    element.style.position = 'absolute';
    element.style.visibility = 'hidden';
    element.style.height = '';
    height = getComputedStyle(element).height;
    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;
  } else {
    element.style.height = getComputedStyle(element).height;
    element.style.position = 'absolute';
    element.style.visibility = 'hidden';
    element.style.width = '';
    width = getComputedStyle(element).width;
    element.style.height = initialStyle.height;
    element.style.position = initialStyle.position;
    element.style.visibility = initialStyle.visibility;
    element.style.width = closed;
    element.style.overflow = 'hidden';
    return initialStyle.width && initialStyle.width != closed ? initialStyle.width : width;
  }
}

function animateTransition(
  element: HTMLElement,
  initialStyle: initialStyle,
  done: () => void,
  keyframes: Keyframe[] | PropertyIndexedKeyframes | null,
  options?: number | KeyframeAnimationOptions,
): void {
  const animation = element.animate(keyframes, options);
  if (props.mode === 'height') {
    element.style.height = initialStyle.height;
  } else {
    element.style.width = initialStyle.width;
  }
  animation.onfinish = () => {
    element.style.overflow = initialStyle.overflow;
    done();
  };
}

function getEnterKeyframes(measurement: string, initialStyle: initialStyle): Keyframe[] {
  if (props.mode === 'height') {
    return [
      {
        height: closed,
        opacity: props.opacityClosed,
        paddingTop: closed,
        paddingBottom: closed,
        borderTopWidth: closed,
        borderBottomWidth: closed,
        marginTop: closed,
        marginBottom: closed,
      },
      {
        height: measurement,
        opacity: props.opacityOpened,
        paddingTop: initialStyle.paddingTop,
        paddingBottom: initialStyle.paddingBottom,
        borderTopWidth: initialStyle.borderTopWidth,
        borderBottomWidth: initialStyle.borderBottomWidth,
        marginTop: initialStyle.marginTop,
        marginBottom: initialStyle.marginBottom,
      },
    ];
  } else {
    return [
      {
        width: closed,
        opacity: props.opacityClosed,
        paddingLeft: closed,
        paddingRight: closed,
        borderLeftWidth: closed,
        borderRightWidth: closed,
        marginLeft: closed,
        marginRight: closed,
      },
      {
        width: measurement,
        opacity: props.opacityOpened,
        paddingLeft: initialStyle.paddingLeft,
        paddingRight: initialStyle.paddingRight,
        borderLeftWidth: initialStyle.borderLeftWidth,
        borderRightWidth: initialStyle.borderRightWidth,
        marginLeft: initialStyle.marginLeft,
        marginRight: initialStyle.marginRight,
      },
    ];
  }
}

function enterTransition(element: Element, done: () => void) {
  const HTMLElement = element as HTMLElement;
  const initialStyle = getElementStyle(HTMLElement);
  const measurement = prepareElement(HTMLElement, initialStyle);
  const keyframes = getEnterKeyframes(measurement, 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);
  let measurement;
  if (props.mode === 'height') {
    measurement = getComputedStyle(HTMLElement).height;
    HTMLElement.style.height = measurement;
  } else {
    measurement = getComputedStyle(HTMLElement).width;
    HTMLElement.style.width = measurement;
  }

  HTMLElement.style.overflow = 'hidden';
  const keyframes = getEnterKeyframes(measurement, 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>
© www.soinside.com 2019 - 2024. All rights reserved.