iQONのアニメーションの裏側ちょっと紹介します

はじめに

iQONではアニメーションなどのアプリとしての演出の部分にこだわりを持っています。 突然ですが皆さんiQONでコーディネート画像をタップしたことはありますか?

実際のアニメーションの動き

こんな感じでコーディネートに含まれる商品がバラバラと広がって行くアニメーションを実装しています。 今回はこのアニメーションの裏側をAndroidアプリでの実装を例に少しご紹介させて頂きたいと思います。 まだこの動きを見たことがない方はダウンロードして確認してみてくださいね! iOSアプリ Androidアプリ

仕組みの概要

構成

一枚の画像が分解されて商品(以下アイテムと表現します)画像に分解されているように見えますが、実は少し違います。 最初にユーザの目に入るコーディネートのサムネイルは一枚のImageViewで実装していますが、その下に分解後の各アイテム画像のImageViewを含むRelativeLayoutが用意されていて、その中で各アイテムのImageViewがアニメーションするという構成になっています。

 

ユーザの操作と分解の動作

ユーザがどんな操作をして、分解が動作して行くかをもう少し細かくみてみましょう。 1.コーディネートのサムネイルをタップ 2.コーディネートのサムネイルをGONE 3.下のRelativeLayout内でアイテム画像のImageViewがアニメーション開始 4.分解後に☓ボタンを押して元に戻す 5.元の位置に戻るアニメーション開始 6.アニメーション完了後コーディネートのサムネイルをVISIBLE ユーザがタップする度にこの1〜6を繰り返して分解したり、もとに戻したりしています

実装の紹介

ではもう少し具体的な実装の内側を大きく2つのステップに分けて紹介していきたいと思います。 1. 初期配置とバラバラにする時の移動先の決定 2. バラバラにするアニメーションと元に戻るアニメーション

初期配置とバラバラにした時の移動先の決定

コーディネートのサムネイルがユーザに見えている時には、以下のようなアイテムの配置情報(layout)をサーバから取得して、コーディネートのサムネイルと同じ位置関係でアイテムのImageViewが配置されています。 余談ですが、もちろんコーディネートのサムネイルもこのlayout情報をもとにサーバサイドで生成され、画像サーバにアップロードされています。

layouts:[
{
    item_id: 4249463,// アイテムID
    index: 1,// Z-index
    x: 1.0356848767906093,// X座標
    y: 0.9997999119799084,// Y座標
    width: 238,// 横幅のサイズ
    height: 194,// 縦幅のサイズ
},
{
    item_id: 4249463,
    index: 2,
    x: 240.99108292274144,
    y: 0,
    width: 238,
    height: 194,
},...
]

サーバから上記のような情報を取得して各アイテム画像のImageViewを配置する時点で、アニメーションで移動する先も決定しています。 コーディネートのサムネイルと同じサイズの正方形を4×4に分割したセルが移動先だと思ってもらえれば大丈夫です。 移動先のイメージは以下のような感じです。

 

コードの一部を紹介すると以下のような感じになります。

class SplitItem {
    private ImageView imageView;
    private String itemId;
    private int index;
    
    // 移動する前の位置とサイズ
    private float orgWidth, orgHeight;
    private float orgX, orgY;
    
    // 移動先の位置とサイズ
    private float dstWidth, dstHeight;
    private float dstX, dstY;
    
    public SplitItem(JSONObject layout) {
        itemId = layout.getString("item_id");
        orgWidth = layout.getDouble("width");
        orgHeight = layout.getDouble("height");
        orgX = layout.getDouble("x");
        orgY = layout.getDouble("y");
        index = layout.getInt("index");
        
        // 移動先の情報を算出
        initDstData();
    }
    
    // 移動先の情報を算出するメソッド
    private void initDstData() {
        // 4*4のセルに分割した時の移動先のサイズと座標を算出
        int cellLength = thumbnailWidth / 4;
        int xIndex = index % 4;
        int yIndex = index / 4;
        float sizeRatio;
        if (orgHeight > orgWidth) {
            sizeRatio = cellLength / orgHeight;
            dstHeight = cellLength;
            dstWidth = orgWidth * sizeRatio;
        } else {
            sizeRatio = cellLength / orgWidth;
            dstHeight = orgHeight * sizeRatio;
            dstWidth = cellLength;
        }
        dstX = cellLength * xIndex + (cellLength - dstWidth) / 2;
        dstY = cellLength * yIndex + (cellLength - dstHeight) / 2;
    }
    
    public void open() {
        // 初期位置から移動先の位置にアニメーションする処理(後述)
    }
    
    public void open() {
        // 移動先の位置から初期位置にアニメーションする処理(後述)
    }
}

バラバラにするアニメーションと元に戻るアニメーション

初期配置情報とアニメーションで動く先が決まったので、実際にコーディネートの画像をバラバラにするアニメーションを紹介していきます。

