やりたいことは次の動画を見てもらうのが早いです。
言葉にすると、
1. Activity1 から Activity2 に遷移するときに、
2. ある View を sharedElement としてアニメーション(移動)させたい
3. Activity2 では sharedElement が移動している間に表示している内容を変更したい
Activity1 の方のレイアウトは ImageView 一つだけで、これが sharedElement の対象。
- <?xml version="1.0" encoding="utf-8"?>
- <FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <ImageView
- android:id="@+id/image"
- android:layout_width="128dp"
- android:layout_height="128dp"
- android:layout_gravity="bottom"
- android:src="@drawable/sample_image1"
- android:transitionName="image"
- tools:ignore="ContentDescription"/>
-
- </FrameLayout>
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/image"
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_gravity="bottom"
android:src="@drawable/sample_image1"
android:transitionName="image"
tools:ignore="ContentDescription"/>
</FrameLayout>
Activity2 の方は ImageView が2つ重なっていて、一つ目の ImageView には Activity1 と同じ画像、二つ目の ImageView には Transition 後に表示したい画像がセットされている。二つ目の ImageView は非表示(INVISIBLE)。二つの ImageView の container である FrameLayout が sharedElement の対象。
- <!--xml version="1.0" encoding="utf-8"?-->
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
-
- <net.yanzm.sample.SquareFrameLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:transitionName="image">
-
- <ImageView
- android:id="@+id/image"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:src="@drawable/sample_image1"
- tools:ignore="ContentDescription"/>
-
- <ImageView
- android:id="@+id/image2"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:src="@drawable/sample_image2"
- android:visibility="invisible"
- tools:ignore="ContentDescription"/>
-
- </net.yanzm.sample.SquareFrameLayout>
-
- </LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<net.yanzm.sample.SquareFrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:transitionName="image">
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/sample_image1"
tools:ignore="ContentDescription"/>
<ImageView
android:id="@+id/image2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/sample_image2"
android:visibility="invisible"
tools:ignore="ContentDescription"/>
</net.yanzm.sample.SquareFrameLayout>
</LinearLayout>
Activity1側で ActivityOptionsCompat.makeSceneTransitionAnimation() を使って Activity2 を呼び出すと、画面遷移時に SharedElement が移動します。
当たり前ですがこの段階では Activity2 側の二つ目の ImageView は出てきません(INVISIBLEなので)。
- public class TransitionActivity extends AppCompatActivity {
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_transition);
-
- findViewById(R.id.image).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- move(view);
- }
- });
- }
-
- private void move(View view) {
- Intent intent = new Intent(this, TransitionActivity2.class);
- final Bundle options = ActivityOptionsCompat
- .makeSceneTransitionAnimation(this, view, "image")
- .toBundle();
- startActivity(intent, options);
- }
- }
public class TransitionActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transition);
findViewById(R.id.image).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
move(view);
}
});
}
private void move(View view) {
Intent intent = new Intent(this, TransitionActivity2.class);
final Bundle options = ActivityOptionsCompat
.makeSceneTransitionAnimation(this, view, "image")
.toBundle();
startActivity(intent, options);
}
}
そこで、独自の SharedElement 用 TransitionSet を作ります。
デフォルトはプラットフォームの @transition/move が指定されており、中身は次のようになっています。
- <transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
- <changeBounds/>
- <changeTransform/>
- <changeClipBounds/>
- <changeImageTransform/>
- </transitionSet>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<changeBounds/>
<changeTransform/>
<changeClipBounds/>
<changeImageTransform/>
</transitionSet>
そこで、以下のようなクラスを用意しました。
- public class CustomTransitionSet extends TransitionSet {
-
- public CustomTransitionSet() {
- addTransition(new ChangeBounds());
- addTransition(new ChangeTransform());
- addTransition(new ChangeClipBounds());
- addTransition(new ChangeImageTransform());
- addTransition(new CustomTransition().addTarget(R.id.image2));
- }
- }
public class CustomTransitionSet extends TransitionSet {
public CustomTransitionSet() {
addTransition(new ChangeBounds());
addTransition(new ChangeTransform());
addTransition(new ChangeClipBounds());
addTransition(new ChangeImageTransform());
addTransition(new CustomTransition().addTarget(R.id.image2));
}
}
デフォルトの設定 + CustomTransition を追加しています。 CustomTransition は Activity2 の二つ目の ImageView だけを対象にしたいので、addTarget で対象を絞っています。
この CustomTransitionSet を Activity2 で SharedElement 用の Transition としてセットします。
- public class TransitionActivity2 extends AppCompatActivity {
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- getWindow().setSharedElementEnterTransition(new CustomTransitionSet());
-
- setContentView(R.layout.activity_transition2);
- }
- }
public class TransitionActivity2 extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setSharedElementEnterTransition(new CustomTransitionSet());
setContentView(R.layout.activity_transition2);
}
}
CustomTransition では Transition 開始時の view の Visibility を持っておいて、それが VISIBLE 以外だったらフェードアウト、VISIBLE だったらフェードインのアニメーションをするようにしました。
- public class CustomTransition extends Transition {
-
-
- private static final String PROP_NAME_VISIBILITY = "net.yanzm.sample:CustomTransition:visibility";
-
- @Override
- public void captureStartValues(TransitionValues transitionValues) {
-
- final View view = transitionValues.view;
- transitionValues.values.put(PROP_NAME_VISIBILITY, view.getVisibility());
- }
-
- @Override
- public void captureEndValues(TransitionValues transitionValues) {
-
- }
-
- @Override
- public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
- if (startValues == null || startValues.view == null) {
- return null;
- }
-
- final View view = startValues.view;
- final int visibility = (int) startValues.values.get(PROP_NAME_VISIBILITY);
- final boolean isEnter = visibility != View.VISIBLE;
-
- view.setVisibility(View.VISIBLE);
- view.setAlpha(isEnter ? 0f : 1f);
-
- final ObjectAnimator anim = ObjectAnimator.ofFloat(view, "alpha", isEnter ? 1f : 0f);
- anim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- view.setAlpha(1f);
- view.setVisibility(isEnter ? View.VISIBLE : View.INVISIBLE);
- super.onAnimationEnd(animation);
- }
- });
- return anim;
- }
- }
public class CustomTransition extends Transition {
// TransitionValues に追加するときのキーは パッケージ名:クラス名:プロパティ名
private static final String PROP_NAME_VISIBILITY = "net.yanzm.sample:CustomTransition:visibility";
@Override
public void captureStartValues(TransitionValues transitionValues) {
// visibility の値を持っておく
final View view = transitionValues.view;
transitionValues.values.put(PROP_NAME_VISIBILITY, view.getVisibility());
}
@Override
public void captureEndValues(TransitionValues transitionValues) {
// end の値は使わないので何もしない
}
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
if (startValues == null || startValues.view == null) {
return null;
}
final View view = startValues.view;
final int visibility = (int) startValues.values.get(PROP_NAME_VISIBILITY);
final boolean isEnter = visibility != View.VISIBLE;
view.setVisibility(View.VISIBLE);
view.setAlpha(isEnter ? 0f : 1f);
final ObjectAnimator anim = ObjectAnimator.ofFloat(view, "alpha", isEnter ? 1f : 0f);
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setAlpha(1f);
view.setVisibility(isEnter ? View.VISIBLE : View.INVISIBLE);
super.onAnimationEnd(animation);
}
});
return anim;
}
}
これで一番上に載せた動画のような動作になりました!