vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

AngularのAnimationsで画像をスライドさせたい

はじめに

AngularのAnimationsで画像をスライドさせてみたら色々大変でした、というお話です。

※2017.7.6 各コードの解説などを追記しました

今回実現したかったことは、Unity日本語サイトトップのバナーのような、
表示中の画像に、画面外から次の画像が重なってくるアニメーションです。

http://japan.unity3d.com/

結局うまく再現できなかったため、表示中の画像が画面外にスライドすると同時に次の画像がスライドしてくる、という内容にしました。

アニメーション1(失敗)

以前は表示中の画像をフェードアウトのアニメーションで非表示にしたあと、
次の画像を表示する、という内容にしていました。

ということは、次の画像を表示するアニメーションを画面外から移動してくるようにして、
フェードアウトと同時に実行すれば完成じゃね?

と思い、そして失敗しましたorz

main-page.component.html

< section id="top-banner-area" >
  < div *ngFor='let bannerImage of bannerImages' >
    < a href='bannerImage.transitionPath'>
      < img [src]="bannerImage.imagePath" class="top-banner" [@bannerState]="bannerImage.bannerState" (@bannerState.done)="onBannerChanged()" />
    < /a>
  < /div>
< /section>
〜省略〜

main-page.component.ts

import { Component, OnInit, HostListener } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Title } from '@angular/platform-browser';
import { BannerImage } from '../banner-image' 

@Component({
  selector: 'app-main-page',
  templateUrl: './main-page.component.html',
  styleUrls: ['./main-page.component.css'],
  animations: [
    trigger('bannerState',
      [
        state('active', style({
          zindex: 1,
          display: 'block',
          transform: 'translateX(0%)',
          filter: 'brightness(100%)',
          
        })),
        state('inactive', style({
          zindex: 0,
          filter: 'brightness(20%)',
          display: 'none',
        })),
        state('beforeActive', style({
          transform: 'translateX(100%)',
          filter: 'brightness(100%)',
          zindex: 1,
        })),
        transition('active => inactive', animate('400ms ease-in')),
        transition('inactive => beforeActive', animate('1ms ease-in')),
        transition('beforeActive => active', animate('400ms ease-in'))
      ]
    )]
})
export class MainPageComponent implements OnInit {
  private bannerImages: BannerImage[];
  private activeImageNum: number;
  private nextImageNum: number;
  
  constructor(private titleService: Title) {}
  ngOnInit() {
    this.titleService.setTitle('メインページ');

    this.bannerImages = [
      {imagePath: "assets/img/banner01.jpg", transitionPath: '', bannerState: 'inactive'},
      {imagePath: "assets/img/sake.jpg", transitionPath: '', bannerState: 'inactive'}
    ];
    this.activeImageNum = -1;
    this.nextImageNum = 0;
    
    this.onBannerChanged();
  }
  @HostListener('document:keyup', ['$event'])
  onArrowKeyUp(event: any){
    if(event.keyCode === 39){ // RightArrow.
      this.nextImageNum = (this.activeImageNum < this.bannerImages.length - 1)? this.activeImageNum + 1 : 0;
      this.bannerImages[this.activeImageNum].bannerState = 'inactive';
      this.bannerImages[this.nextImageNum].bannerState = 'beforeActive';
    }
    else if(event.keyCode === 37){ // LeftArrow
      this.nextImageNum = (this.activeImageNum > 0)? this.activeImageNum - 1: this.bannerImages.length - 1;
      this.bannerImages[this.activeImageNum].bannerState = 'inactive';
      this.bannerImages[this.nextImageNum].bannerState = 'beforeActive';
    }
  }
  onBannerChanged(){
    if(this.activeImageNum === this.nextImageNum){
      return;
    }
    this.bannerImages[this.nextImageNum].bannerState = 'active';
    this.activeImageNum = this.nextImageNum;
  }
}

main-page.component.css

#top-banner-area{
    background-color: #000;
    width: 81%;
    min-width: 1200px;
    height: 600px;

}
.top-banner{
    height: 600px;
    position:absolute;
    margin-left: auto;
    margin-right: auto;
}

内容

  • active -> inactive -> beforeActive -> activeの繰り返し
  • inactiveで画像の明るさを落としたあと非表示
  • beforeActiveで画像をX軸方向に枠外まで移動 + z-indexを1にしてinactiveより前に表示
  • activeでbeforeActiveの位置から画面中央に移動

うまくいかなかったところ

  • z-indexが効かず、beforeActive -> activeで移動する画像が表示中の画像より前に表示されたり後ろに表示されたりする
  • 上記のコードで再確認したところ再現はしなかったのだが(stateが2つの場合?)アニメーションの実行完了速度が画像(配列の順番?)によって異なる現象が発生した

