iOSとAndroidで画像を光らせる演出の実装比較

iOS版で作ったアニメーション演出をAndroid版に移植したので、その時ことを備忘録として残します。

# iOS版の完成品
![stamp_short_ios.gif](https://qiita-image-store.s3.amazonaws.com/0/24765/5cd14d47-a7f2-2603-3b17-138804f581fa.gif)
※gifだとアニメーション完了の待ち合わせもしていますが、この記事では光った画像の生成と単体のアニメーションの実装方法のみ書きます。
※Android版はgifを用意してないですが、ほぼ同じにはなります。違いについては最後に。

# アニメーション実行の流れ
完成品のgifでは以下の処理を行っています。

1. 通常の画像を配置
2. 通常の画像を加工して光った画像を生成
3. ↑で生成した光った画像を通常の画像と同じ位置に、アルファ値を0にして追加（上に重ねる）
4. 通常の画像をフェードイン＋スケールアウトのアニメーションで表示させる
5. ↑のアニメーションが終わったら光った画像をフェードインで表示させる
6. ↑のアニメーションが終わったら光った画像をフェードアウト＋スケールアウトのアニメーションで消す

以下、要所要所のiOSとAndroidのコードです。書くまでもないところは省いています。
※Android版は ViewAnimator を使わせていただいています。
https://github.com/florent37/ViewAnimator

# 光った画像を生成する

## iOS(Swift)
```
// 元の画像に白色をhardLightモードでブレンド
let image = UIImage.imageFromAsset(name: "stamp_complete")?
                .colorizeImage(color: UIColor.white, blendMode: CGBlendMode.hardLight)

// ブレンドするメソッド
public func colorizeImage(color: UIColor, blendMode: CGBlendMode = CGBlendMode.plusLighter) -> UIImage? {
    UIGraphicsBeginImageContextWithOptions(self.size, false, 0)
    let context = UIGraphicsGetCurrentContext()
    let rect    = CGRect(origin: CGPoint.zero, size: self.size)
    context?.scaleBy(x: 1, y: -1)
    context?.translateBy(x: 0, y: -rect.size.height)
    context?.saveGState()
    context?.clip(to: rect, mask: self.cgImage!)
    color.set()
    context?.fill(rect)
    context?.restoreGState()
    context?.setBlendMode(blendMode)
    context?.draw(self.cgImage!, in: rect)
    let colorizedImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return colorizedImage   
}
```
## Android(Java)
```
ImageView glowView = new ImageView(getContext());
glowView.setLayoutParams(new RelativeLayout.LayoutParams(
        LayoutParams.WRAP_CONTENT,
        LayoutParams.WRAP_CONTENT
));
// 元の画像をセット
glowView.setImageResource(R.drawable.stamp_star);
glowView.setAdjustViewBounds(true);
// 画像を明るくするフィルタ
ColorFilter filter = new LightingColorFilter(Color.rgb(255, 255, 255), Color.rgb(100, 100, 100));
glowView.setColorFilter(filter);
```

iOSではUIImage自体を加工していますが、AndroidではImageViewのフィルタメソッドを使っています。

# 通常の画像をフェードイン＋スケールアウトで表示

## iOS(Swift)

```
// 登場アニメーション(スケールアウト)
// あらかじめ縮めておく
stampView.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
UIView.animate(withDuration: 0.3,
               delay: delay,
               usingSpringWithDamping: 0.5,
               initialSpringVelocity: 1.0,
               options: [.beginFromCurrentState],
               animations: {
                // もとの大きさに戻す
                stampView.transform = CGAffineTransform.identity
},
               completion: { (finished) in
})

// 登場アニメーション(フェードイン)
let alphaAnim = CABasicAnimation(keyPath: "opacity")
alphaAnim.duration  = 0.15
alphaAnim.toValue   = 1.0
alphaAnim.fillMode  = kCAFillModeForwards
alphaAnim.isRemovedOnCompletion = false
CATransaction.begin()
CATransaction.setCompletionBlock {
}
stampView.layer.add(alphaAnim, forKey: "stamp")
CATransaction.commit()
```

## Android(Java)
```
// 登場アニメーション（スケールアウト＋フェードイン）
ViewAnimator.animate(view)
        .fadeIn()
        .duration(150)
        .andAnimate(view)
        .scale(0, 1)
        .duration(200)
        .interpolator(new BounceInterpolator())
        .onStop(() -> {
        })
        .start();
```

イージングはそれぞれの方法になっているので、完全に揃ってはいないです。
Android版はViewAnimatorという素敵なライブラリのおかげで綺麗に書けます。

# 光った画像をフェードインで表示
## iOS(Swift)

```
UIView.animate(withDuration: 0.15, delay: delay, options: [.beginFromCurrentState, .curveLinear], animations: {
    imageView.alpha = 1
}, completion: { (finished) in
    UIView.animate(withDuration: 0.3, delay: delay, options: [.beginFromCurrentState, .curveEaseOut], animations: {
        imageView.transform = CGAffineTransform(scaleX: 1.8, y: 1.8)
        imageView.alpha     = 0
    }, completion: { (finished) in
    })
})
```

## Android(Java)

```
ViewAnimator.animate(view)
        .onStart(() -> {
            view.setVisibility(VISIBLE);
        })
        .fadeIn()
        .duration(150)
        .thenAnimate(view)
        .scale(1.0f, 1.8f)
        .fadeOut()
        .interpolator(new DecelerateInterpolator())
        .duration(300)
        .onStop(() -> {
        })
        .start();
```

# iOS版とAndroid版で違う点
## イージング
星が表示されるときの拡大アニメーションのイージングはそれぞれの方法を使っているので異なります。細かく調整すれば揃えられると思います。
（AndroidではBounceInterporator、iOSではanimateWithSpringDumpingを利用。）
## 加工後の星の画像
iOSの方が白が強い気がするので、こちらも細かく調整すればもっと近づけると思います。

# 感想
光った画像を用意してもらえるのであれば加工不要ですが、動的に変えられると色々試したいときや微妙に調整したい時に便利です。
コードを抜き出して部分的に載せているので、もしおかしな箇所があれば教えていただけると助かります。