fresh digitable

めんどくさかったなってことを振り返ったり振り返らなかったりするための記録

RecyclerViewの中の要素をshared elementsにしてアニメーションする

今作っているツイッタークライアントは、タイムラインのユーザアイコンがタップされたらユーザ情報Activityに遷移するのだが、そのときにタップされたユーザアイコンが移動してユーザ情報Activityのユーザアイコンに重なるアニメーションを実装した。現時点ではAndroid 5.0 (API level 21)以上で有効。

参考:developer.android.com

まずは遷移元(タイムライン)と先(ユーザ情報)のActivityにWindow.requestFeature()する。

// setContentView()の前に行う
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
  getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
}

遷移元と先のビューにユニークなtransitionNameをつける。RecyclerViewの中の要素にtransitionNameをつけるときはアニメーションさせようとする直前にセットしておかないとユニークにならないようで、カスタムビューの中でinflateした後にセットするとうまくいかなかった(タイムライン→ユーザ情報はうまくいくが、そこからbackボタンで戻るときにアニメーションの起点が右下とか左下になったりした)。クリックされたビューがアニメーションするので、クリックリスナの中でやってしまっている。

userIcon.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View view) {
    ViewCompat.setTransitionName(view, "user_icon");
  }
});

ActivityOptionを渡してstartActivity()する。同様に上のコードのクリックリスナの中でやっている。

Intent intent = new Intent(context, UserInfoActivity.class);
ActivityCompat.startActivity(this, intent,
    ActivityOptionsCompat.makeSceneTransitionAnimation(this, userIcon, "user_icon").toBundle());

UserInfoActivity.start(Activity, View)みたいなユーティリティメソッドの中でやってしまうのもありかもしれない。

public static Intent createIntent(Context context) {
  return new Intent(context, UserInfoActivity.class);
}

public static void start(Activity activity, View userIcon) {
  final Intent intent = createIntent(activity.getApplicationContext());
  ViewCompat.setTransitionName(userIcon, "user_icon");
  ActivityCompat.startActivity(activity, intent,
      ActivityOptionsCompat.makeSceneTransitionAnimation(activity, userIcon, "user_icon").toBundle());
}

ViewPagerで表示するviewのclickがひろえないなと思ったら

Twitter公式クライアントの画像プレビューに限りなく近いものを作りたくて、ViewPagerにわたすFragmentの中でImageViewを作って、OnClickListenerをセットしてクリックイベントをとろうと思って次のように書いたのだが、だめだった。

@Override
public View onCreateView(LayoutInflater inflater,
                           @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
  super.onCreateView(inflater, container, savedInstanceState);
  return new ImageView(getContext());
}

@Override
public void onStart() {
  super.onStart();
  getView().setOnClickListener(new View.OnClickListener(View view) {
    // immersiveな状態を解除したりUIを表示したりする
  });
}

stackoverflowで解決策を探していると、PagerAdapter.instantiateItem()をオーバライドしてその中でクリックリスナやタッチリスナをセットしろというアドバイスがあったので、onCreateViewの中でImageViewsetOnClickListenerしてから返したりして難を逃れたりしていたが、次のようにも書けた。

private ImageView imageView;

@Override
public View onCreateView(LayoutInflater inflater,
                           @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
  super.onCreateView(inflater, container, savedInstanceState);
  this.imageView = new ImageView(getContext());
  return this.imageView;
}

@Override
public void onStart() {
  super.onStart();
  this.imageView.setOnClickListener(new View.OnClickListener(View view) {
    // immersiveな状態を解除したりUIを表示したりする
  });
}

onStartでセットしてonStopで解除すれば生存期間が若干短くなるのでこっちの方がいいかなと思いました(こなみ)。

ただ、これだとimmersive modeから復帰するための「システムUIを引っ張り出すスワイプ」でも、指を離したときにクリックイベントがなぜか発生してしまうので、独自実装したタッチリスナを同様に渡したほうがよい。

SYSTEM_UI_FLAG_IMMERSIVEとSYSTEM_UI_FLAG_IMMERSIVE_STICKYとの違い

API Level 19からは全画面表示をしたいときにどっちかのフラグをセット(View.setSystemUiVisibility())することで没入感をより高めることができる。

SYSTEM_UI_FLAG_HIDE_NAVIGATIONSYSTEM_UI_FLAG_FULLSCREENとを合わせてセットすることで、システムUIが画面外に押し出されている状態になる。外にあるUIを引っ張り出すように(上か下エッジからスワイプ)すると表示される。二つのフラグの違いは次の通り。

  • SYSTEM_UI_FLAG_IMMERSIVE: システムUIが引っ張り出されたら、システムUIが消えた状態に戻らない
  • SYSTEM_UI_FLAG_IMMERSIVE_STICKY: システムUIが引っ張り出されたあと、しばらくするとまたシステムUIが引っ込む

IMMERSIVEの方を使い、View#setOnSystemUiVisibilityChangeListener()にリスナをセットすることでシステムUIの見え方が変わったイベントを受け取れる。このリスナの中で独自のUIを表示させるようにして、時間とか特定の操作で再びimmersiveな状態に遷移させれば良い。

システムUIが無いレイアウトに設定するためのフラグも一緒にセットしてやればカクついたりしない。詳しくは公式リファレンスを。

developer.android.com