画像をスライドさせる

ということで、表示中の画像が画面外にスライドすると同時に次の画像がスライドしてくる、という内容のアニメーションを作ってみることにしました。

main-page.component.html

アニメーション1(失敗)と同じです。

main-page.component.ts

import { Component, OnInit, HostListener } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Title } from '@angular/platform-browser';
import { BannerImage } from '../banner-image' 

@Component({
  selector: 'app-main-page',
  templateUrl: './main-page.component.html',
  styleUrls: ['./main-page.component.css'],
  animations: [
    trigger('bannerState',
      [
        state('active', style({
          display: 'block',
          transform: 'translateX(0%)',
          filter: 'brightness(100%)',
        })),
        state('moveLeft', style({
          transform: 'translateX(-100%)',
          filter: 'brightness(20%)',
          display: 'none'
        })),
        state('fromRight', style({
          transform: 'translateX(100%)',
        })),
        state('moveRight', style({
          transform: 'translateX(100%)',
          filter: 'brightness(20%)',
          display: 'none'
        })),
        state('fromLeft', style({
          transform: 'translateX(-100%)',
        })),
        transition('active => *', animate('400ms ease-in')),
        transition('moveLeft => fromRight', animate('1ms ease-in')),
        transition('fromRight => active', animate('400ms ease-in')),
        transition('moveRight => fromLeft', animate('1ms ease-in')),
        transition('fromLeft => active', animate('400ms ease-in'))
      ]
    )]
})
export class MainPageComponent implements OnInit {
  private bannerImages: BannerImage[];
  private activeImageNum: number;
  private nextImageNum: number;
〜省略〜
  ngOnInit() {
〜省略〜
    this.bannerImages = [
      {imagePath: "assets/img/banner01.jpg", transitionPath: '', bannerState: 'inactive'},
      {imagePath: "assets/img/sake.jpg", transitionPath: '', bannerState: 'inactive'}
    ];
    this.activeImageNum = -1;
    this.nextImageNum = 0;
    
    this.onBannerChanged();
  }
  @HostListener('document:keyup', ['$event'])
  onArrowKeyUp(event: any){
    if(event.keyCode === 39){ // RightArrow.
      this.nextImageNum = (this.activeImageNum < this.bannerImages.length - 1)? this.activeImageNum + 1 : 0;
      this.bannerImages[this.activeImageNum].bannerState = 'moveLeft';
      this.bannerImages[this.nextImageNum].bannerState = 'fromRight';
    }
    else if(event.keyCode === 37){ // LeftArrow
      this.nextImageNum = (this.activeImageNum > 0)? this.activeImageNum - 1: this.bannerImages.length - 1;
      this.bannerImages[this.activeImageNum].bannerState = 'moveRight';
      this.bannerImages[this.nextImageNum].bannerState = 'fromLeft';
    }
  }
  onBannerChanged(){
    if(this.activeImageNum === this.nextImageNum){
      return;
    }
    this.bannerImages[this.nextImageNum].bannerState = 'active';
    this.activeImageNum = this.nextImageNum;
  }
}

main-page.component.css

#top-banner-area{
    background-color: #000;
    width: 100%;
    min-width: 1200px;
    height: 600px;
    margin-left: auto;
    margin-right: auto;
    overflow: hidden;
    position: relative;
}
.top-banner{
    position: absolute;
    display: block;
    margin-left: auto;
    margin-right: auto;
    height: 600px;
}
  • class:top-banner-areaで「overflow: hidden;」を指定することで、
    アニメーション中に「translateX(100%)」を実行したときもブラウザの横方向のスライドバーが表示されるのを防ぎます。
  • 最初は横方向のスライドバーの表示を抑えるためclass:top-bannerのpositionを「fixed」にしていたのですが、
    画面に固定されてしまうため、positionは「absolute」に変更し、overflowを指定しました。
  • 「overflow: hidden;」は、一番外側の要素に設定しないと効果が無いようです。
  • スライドする画像を全て同じ位置に表示するためpositionを「absolute」にする必要があり、
    他の要素を並べるためにはheightを固定値で持つ必要があります(%指定だと正しく表示できない)。

背景画像をスライドさせる

画像の高さを固定にすると、画像の比率を保つためには幅も固定にする必要がでてきます。

この場合ここまで作成したようにimgタグを使用していると、
(少なくとも今の私の知識では)画面の幅が狭いデバイスで見たとき画像の左側だけが見える、という状態になります。

これを例えば画像の中央部分を表示するよう指定するためには、
imgタグで画像を表示せず、要素の背景画像として指定する、という方法があります。

