(この記事は Fran Dios が 2017年10月5日に The Web Tub に投稿した Improve UX with Swiping Tab Bar Using Onsen UI for Vue の翻訳です。)

最近では、モバイルデバイスをターゲットとしたCordovaアプリやプログレッシブウェブアプリを頻繁に見かけます。ユーザー体験・ユーザーエンゲージメントを向上させるためにはネイティブ同様の外見と操作性をユーザーに提供することが重要となりますが、そう簡単に実現できるものではありません。チェックボックスやラジオボタンなどにスタイルを適用してそれなりの見栄えにするだけで事なきを得たのはすでに過去の話です。私たちが次の目標にしているのは、ユーザーとの対話(効果的な演出方法など) に着目したアプリの質の向上です。

ここで紹介するスワイプ対応タブバー(スワイプに連動するタブバー)では、複数のページにコンテンツを分散させ、目的のページをスワイプ操作で表示できるようにします。また、ユーザーの操作に応じてアプリの外見が変わるような演出も加えてみます。一聞すると複雑な処理が必要かと思われますが、このような処理もVue.jsを使用すれば簡単に行えます。

はじめてみよう

まず、スワイプに連動するタブバーコンポーネントが必要です。多くの選択肢があるかと思いますが、ここでは Onsen UI for Vue 提供のタブバーを使用します。このタブバーではスワイプ操作中にさまざまな処理を組み込むことができます。なお、この他にもOnsen UIではAndroid・iOS対応のVue向けコンポーネントを各種取り揃えております

Monacaを利用する場合

Monaca Proプラン以上で利用可能な、Monaca Localkit または Monaca CLI で 「Onsen UI and Vue.js」カテゴリのテンプレートをベースにしてプロジェクトを作成します。
クラウドIDEは Onsen UI for Vue プロジェクトの作成に対応しておりません。

Monacaを利用しない場合

既存のプロジェクトであればNPMまたはYarnを使用してインストールすることができます。

$> npm install onsenui vue-onsenui --save-dev
$> yarn add onsenui vue-onsenui -D

次に、必要なファイルをアプリ内に記述します。

import 'onsenui/css/onsenui.css'; // Webpack CSS import
import 'onsenui/css/onsen-css-components.css'; // Webpack CSS import
import VueOnsen from 'vue-onsenui';
Vue.use(VueOnsen);

新規であれば、Vue CLIを使用してプロジェクトを作成できます。また、VueXを含む多数の機能も任意で追加できます。

$> vue init OnsenUI/vue-cordova-webpack # For Cordova apps
$> vue init OnsenUI/vue-pwa-webpack # For PWA

以上の処理を行えば、<v-ons-tabbar> を含む他のコンポーネントもアプリ内で使用できます

スワイプ対応タブバーの記述方法(Vue向け)

基本のスワイプ対応タブバーの記述はとても簡単です。Vueテンプレート内に次のように記述します。

<v-ons-tabbar swipeable :tabs="tabs" />

swipeable 属性を使用してスワイプ操作のオンオフを適宜設定できます。tabs はオブジェクトを格納するための配列です。格納する各オブジェクトにはタブの外見やコンテンツ(pagelabelicon プロパティー)が保持されています。このコンポーネントのリファレンスは こちら です。

他にも多様なプロパティー が用意されており、これらを使用すればデフォルトの挙動を変更したり、特別なカスタマイズを追加したり、独自のスタイルを適用したりすることができます。次のサンプルアプリでは on-swipe プロパティー を使用しUIの色を変化させています。色の変化はスワイプ時に起こるようにし、色の変遷は徐々に起こるようにします。このようにすれば、現在表示されているセクション(タブ)をユーザーが認識しやすくなります。(この記事の最後にサンプルアプリへのリンクを貼っています。)

色の変遷と補間
<template>
  <v-ons-page>
    <v-ons-toolbar :style="swipeTheme">
      <div class="center">Swiping Tab Bar</div>
    </v-ons-toolbar>

    <v-ons-tabbar
      swipeable
      position="top"
      :tabs="tabs"
      :on-swipe="onSwipe"
      :tabbar-style="swipeTheme"
    />
  </v-ons-page>
</template>

<script>
import Home from './pages/Home.vue';
import Forms from './pages/Forms.vue';
import Animations from './pages/Animations.vue';
// Just a linear interpolation formula
const lerp = (x0, x1, t) => parseInt((1 - t) * x0 + t * x1, 10);
// RGB colors
const red = [244, 67, 54];
const blue = [30, 136, 229];
const purple = [103, 58, 183];
export default {
  data () {
    return {
      colors: red,
      animationOptions: {},
      tabs: [
        {
          page: Home,
          label: 'Home',
          theme: red
        },
        {
          page: Forms,
          label: 'Forms',
          theme: blue
        },
        {
          page: Animations,
          label: 'Anim',
          theme: purple
        }
      ]
    };
  },
  computed: {
    swipeTheme() {
      return {
        backgroundColor: `rgb(${this.colors.join(',')})`,
        transition: `all ${this.animationOptions.duration || 0}s ${this.animationOptions.timing || ''}`
      }
    }
  },
  methods: {
    onSwipe(index, animationOptions) {
      // Apply the same transition as v-ons-tabbar
      this.animationOptions = animationOptions;
      // Interpolate colors
      const a = Math.floor(index), b = Math.ceil(index), ratio = index % 1;
      this.colors = this.colors.map((c, i) => lerp(this.tabs[a].theme[i], this.tabs[b].theme[i], ratio));
    }
  }
};
</script>

