/* * Copyright (c) 2015 Zhang Hai <Dreaming.in.Code.ZH@Gmail.com> * All Rights Reserved. */ package me.zhanghai.android.douya.util; import android.annotation.TargetApi; import android.app.Activity; import android.os.Build; import android.os.Bundle; import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityOptionsCompat; import android.support.v4.app.Fragment; import android.support.v4.util.Pair; import android.transition.Explode; import android.transition.Transition; import android.view.View; import android.view.Window; import java.util.ArrayList; import me.zhanghai.android.douya.R; /** * Facts: * - System window must be shared elements if fullscreen, otherwise overlap occurs. * - Shared element transition overlaps old and new elements for a period of time. * - Translucent element overlap is unacceptable. */ public class TransitionUtils { private static final String TRANSITION_NAME_APPBAR = "appbar"; private TransitionUtils() {} public static boolean shouldEnableTransition() { // I hprof-ed the app, and found that bitmaps are kept by // ExitTransitionCoordinator.mSharedElementsBundle, which is in turn kept by // ResultReceiver$MyResultReceiver, which is kept by a FinalizerReference. // But this fix ( // https://android.googlesource.com/platform/frameworks/base/+/a0a0260e48e1ee4e9b5d98b49571e8d2a6fd6c3a // ) should have been incorporated into android-5.0.0_r1. So I really don't know the root // cause now. // New finding at 2015-12-06: OOM even happens on Android 5.1 (CM). // TODO: Allow disabling transition on some pre-marshmallow devices for OOM. return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static void setupTransitionBeforeDecorate(Activity activity) { if (!shouldEnableTransition()) { return; } Window window = activity.getWindow(); window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS); window.setSharedElementsUseOverlay(false); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static void postponeTransition(Activity activity) { if (!shouldEnableTransition()) { return; } ActivityCompat.postponeEnterTransition(activity); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static void setupTransitionOnActivityCreated(Fragment fragment) { if (!shouldEnableTransition()) { return; } setupTransitionForAppBar(fragment); ActivityCompat.startPostponedEnterTransition(fragment.getActivity()); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static void setupTransitionForAppBar(Fragment fragment) { if (!shouldEnableTransition()) { return; } View appbar = fragment.getView().findViewById(R.id.appBarWrapper); if (appbar != null) { appbar.setTransitionName(TRANSITION_NAME_APPBAR); } } // AppCompatDelegateImplV7.setContentView() removes all views under android.R.id.content, so we // have to do this after it. @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static void setupTransitionAfterSetContentView(Activity activity) { if (!shouldEnableTransition()) { return; } setupTransitionForAppBar(activity); postponeTransitionUntilDecorViewPreDraw(activity); } // FIXME: Duplicate code. @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static void setupTransitionForAppBar(Activity activity) { if (!shouldEnableTransition()) { return; } View appbar = activity.findViewById(R.id.appBarWrapper); if (appbar != null) { appbar.setTransitionName(TRANSITION_NAME_APPBAR); } } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private static void postponeTransitionUntilDecorViewPreDraw(final Activity activity) { if (!shouldEnableTransition()) { return; } activity.postponeEnterTransition(); ViewUtils.postOnPreDraw(activity.getWindow().getDecorView(), new Runnable() { @Override public void run() { activity.startPostponedEnterTransition(); } }); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static void setEnterReturnExplode(Fragment fragment) { if (!shouldEnableTransition()) { return; } Window window = fragment.getActivity().getWindow(); Transition explode = new Explode() .excludeTarget(android.R.id.statusBarBackground, true) .excludeTarget(android.R.id.navigationBarBackground, true); window.setEnterTransition(explode); window.setReturnTransition(explode); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static void postAfterTransition(Fragment fragment, Runnable runnable) { if (!shouldEnableTransition()) { runnable.run(); return; } // HACK: Horrible hack, because we have no good way of being notified at the end of // transition. Activity activity = fragment.getActivity(); activity.getWindow().getDecorView().postDelayed(runnable, ViewUtils.getMediumAnimTime(activity)); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static Bundle makeActivityOptionsBundle(Activity activity, View... sharedViews) { if (!shouldEnableTransition()) { return null; } ArrayList<Pair<View, String>> sharedElementList = new ArrayList<>(); for (View sharedView : sharedViews) { sharedElementList.add(Pair.create(sharedView, sharedView.getTransitionName())); } View appbar = activity.findViewById(R.id.appBarWrapper); if (appbar != null) { sharedElementList.add(Pair.create(appbar, appbar.getTransitionName())); } //noinspection unchecked Pair<View, String>[] sharedElements = sharedElementList.toArray(new Pair[sharedElementList.size()]); //noinspection unchecked return ActivityOptionsCompat.makeSceneTransitionAnimation(activity, sharedElements) .toBundle(); } }