main-page.component.html

< section id="top-banner-area" >
  < div *ngFor='let bannerImage of bannerImages' >
    < a href='bannerImage.transitionPath'>
      < div [style.background-image]="'url(' + bannerImage.imagePath + ')'" class="top-banner" [@bannerState]="bannerImage.bannerState" (@bannerState.done)="onBannerChanged()">
      < /div>
    < /a>
  < /div>
< /section>
〜省略〜
  • ngForを使って画像のURLを指定したかったため、HTMLでbackground-imageを設定しています
  • background-imageのURLを「"‘url(bannerImage.imagePath)’“」のようにしてしまうと、
    「bannerImage.imagePath」がngForで得られた内容ではなく、「bannerImage.imagePath」という文字列として扱われ、エラーになります。

main-page.component.ts

import { Component, OnInit, HostListener } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Title } from '@angular/platform-browser';
import { BannerImage } from '../banner-image' 

@Component({
  selector: 'app-main-page',
  templateUrl: './main-page.component.html',
  styleUrls: ['./main-page.component.css'],
  animations: [
    trigger('bannerState',
      [
        state('active', style({
          zIndex: 1,
          display: 'block',
          transform: 'translateX(0%)',
          filter: 'brightness(100%)',
        })),
        state('moveLeft', style({
          zIndex: 0,
          transform: 'translateX(-100%)',
          filter: 'brightness(20%)',
          display: 'none'
        })),
        state('fromRight', style({
          transform: 'translateX(100%)',
        })),
        state('moveRight', style({
          zIndex: 0,
          transform: 'translateX(100%)',
          filter: 'brightness(20%)',
          display: 'none'
        })),
        state('fromLeft', style({
          transform: 'translateX(-100%)',
        })),
        state('start', style({
          transform: 'translateX(0%)',
          display: 'none'
        })),
        transition('active => *', animate('400ms ease-in')),
        transition('moveLeft => fromRight', animate('1ms ease-in')),
        transition('fromRight => active', animate('400ms ease-in')),
        transition('moveRight => fromLeft', animate('1ms ease-in')),
        transition('fromLeft => active', animate('400ms ease-in')),
        transition('* => start', animate('1ms ease-in')),
        transition('start => *', animate('1ms ease-in')),
      ]
    )]
})
export class MainPageComponent implements OnInit {
  private bannerImages: BannerImage[];
  private activeImageNum: number;
  private nextImageNum: number;
  
  constructor(private titleService: Title) {}
  ngOnInit() {
    this.titleService.setTitle('メインページ');

    this.bannerImages = [
      {imagePath: "assets/img/banner01.jpg", transitionPath: '', bannerState: 'start'},
      {imagePath: "assets/img/sake.jpg", transitionPath: '', bannerState: 'start'},
      {imagePath: 'assets/img/pic_1466247624676.jpg', transitionPath: '', bannerState: 'start'}
    ];
    this.activeImageNum = -1;
    this.nextImageNum = 0;
    
    this.onBannerChanged();
  }
  @HostListener('document:keyup', ['$event'])
  onArrowKeyUp(event: any){
    if(event.keyCode === 39){ // RightArrow.
      this.nextImageNum = (this.activeImageNum < this.bannerImages.length - 1)? this.activeImageNum + 1 : 0;
      this.bannerImages[this.activeImageNum].bannerState = 'moveLeft';
      this.bannerImages[this.nextImageNum].bannerState = 'fromRight';
    }
    else if(event.keyCode === 37){ // LeftArrow
      this.nextImageNum = (this.activeImageNum > 0)? this.activeImageNum - 1: this.bannerImages.length - 1;
      this.bannerImages[this.activeImageNum].bannerState = 'moveRight';
      this.bannerImages[this.nextImageNum].bannerState = 'fromLeft';
    }
  }
  onBannerChanged(){
    if(this.activeImageNum === this.nextImageNum){
      return;
    }
    this.bannerImages[this.nextImageNum].bannerState = 'active';
    this.activeImageNum = this.nextImageNum;
  }
}
  • 先程はうまく活躍できなかったz-indexですが、ここではactive状態の場合のみ1にして、他より前に表示されるようにしておくことで、
    移動時の画像などが表示されてしまうことによるチラツキを抑えることができます。

main-page.component.css

#top-banner-area{
    background-color: #000;
    width: 80%;
    height: 600px;
    display: block;
    margin-top: 90px;
    margin-left: auto;
    margin-right: auto;
    overflow: hidden;
    position: relative;
}
.top-banner{
    position: absolute;
    background-size: cover;
    background-position: center;
    display: block;
    width: 100%;
    height: 600px;
}

参照