上記のコードでは、v-ons-page , v-ons-toolbar , v-ons-tabbar を使用してツールバーとタブバーから構成されるページを作成しています。また、tabs プロパティーにはタブが配列形式で格納されています。タブバーコンポーネントでは pagelabel プロパティーを使用してタブのコンテンツと外見を設定しますが、カスタムプロパティーを使用することもできます。このサンプルアプリではカスタムプロパティー「theme」を使用しています。theme はRGB色を格納するための配列形式のプロパティーです。なぜ theme に対してこのような形式を使用するのかは後ほどわかります。

注目していただきたいのは、算出プロパティーである swipeTheme が、ツールバー(style プロパティーを介して)とタブバー(tabbar-style プロパティーを介して)に渡されている点です。この swipeTheme が変更されるたびに両コンポーネントのスタイルが更新される仕組みになっています。また、onSwipe メソッドが on-swipe プロパティーで定義され、ユーザーが画面上で指を移動させるたびに呼び出されるようになっています。それではUIの色を変化させる仕組みを説明します。

線形補間

線形補間(コンピューターグラフィックスの世界では「lerp」と呼ばれています)とは、簡単に説明すると、2つの値の間に存在する中間点を算出するための式です(色の算出にも応用できます)。たとえば、画面上のドットを始点(x0)から終点(x1)に徐々に移動させたいとします。式に代入するものは始点の値、終点の値および「完了の割合」(終点までの距離を示した比率とも言えます。また、アルファ値と呼ばれていることもあります)です。これにより、特定の時点においてドットが置かれるべき位置を取得できます。

const lerp = (x0, x1, r) => (1 — r) * x0 + r * x1;

たとえば、x0 から x1 の間に3つの点を生成するものとします(始点と終点は除外します)。1つ目の点を r = 0.25 に(25%完了)、2つ目の点を r = 0.5 に(50%完了・中間点)、3つ目の点を r = 0.75 (75%完了)に設定します。このように異なる割合を設定すれば、点をいくつでも算出することができます。

ここまでは物理的な距離に関しての適用例を挙げましたが、スワイプ操作に応じた色の変遷についても応用することができます。これを実現するには、色を数値に変換し、ページ移動するときのスワイプの進捗割合を取得する必要があります。RGBはR,G,Bの3つの値から構成されるため、それぞれの値を補間します。他の色空間の補間法に関しては こちら をご確認ください(RGBと比べると、補間には複雑な計算が必要となります)。

また、タブバーコンポーネントには、スワイプ時に実行される onSwipe 関数が設定されており、この関数によって現在のページの表示位置を示す少数形式の値を取得できます。たとえば、この値が 1.65 の場合、ページ1からページ2に遷移するためにスワイプが65%ほど進んだことを示します(r = 0.65)。

よって、各タブにRGBそれぞれの値(配列)が割り当てられていれば、現在のスワイプ度合いに基づいて各値を補間することができます。

onSwipe(index, animationOptions) {
  this.animationOptions = animationOptions;
  const x0 = Math.floor(index),
    x1 = Math.ceil(index),
    ratio = index % 1;
  this.colors = this.colors.map((c, i) =>
    lerp(
      this.tabs[x0].theme[i],
      this.tabs[x1].theme[i],
      ratio
    )
  );
}

上記のコードでは、色情報配列が this.colors に割り当てられています。これに連動する形で、算出プロパティーの this.swipeTheme は割り当てられた色情報に基づいたCSSを作成します。

また、animationOptions が第二引数として使用されています。タブバーではスワイプ操作の完了時、指の速度(velocity)に応じたアニメーション処理を行います(なお、スワイプ中はこの animationOptions は空になっています)。速度は animationOptions 内の durationtiming から算出され(3次ベジェ曲線)、CSSトランジションを実行するときに使用されます。この方法により、すべてのアニメーション(ページ遷移、タブの下線の移動、色の変遷)を同期させることができます。

最後に

宣言的な構造を採用しているVueフレームワークだからこそ上記のような処理が簡単に行えました(DOMから要素を取得したり、スタイルを手動で変更したりする手間が省け、特定のプロパティーを更新するだけで上記のような処理が行えます)。

上記のコードを含むすべてのコードは こちら で確認できます。線形補間の概念を基礎として、他にも多数の補間処理を行っています。また、Onsen UI for Vueの詳細を知りたい方は こちら の公式サイトをご確認ください。

他のアニメーションの提案や開発事例を共有されたい方はぜひOnsen UIの Twitter までお声をお寄せください。

Happy coding!

翻訳者紹介
渡部正和/Masakazu Watabe
フリーランスの翻訳家。インターネット・通信業界で10年ほど翻訳を経験したあと、2013年からフリーランスに。直訳ではない、わかりやすい翻訳を心がけています。校正など翻訳全般に関するご相談がありましたら watabe08@gmail.com(渡部) まで。