private ArrayList items;

@Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    items = new ArrayList();
    
    // サーバから返却されるJSONのlayoutsから各アイテムを初期化する処理
    JSONArray layouts = coordinate.getLayoutJSONArray();
    for (int i = 0; i < layouts.length(); i++) {
        JSONObject layout = layouts.getJSONObject(i);
        SplitItem splitItem = new SplitItem(layout); items.add(splitItem);
    }
    
    // 省略
    
    // サムネイルのタップイベントの設定
    thumbnailImageView.setOnClickListener(new View.OnClickListener() {
        @Override public void onClick(View v) {
            v.setVisiblity(View.GONE);
            for (SplitItem item : items) {
                item.open();
                
                // 各アイテムを移動先へアニメーションする
            }
        }
    });
    
    // 一度バラバラに移動したアイテム画像を元に戻すボタン
    closeButton.setOnClickListener(new View.OnClickListener() {
        @Override public void onClick(View v) {
            for (SplitItem item : items) {
                item.close();
                
                //各アイテムを移動元へアニメーションする
            }
        }
    });
}

バラバラにするアニメーション

class SplitItem {
    // open時のアニメーションの実行
    public void open() {
        PropertyValuesHolder holderX = PropertyValuesHolder.ofFloat("translationX", 0f, dstX - orgX);
        PropertyValuesHolder holderY = PropertyValuesHolder.ofFloat("translationY", 0f, dstY - orgY);
        PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1f, dstWidth / orgWidth);
        PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1f, dstHeight / orgHeight);
        ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(imageView, holderX, holderY, scaleX, scaleY);
        objectAnimator.setInterpolator(new DecelerateInterpolator());
        objectAnimator.setDuration(duration); objectAnimator.start();
    }
}

上記のようにObjectAnnimatorを使って、移動先のセルに向かってサイズを変えながら移動するというアニメーションを実装しています。 元に戻る時のアニメーション 移動した後に☓ボタンを押せば元の位置に戻ります。もとに戻るときのコードもご紹介します。

class SplitItem {
    // close時のアニメーションの実行
    public void close() {
        PropertyValuesHolder holderX = PropertyValuesHolder.ofFloat("translationX", dstX - orgX, 0f);
        PropertyValuesHolder holderY = PropertyValuesHolder.ofFloat("translationY", dstY - orgY, 0f);
        PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", dstWidth / orgWidth, 1f);
        PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", dstWidth / orgWidth, 1f);
        ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(imageView, holderX, holderY, scaleX, scaleY);
        objectAnimator.setDuration(duration);
        objectAnimator.addListener(new Animator.AnimatorListener() {
            @Override public void onAnimationStart(Animator animation) { }
            
            @Override public void onAnimationEnd(Animator animation) {
                // コーディネートのサムネイルを表示VisiblityをVISIBLEにする処理(今回は省略)
            }
            
            @Override public void onAnimationCancel(Animator animation) { }
            
            @Override public void onAnimationRepeat(Animator animation) { }
        });
        
        objectAnimator.start();
    }
}

先ほどのopenメソッドとは反対の処理ですが、ポイントはAnimationListenerのonAnimationEndの部分です。今回は省略しますがアニメーションが終了したらGONEにしていたサムネイルのvisiblityをVISIBLEにもどして最初にユーザ見ていた状態に戻す処理を実装しています

まとめ

配置情報の管理はチームで協力

この機能の肝とも言える部分ですが、ユーザが作ってくれたコーディネートをどんなフォーマットでデータ管理するかをバックエンドチームと協力してDBで管理しています。

アニメーションの処理自体はそこまで複雑ではない

ご紹介したとおり配置情報の仕樣さえしっかりチームで共有できてれば、アニメーションの処理自体はそこまで複雑ではありません

実際に大変なところ

今回はかなり省略しましたが、実際には画像をネットワーク越しに取得してImageViewに設定しなければなりません。 そうなるとListViewなどで同じ表現を実装する場合、スクロールをスムーズにしてもらうためにはアイテム一つ一つの画像の処理を相当工夫する必要があります。(1つのコーディネートにつきアイテムが約15個あるため全部、その都度素直に処理してしまうと高速にスクロールされると大変なことになります) このあたりぜひ聞いてみたいという方いらっしゃいましたら気軽にオフィスに遊びにきてください。 大変だった点、この機能の誕生の裏話などたくさんご用意してお待ちしております。

最後に

ユーザがつい押したくなってしまうような表現を求めてアプリ、バックエンドに関わらずチームで実装の壁を超えて日々挑戦しております。 アイデアをどんどん形にしてユーザに届けたい方、ユーザにワクワクしてもらえるようなアプリを一緒に作りましょう。 興味のある方はこちらでお待ちしております! https://www.wantedly.com/projects/7595