Androidアプリ開発 RecyclerView Google Plusのようなリストアニメーションを実装する

2017年04月12日(編集2017年04月12日)
このエントリーをはてなブックマークに追加

Androidアプリで一覧データのUI(ユーザーインターフェース)を作成する場合は、ListViewかRecyclerViewを使います。
この記事は、RecyclerViewでGoogle Plusのようなリストアニメーションを実装する方法を記載した記事です。

環境はAndroid 7.1 (API level 25) です。

環境

  • OS X El Capitan
  • android sdk 25
  • Oracle jdk version 1.8.0_72
  • Android Studio 2.3.0

難易度

中級者向け

サンプルコード

Android-RecyclerView-Demo

RecyclerView

RecyclerViewは一覧表示の実装に利用するクラスです。 ListViewを使うより、表現力のある、柔軟なユーザーインターフェースを作成できます。
今回の記事で説明をするGoogle Plusのようなリストアニメーションを実装するには、RecyclerViewの一覧表示の実装方法をきちんと理解している必要があります。理解不足の場合、まずはこちらの記事を読んでRecyclerViewの使い方を理解してください。

動画

記事の内容を実装したGoogle Plusのようなリストアニメーションアプリの動画です。

実装

RecyclerViewの一覧処理を実装します。RecyclerView.Adapterを継承したクラスでアニメーションを実装します。クラス名はRecyclerViewGooglePlusFragmentAdapterにします。

{project_folder}/ui/AutoScrollRecyclerViewFragment.java
public class RecyclerViewGooglePlusFragmentAdapter extends RecyclerView.Adapter<RecyclerViewGooglePlusFragmentAdapter.ViewHolder> {
    public static final String TAG = "Adapter";

    private final Context context;
    private final List<Note> noteList;
    private int lastPosition;
    private final RecyclerViewGooglePlusFragment.OnFragmentInteractionListener listener;

    public RecyclerViewGooglePlusFragmentAdapter(Context context, List<Note> items, RecyclerViewGooglePlusFragment.OnFragmentInteractionListener listener) {
        this.context = context;
        this.noteList = items;
        this.listener = listener;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fragment_recycler_view_google_plus_item, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(final ViewHolder holder, int position) {
        holder.note = noteList.get(holder.getAdapterPosition());
        holder.title.setText(holder.note.title);
        holder.summary.setText(holder.note.summary);

        holder.view.setOnClickListener(v -> onClickView(holder));

        holder.plus.setOnClickListener(v -> onClickPlus(holder));

       +if (lastPosition < holder.getAdapterPosition()) {
            startAnimation(holder.view, holder.getAdapterPosition());
            lastPosition = holder.getAdapterPosition();
        }
    }

    /**
     * click view
     *
     * @param holder ViewHolder
     */
    private void onClickView(final ViewHolder holder) {
        if (null != listener) {
            listener.onClickItem(holder.note);
        }
    }

    /**
     * click plus
     *
     * @param holder ViewHolder
     */
    private void onClickPlus(final ViewHolder holder) {
        holder.note.onClickPlus();
        if (holder.note.onPlus) {
            holder.plus.setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.button_google_plus));
        } else {
            holder.plus.setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.button_circle));
        }
    }

    /**
     * start Animation
     *
     * @param view
     * @param position
     */
   +private void startAnimation(View view, int position) {
        if (position > 2) {
            Log.d(TAG, "animation start");
            Animation animation = AnimationUtils.loadAnimation(context, R.anim.up_from_bottom);
            animation.setInterpolator(new LinearOutSlowInInterpolator());
            animation.setStartTime(500);
            view.startAnimation(animation);
        }
    }


    @Override
    public int getItemCount() {
        return noteList.size();
    }

   +@Override
    public void onViewDetachedFromWindow(final ViewHolder holder) {
        super.onViewDetachedFromWindow(holder);
        ViewCompat.animate(holder.view).cancel();
        Log.d(TAG, "animation cancel");
    }

    public class ViewHolder extends RecyclerView.ViewHolder {
        public final View view;
        public final TextView title;
        public final TextView summary;
        public final ImageView image;
        public final AppCompatImageView plus;
        public final AppCompatImageView share;
        public Note note;
        public int viewType;

        public ViewHolder(View view) {
            super(view);
            this.view = view;
            this.viewType = viewType;
            title = (TextView) view.findViewById(R.id.title);
            summary = (TextView) view.findViewById(R.id.summary);
            image = (ImageView) view.findViewById(R.id.image);
            plus = (AppCompatImageView) view.findViewById(R.id.plus);
            share = (AppCompatImageView) view.findViewById(R.id.share);
        }
    }
}
        

