概要
Twitter が「いいね」ボタンのアニメーションをどのように実装しているのか調査してみました
You can say a lot with a heart. Introducing a new way to show how you feel on Twitter: https://t.co/WKBEmORXNW pic.twitter.com/G4ZGe0rDTP
— Twitter (@Twitter) 2015年11月3日
調べてみた結果
GitHub で公開されている Twitter Kit for Android のコミット「Add like heart animations for API 21+(2015 年 11 月 9 日)」によると、AnimatedStateListDrawable を用いているようでした。
AnimatedStateListDrawable とは
API レベル 21 (Lollipop) から追加された機能で、画像をパラパラ漫画の要領でアニメーションにします。
Twitter の「いいね」ボタンは res/drawable-xhdpi
内にある 60 枚の PNG 画像を使用していました。
Android 4.4 (KitKat) 以下の対応はどうしているのか
Twitter は res/drawable
と res/drawable-v21
パッケージ直下に同名の XML ファイルを作成して、中身を <selector >
タグと <animated-selector >
タグで書き分ける事により、Android 4.4 以下でアニメーション非対応とし、Android 5.0 以上でアニメーションを有効にしていました。
ボタンの ON/OFF
ボタン ON 時のアニメーションが実施されている途中にボタンをタップすると、アニメーションがキャンセルされ OFF 状態に切り替わりました。
サンプル
Twitter Kit for Android の ImageButton を拡張したクラスとコミット「Add like heart animations for API 21+」を参考にアニメーション付きボタンを作ってみます。
リソース
39 枚の PNG 画像を用いてボタン ON 時のアニメーションを作成します。

コード
<resources>
<declare-styleable name="AnimationImageButton">
<attr name="state_toggled_on" format="boolean" />
</declare-styleable>
</resources>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/animation_on"
app:state_toggled_on="true">
<bitmap android:src="@drawable/star_animation_01" />
</item>
<item android:id="@+id/animation_off">
<bitmap android:src="@drawable/off" />
</item>
<transition
android:fromId="@+id/animation_off"
android:toId="@+id/animation_on">
<animation-list>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_01" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_02" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_03" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_04" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_05" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_06" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_07" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_08" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_09" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_10" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_11" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_12" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_13" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_14" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_15" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_16" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_17" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_18" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_19" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_20" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_21" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_22" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_23" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_24" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_25" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_26" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_27" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_28" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_29" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_30" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_31" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_32" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_33" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_34" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_35" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_36" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_37" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_38" />
</item>
<item android:duration="30">
<bitmap android:src="@drawable/star_animation_39" />
</item>
</animation-list>
</transition>
</animated-selector>
<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:drawable="@drawable/star_animation_01" app:state_toggled_on="true" />
<item android:drawable="@drawable/off" android:state_enabled="false" />
</selector>
package com.sample.application;
import android.content.Context;
import android.support.v7.widget.AppCompatImageButton;
import android.util.AttributeSet;
/**
* アニメーション付き {@link AppCompatImageButton}
*/
public class AnimationImageButton extends AppCompatImageButton {
private static final int[] STATE_TOGGLED_ON;
boolean isToggledOn;
public AnimationImageButton(Context context) {
this(context, null);
}
public AnimationImageButton(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AnimationImageButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setImageResource(R.drawable.star_animation);
setToggledOn(false);
}
public int[] onCreateDrawableState(int extraSpace) {
int[] drawableState = super.onCreateDrawableState(extraSpace + 2);
if (isToggledOn) {
mergeDrawableStates(drawableState, STATE_TOGGLED_ON);
}
return drawableState;
}
public boolean performClick() {
toggle();
return super.performClick();
}
public void setToggledOn(boolean isToggledOn) {
this.isToggledOn = isToggledOn;
setContentDescription(isToggledOn ? "ON" : "OFF");
refreshDrawableState();
}
public void toggle() {
this.setToggledOn(!isToggledOn);
}
public boolean isToggledOn() {
return isToggledOn;
}
static {
STATE_TOGGLED_ON = new int[]{R.attr.state_toggled_on};
}
}
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.sample.application.AnimationImageButton
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<!-- アニメーション付きボタン -->
</android.support.constraint.ConstraintLayout>
package com.sample.application;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
}
}
実行結果
関連項目
iOS と Android、React Native で高品質なアニメーションを簡単に作成できる Lottie というライブラリがあります。