【问题标题】:Angular Animation For Dynamically Changing Height动态改变高度的角度动画
【发布时间】:2017-11-15 18:59:35
【问题描述】:

我已经成功获得了一个面板,用于在进入和离开 DOM 时动画展开和关闭。问题是我现在在显示详细信息之前面板内有一个忙碌指示器,并且动画仅在打开忙碌指示器时出现,并在显示详细内容时捕捉。

如何让 Angular 动画在任何高度变化时进行动画处理?

我这里有一个例子:https://stackblitz.com/edit/angular-animation-for-dynamically-changing-height?embed=1&file=src/app/app.component.ts

trigger('expandCollapseDetails', [
    state('void', style({
        'height': '0px',
        overflow: 'hidden'
    })),
    //element being added into DOM.
    transition(':enter', [
        animate('500ms ease-in-out', style({
            'height': '*',
            overflow: 'hidden'
        }))
    ]),
    //element being removed from DOM.
    transition(':leave', [
        animate('500ms ease-in-out', style({
            'height': '0px',
            overflow: 'hidden'
        }))
    ])
])

【问题讨论】:

    标签: angular animation


    【解决方案1】:

    我编写了一个组件,它可以在内容发生变化时平滑地为投影内容的高度设置动画。 它是这样使用的:

    <smooth-height [trigger]="content">
      {{content}}
    </smooth-height>
    

    这是一个堆栈闪电战:https://stackblitz.com/edit/angular4-kugxw7

    这是组件:

    import {ElementRef, HostBinding, Component, Input, OnChanges} from '@angular/core';
    import {animate, style, transition, trigger} from "@angular/animations";
    
    @Component({
      selector: 'smooth-height',
      template: `
        <ng-content></ng-content>
      `,
      styles: [`
        :host {
          display: block;
          overflow: hidden;
        }
      `],
      animations: [
        trigger('grow', [
          transition('void <=> *', []),
          transition('* <=> *', [
            style({height: '{{startHeight}}px', opacity: 0}),
            animate('.5s ease'),
          ], {params: {startHeight: 0}})
        ])
      ]
    })
    export class SmoothHeightComponent implements OnChanges {
      @Input()
      trigger: any;
    
      startHeight: number;
    
      @HostBinding('@grow') grow: any;
    
      constructor(private element: ElementRef) {}  
                  
      ngOnChanges(){
        this.startHeight = this.element.nativeElement.clientHeight;
    
        this.grow = {
          value: this.trigger,
          params: {startHeight: this.startHeight}
        };
      }
    }
    

    【讨论】:

    • 您的示例与我的示例有点不同(我的动态内容不仅仅是一个字符串),但我认为我已经对您的示例进行了足够的调整以应付。谢谢!
    • 欢迎您。关于您的 plnkr,我认为您可以像 ... 一样使用它
    • 好主意。 indicator 实际上是动画的触发器。
    • 谢谢,将“indicator”改为“trigger”,确实是一个更好的词!
    • 因为在模板中调用函数(包括get getter)可能被认为是不好的做法,我建议将@HostBinding('@grow') 更改为一个简单的属性:@HostBinding('@grow') grow: any;。然后,在ngOnChanges 中设置该属性。 this.grow = { value: this.trigger, params: { startHeight: this.startHeight } };。这可以防止 get grow() 在 Angular Change Detection 确定值是否一致时被多次调用。否则,我喜欢这个答案,它非常适合我!
    【解决方案2】:

    我根据@MartinCremer 的回答做了一个指令。我认为使用指令更有意义,因为这样做,您还应该将动画添加到您的父组件(并且它接近添加动画的标准方式)。

    所以在我的animations.ts 文件中。我已经添加了动画:

        export const smoothHeight = trigger('grow', [
          transition('void <=> *', []),
          transition('* <=> *', [style({ height: '{{startHeight}}px', opacity: 0 }), animate('.5s ease')], {
            params: { startHeight: 0 }
          })
        ]);
    

    那么您应该将此动画添加到您的父组件(您要在其中使用动画的组件):

        import { smoothHeight } from '@app/animations';
        @Component({
          selector: 'app-parent',
          templateUrl: './parent.component.html',
          styleUrls: ['./parent.component.scss'],
          animations: [smoothHeight]
        })
    

    这是与@MartinCremer 组件非常接近的指令:

        import { Directive, OnChanges, Input, HostBinding, ElementRef } from '@angular/core';
    
        @Directive({
          selector: '[smoothHeight]',
          host: { '[style.display]': '"block"', '[style.overflow]': '"hidden"' }
        })
        export class SmoothHeightAnimDirective implements OnChanges {
          @Input()
          smoothHeight;
          pulse: boolean;
          startHeight: number;
    
          constructor(private element: ElementRef) {}
    
          @HostBinding('@grow')
          get grow() {
            return { value: this.pulse, params: { startHeight: this.startHeight } };
          }
    
          setStartHeight() {
            this.startHeight = this.element.nativeElement.clientHeight;
          }
    
          ngOnChanges(changes) {
            this.setStartHeight();
            this.pulse = !this.pulse;
          }
        }
    

    最后在parent.component.html里面使用指令:

        <div [smoothHeight]="yourAnimationIndicator">
          // any html content goes here
        </div>
    

    只需将yourAnimationIndicator 替换为动画应在其值更改时触发的变量。

    【讨论】:

    • 这是一个很棒的解决方案,谢谢!
    • 如果你能集成一个 stackblitz 例子就更好了。 :)
    【解决方案3】:

    你可以用一点 css 和 js 实现类似的东西:

    解决方案:

    component.ts

    import { Component, OnChanges, ViewChild, Input } from '@angular/core';
    
    @Component({
      selector: 'app-exandable',
      templateUrl: './exandable.component.html',
      styleUrls: ['./exandable.component.css']
    })
    export class ExandableComponent implements OnChanges {
    
      @Input()
      src;
    
      @ViewChild('expandable')
      expandable;
    
      ngOnChanges() {
        this.updateHeight();
      }
    
      updateHeight(delay = 0) {
        const el = this.expandable.nativeElement;
    
        setTimeout(() => {
    
          const prevHeight = el.style.height;
          el.style.height = 'auto';
          const newHeight = el.scrollHeight + 'px';
          el.style.height = prevHeight;
    
          setTimeout(() => {
            el.style.height = newHeight;
          }, 50);
        }, delay);
      }
    }
    

    css

    .expandable {
      transition: height 0.2s ease-in-out;
      overflow: auto;
    }
    

    代码:

    https://stackblitz.com/edit/angular-il71da

    【讨论】:

    • 感谢您在没有animations 依赖的情况下回答了这个问题。谢谢。
    【解决方案4】:

    过渡和不固定大小(fit-content、max-content 等)不能很好地沟通。

    以下是针对这种情况的 hack 示例:

    animations: [
        trigger('openCloseAnimation', [
          state('open', style({ maxHeight: '100px', overflow: 'auto' })),
          state('closed', style({ maxHeight: '60px' })),
          transition('* => closed', animate('0.2s')),
          transition('* => open', animate('0.5s')),
        ]),
      ],
    

    使用 MaxHeight,您的 div/container 不会超过 'max-content',但会使用 'fit-content'。

    【讨论】:

      【解决方案5】:

      @Martin Cremer 的解决方案遇到了一些跳跃问题。对于需要一点时间显示的复杂内容,触发动画时设置的目标高度并不是新内容的实际最终高度。

      所以我用了2个容器,里面的#content,用来监听高度的变化,外面的#container,我调整高度以匹配里面的(平滑过渡)。

      只要动画正在进行,在被触发后,我就会将高度变化从内部容器复制到外部容器,因此是 msSpeed 变量属性。

      在初始化时,我不这样做,因此使用了 initDone 属性。

      HTML:

      <div #container
           class="container"
           [style.transition]="animationOn?('height ease '+msSpeed+'ms'): 'none'">
          <div #content
               class="content">
              <ng-content></ng-content>
          </div>
      </div>
      

      TS:

      import { animate, style, transition, trigger } from '@angular/animations';
      import { Component, ElementRef, HostBinding, Input, OnChanges, OnInit, Renderer2, ViewChild } from '@angular/core';
      
      @Component({
        selector: 'app-smooth-height',
        templateUrl: './smooth-height.component.html',
        styleUrls: ['./smooth-height.component.scss'],
        animations: [
          trigger('grow', [
            transition('void <=> *', []),
            transition('* <=> *', [
              style({ opacity: 0 }),
              animate('{{speed}} ease')])
          ])
        ]
      })
      export class SmoothHeightComponent implements OnInit, OnChanges {
      
        @Input() trigger: any;
        @Input() msSpeed: number = 500;
      
        @ViewChild('container') container: ElementRef;
        @ViewChild('content') content: ElementRef;
      
        initDone = false;
        animationOn = false;
      
        constructor(private element: ElementRef, private renderer: Renderer2) { }
      
        ngOnInit(): void { }
      
        @HostBinding('@grow') get grow(): any {
          return { value: this.trigger, params: { speed: this.msSpeed + 'ms' } }; 
        }
      
        ngAfterViewInit(): void {
      
          const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
            const contentHeight = entries[0].contentRect.height;
            this.renderer.setStyle(this.container.nativeElement, "height", (contentHeight) + 'px');
          })
          observer.observe(this.content.nativeElement)
      
          this.initDone = true;
        }
      
        ngOnChanges(): void {
          if (this.initDone) {
            this.animationOn = true;
            setTimeout(() => { this.animationOn = false; }, this.msSpeed)
          }
        }
      }

      CSS:

      :host {
        display: block;
      }
      
      .container {
        display: grid;
        width: 100%;
        overflow-x: visible;
        margin: 0 -20px;
        padding: 0 20px;
      }
      
      .content {
        height: fit-content;
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-12-22
        • 1970-01-01
        • 2021-10-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多