■ 実装の解説

1. onBindViewHolder

onBindViewHolderメソッドは、指定された位置にデータを表示するために、RecyclerViewによって呼び出されます。

public void onBindViewHolder(final ViewHolder holder, int position) {}
        

このメソッドで、行のViewの内容を更新します。なので、アニメーションはこのメソッド内で実装します。

また、引数のpositionを利用するとLintの警告で"Do not treat position as fixed; only use immediately…"が表示されます。
リストの位置情報が必要な場合は、ViewHolder#getAdapterPosition()を使用します。

@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
    holder.note = noteList.get(holder.getAdapterPosition());
}
        

positionを使うとクラッシュする可能性があるので、なるべくholder.getAdapterPosition()を使ってください。

2. アニメーション

行のViewが画面下から上にスライドアップするアニメーションを実装します。

if (lastPosition < position) {
    startAnimation(holder.view, position);
    lastPosition = position;
}
        

上記のコードは、1度アニメーションで表示されたviewを、2度アニメーションさせないようにしています。こうすることで、スクロールで画面上部に戻す時に列のviewが再びアニメーションすることを防いでいます。

3. アニメーション実装

アニメーションの実装は、xmlを使います。

private void startAnimation(View view, int position) {
    if (position > 2) {
        Log.d(TAG, "animation start");
        Animation animation = AnimationUtils.loadAnimation(context, R.anim.up_from_bottom);
        animation.setInterpolator(new LinearOutSlowInInterpolator());
        animation.setStartTime(500);
        view.startAnimation(animation);
    }
}
        

position > 2なので、3列目のviewからアニメーションを実行するようにしています。

AnimationUtilsは、View Animation(API Level 1)のアニメーションを扱います。res/anim/xxxx.xmlに配置されたアニメーションのxmlを読み込むことができます。

InterpolatorにはLinearOutSlowInInterpolatorを設定します。Interpolatorはアニメーションの補間クラスです。LinearOutSlowInInterpolatorを使うと、オブジェクトは画面の外からフルスピードで画面に入り、停止点に向かってゆっくりと減速します。画面外から中へのアニメーションを実装するとき、LinearOutSlowInInterpolatorを使うと自然なアニメーションになります。

4. onViewDetachedFromWindow

onViewDetachedFromWindowメソッドで、アニメーションのキャンセル処理を実装します。

@Override
public void onViewDetachedFromWindow(final ViewHolder holder) {
    super.onViewDetachedFromWindow(holder);
    ViewCompat.animate(holder.view).cancel();
}
        

onViewDetachedFromWindowメソッドはRecyclerView.Adapterによって作成されたビューがウィンドウから切り離されたときに呼び出されます。
アニメーションを削除しないとアプリがクラッシュする可能性があるので、忘れずに実装してください。

ビルド

上記のコードの動作を確認してみましょう。

fig1. Google Plus Animation

スクロールで列のviewがアニメーションしながら表示されるようになりました。

結論

RecyclerViewが導入されたことで、Androidでは色々なリスト表示のやり方ができるようになりました。Googleの純正アプリでは面白いリスト表示が多く実装されているので試してみてください。

関連記事

タグ検索で調べてみよう

Android7.1 UI