🙌

IonicとAngularで作るストーリーズ風UI

2021/12/11に公開

はじめに

この記事は、Startup Angular #2で登壇した「Ionic×Angularで作るストーリーズ風UI」の内容をIonicアドベントカレンダー2021用に記事した記事です。

イベントのリンク

https://voicy.connpass.com/event/229367/

スライド

https://speakerdeck.com/scrpgil/ionicxangulardezuo-ru-sutorizufeng-ui

追記

自社でも開発ブログを作ったので、そちらにも記載しました。
https://kineca.dev/Ionic-Angular-UI-10b87e786113459d9bfeec91ec3bb4da

概要

イベント駆動開発で、IonicとAngualrで、zuck.jsと似たようなものを実装しました。

この記事では、その実装内容について紹介しています。

完成したサイト

https://ionic-angular-stories-ui.netlify.app/

ソースコード

https://github.com/scrpgil/ionic-angular-stroies-ui

ストーリーズとは

Instagramの一機能で、写真 or 動画をスライド形式で表示する機能です。一定時間で自動で切り替わる、一定時間断つと消えるという特徴があり、モバイル向けUIとして非常に人気の高いUIです。
Instagram以外でも、Shazamや、Google Photo、TwitterのFleetなどで似たようなUIが実装されていました。

詳しくは以下のリンクで
Mobile design trends to watch out for in 2020

ストーリーズを構成している要素

ストーリーズ機能は、一つの機能の中に色々なUI/UXの要素がありますが、本記事では、以下の3点をとりあげます。

  • ポップイン表示
  • スライドのアニメーション
  • ストーリー自動切り替えとプログレスバー

ポップイン表示

ストーリーを開くときのアニメーションです。クリックした場所から拡大されて開くのが特徴です。

スライドアニメーション

ストーリーが切り替わるときのアニメーションです。立体的なアニメーションが特徴です。

ストーリー自動切り替えとプログレスバー

一定時間経過で自動的にストーリーを切り替えるのと、時間経過を表すプログレスバーです。

Ionicとは?

上記のストーリーズ機能を構成する要素ですが、Ionicを使うと割と簡単に実装できます。
Ionicは、モバイル向けUIコンポーネントライブラリで、ポップオーバーや、モーダル、アラートなどモバイル向けアプリでよく見るようなUIを提供してくれています。Angualrで使う場合には、@ionic/angularというラッパーも用意されています。
https://ionicframework.com/

今回作ったサイトでは、モーダル(ion-modal)や、スライド(ion-slides)を利用して、

例)ion-modal

例)ion-slides

ion-modalを使ったポップインの実装

まずは、ポップインを実装します。Ionicのモーダルはデフォルトでは、下からスライドするアニメーションですが、カスタマイズ可能です。
サンプルコードは公式においてあります。
https://ionicframework.com/docs/utilities/animations#modals

公式が提供しているサンプルのアニメーションは画面中央から拡大されるアニメーションです。これを改造して、ストーリーズ風の画面拡大のモーダルを作っていきます。

まず、クリックした位置を取得します。

クリック位置を取得するには、イベントペイロード($event)を渡す方法があります。
下のHTMLコードの(click)="ShowModal($event, idx)"のような書き方をすれば、TypeScript側でクリック位置の取得ができます。

    <div
      class="story"
      *ngFor="let story of stories; let idx = index"
      tappable
      (click)="showModal($event, idx)" 
    >

取得したクリック位置を使って、モーダル拡大をクリックした位置から表示されるようにします。クリック位置よりtranslateX、translateYの位置を指定することで実装可能です。

  // モーダル作成
  async showModal(ev: any, idx: number) {
    const enterAnimation = this.createMyFadeInAnimation(ev);
    // eslint-disable-next-line arrow-body-style
    const leaveAnimation = (baseEl: any) => {
      return enterAnimation(baseEl).direction('reverse');
    };
    // 省略
  }
  createMyFadeInAnimation(ev) {
    const enterAnimation = (baseEl: any) => {
      const backdropAnimation = this.animationCtrl
        .create()
        .addElement(baseEl.querySelector('ion-backdrop'))
        .fromTo('opacity', '0.01', 'var(--backdrop-opacity)');

      // モーダルの最初の位置を計算
      // clientX、clientYでクリック位置を取得
      const x = ev.clientX - Math.round(window.outerWidth / 2);
      const y = ev.clientY - Math.round(window.outerHeight / 2);

      const wrapperAnimation = this.animationCtrl
        .create()
        .addElement(baseEl.querySelector('.modal-wrapper'))
        .keyframes([
          {
            offset: 0,
            opacity: '1',
            transform: `translate(${x}px, ${y}px) scale(0)`,
          },
          {
            offset: 1,
            opacity: '1',
            transform: `translate(0) scale(1)`,
          },
        ]);

      return this.animationCtrl
        .create()
        .addElement(baseEl)
        .easing('ease-out')
        .duration(300)
        .addAnimation([backdropAnimation, wrapperAnimation]);
    };
    return enterAnimation;
  }

上記コードで、ポップイン表示は完成です。

ion-slidesを使ったスライドアニメーション

こちらは、Ionic Slideを使って実装します。デフォルトは横スライドですが、こちらもアニメーションのカスタマイズが可能です。
立体的なアニメーションとなると実装が難しそうですが、公式で似たようなcubeアニメーションのコードが提供されているので、こちらをカスタマイズすることで実装が可能です。
https://ionicframework.com/docs/api/slides#cube

実装的にはcubeアニメーションのIonicSlideをフルスクリーンで表示すれば、まあまあ良い感じになりますが、画面端が見切れたりなど、少し違和感があります。
なので、Ionic Slideのイベントハンドラを使いつつ、ドラッグ中は少し画像を縮小するようなことを行います。

まず、HTML側でionSlideDrag、ionSlideTouchEndの関数を定義します。

<ion-content>
  <ion-slides
    #slides
    [options]="slideOpts"
    (ionSlideDrag)="slideStart()"
    (ionSlideTouchEnd)="slideEnd()"
    (ionSlideNextEnd)="slideNextEnd()"
    (ionSlidePrevEnd)="slidePrevEnd()"
    (ionSlideDidChange)="slideDidChange()"
  >

TypeScript側では、変数isDragを用意し、ドラッグ中はtrueになるようにします。

  isDrag: boolean = false;
  slideStart() {
    this.isDrag = true;
  }
  slideEnd() {
    this.isDrag = false;
  }

公式のアニメーションを下のコードの部分を変更すればドラッグ中だけ画像を縮小させることが可能です。

      setTranslate: (): void => {
        const swiper: any = this.slides.el.swiper;     
	// **省略**
        // ドラッグ開始時にはcubeを少し縮小させる
        const zFactor = this.isDrag ? -70 : 0;
        $wrapperEl.transform(
          `translate3d(0px,0,${zFactor}px) rotateX(${
            swiper.isHorizontal() ? 0 : wrapperRotate
          }deg) rotateY(${swiper.isHorizontal() ? -wrapperRotate : 0}deg)`
        );

        // ドラッグ開始時と終わるときでアニメーションスピードを変える
        $el.find('.swiper-wrapper').transition(this.isDrag ? 100 : 300);
      },

以上で、スライドのアニメーションも完成です。

プログレスバーとアニメーションイベントのハンドリング

最後にプログレスバーのアニメーションを作成します。
これは、IonicやAngularの機能ではなくて、CSSアニメーションを利用しています。CSSアニメーションも、アニメーション終了のタイミングをハンドリングすることが可能で、これを利用すれば、一定時間表示したら次の写真に切り替えるというストーリーズのUIを実現可能です。

下のコードのように(animationend)と書くことでアニメーション終了時に特定の処理を実行することが可能です。

            <div
              class="progress-bar-white"
              [class.runninng]="!isPause && progressIdx === activeItemIndex && storyIdx === activeIndex"
              [class.viewing]="progressIdx < activeItemIndex && storyIdx === activeIndex"
              [class.restart]="isRestart"
              [style.animation-duration]="item.length + 's'"
              (animationend)="onAnimationEnd()"
            ></div>

アニメーションの表示秒数も [style.animation-duration]で動的に指定することが可能です。
例えば、動画などは、再生時間に応じて、プログレスバーのアニメーションスピードを調整する必要があるので、 (loadedmetadata) で動画の再生時間を取得し、アニメーションスピードを動的に切り替えることも可能です。

完成したプログレスバーは以下のような感じです。

おわりに

以上で、IonicとAngularを使ったストーリーズ風UIの実装は終わりです。
この記事ではざっくりと実装を紹介しています。より詳しく実装をみたい場合は、以下のリポジトリでソースコードを公開しているので、よかったら見てください。
https://github.com/scrpgil/ionic-angular-stroies-ui

あとTwitterやってます。Ionic関連やアスキーアートについて呟いてるので、良かったらフォローしてください。
https://twitter.com/scrpgil

Discussion