/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.v4.app; import android.graphics.Rect; import android.support.annotation.RequiresApi; import android.transition.Transition; import android.transition.TransitionManager; import android.transition.TransitionSet; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; import java.util.List; import java.util.Map; @RequiresApi(21) class FragmentTransitionCompat21 { /** * Returns a clone of a transition or null if it is null */ public static Object cloneTransition(Object transition) { Transition copy = null; if (transition != null) { copy = ((Transition) transition).clone(); } return copy; } /** * Wraps a transition in a TransitionSet and returns the set. If transition is null, null is * returned. */ public static Object wrapTransitionInSet(Object transition) { if (transition == null) { return null; } TransitionSet transitionSet = new TransitionSet(); transitionSet.addTransition((Transition) transition); return transitionSet; } /** * Finds all children of the shared elements and sets the wrapping TransitionSet * targets to point to those. It also limits transitions that have no targets to the * specific shared elements. This allows developers to target child views of the * shared elements specifically, but this doesn't happen by default. */ public static void setSharedElementTargets(Object transitionObj, View nonExistentView, ArrayList sharedViews) { TransitionSet transition = (TransitionSet) transitionObj; final List views = transition.getTargets(); views.clear(); final int count = sharedViews.size(); for (int i = 0; i < count; i++) { final View view = sharedViews.get(i); bfsAddViewChildren(views, view); } views.add(nonExistentView); sharedViews.add(nonExistentView); addTargets(transition, sharedViews); } /** * Uses a breadth-first scheme to add startView and all of its children to views. * It won't add a child if it is already in views. */ private static void bfsAddViewChildren(final List views, final View startView) { final int startIndex = views.size(); if (containedBeforeIndex(views, startView, startIndex)) { return; // This child is already in the list, so all its children are also. } views.add(startView); for (int index = startIndex; index < views.size(); index++) { final View view = views.get(index); if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; final int childCount = viewGroup.getChildCount(); for (int childIndex = 0; childIndex < childCount; childIndex++) { final View child = viewGroup.getChildAt(childIndex); if (!containedBeforeIndex(views, child, startIndex)) { views.add(child); } } } } } /** * Does a linear search through views for view, limited to maxIndex. */ private static boolean containedBeforeIndex(final List views, final View view, final int maxIndex) { for (int i = 0; i < maxIndex; i++) { if (views.get(i) == view) { return true; } } return false; } /** * Sets a transition epicenter to the rectangle of a given View. */ public static void setEpicenter(Object transitionObj, View view) { if (view != null) { Transition transition = (Transition) transitionObj; final Rect epicenter = new Rect(); getBoundsOnScreen(view, epicenter); transition.setEpicenterCallback(new Transition.EpicenterCallback() { @Override public Rect onGetEpicenter(Transition transition) { return epicenter; } }); } } /** * Replacement for view.getBoundsOnScreen because that is not public. This returns a rect * containing the bounds relative to the screen that the view is in. */ public static void getBoundsOnScreen(View view, Rect epicenter) { int[] loc = new int[2]; view.getLocationOnScreen(loc); epicenter.set(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight()); } /** * This method adds views as targets to the transition, but only if the transition * doesn't already have a target. It is best for views to contain one View object * that does not exist in the view hierarchy (state.nonExistentView) so that * when they are removed later, a list match will suffice to remove the targets. * Otherwise, if you happened to have targeted the exact views for the transition, * the replaceTargets call will remove them unexpectedly. */ public static void addTargets(Object transitionObj, ArrayList views) { Transition transition = (Transition) transitionObj; if (transition == null) { return; } if (transition instanceof TransitionSet) { TransitionSet set = (TransitionSet) transition; int numTransitions = set.getTransitionCount(); for (int i = 0; i < numTransitions; i++) { Transition child = set.getTransitionAt(i); addTargets(child, views); } } else if (!hasSimpleTarget(transition)) { List targets = transition.getTargets(); if (isNullOrEmpty(targets)) { // We can just add the target views int numViews = views.size(); for (int i = 0; i < numViews; i++) { transition.addTarget(views.get(i)); } } } } /** * Returns true if there are any targets based on ID, transition or type. */ private static boolean hasSimpleTarget(Transition transition) { return !isNullOrEmpty(transition.getTargetIds()) || !isNullOrEmpty(transition.getTargetNames()) || !isNullOrEmpty(transition.getTargetTypes()); } /** * Simple utility to detect if a list is null or has no elements. */ private static boolean isNullOrEmpty(List list) { return list == null || list.isEmpty(); } /** * Creates a TransitionSet that plays all passed transitions together. Any null * transitions passed will not be added to the set. If all are null, then an empty * TransitionSet will be returned. */ public static Object mergeTransitionsTogether(Object transition1, Object transition2, Object transition3) { TransitionSet transitionSet = new TransitionSet(); if (transition1 != null) { transitionSet.addTransition((Transition) transition1); } if (transition2 != null) { transitionSet.addTransition((Transition) transition2); } if (transition3 != null) { transitionSet.addTransition((Transition) transition3); } return transitionSet; } /** * After the transition completes, the fragment's view is set to GONE and the exiting * views are set to VISIBLE. */ public static void scheduleHideFragmentView(Object exitTransitionObj, final View fragmentView, final ArrayList exitingViews) { Transition exitTransition = (Transition) exitTransitionObj; exitTransition.addListener(new Transition.TransitionListener() { @Override public void onTransitionStart(Transition transition) { } @Override public void onTransitionEnd(Transition transition) { transition.removeListener(this); fragmentView.setVisibility(View.GONE); final int numViews = exitingViews.size(); for (int i = 0; i < numViews; i++) { exitingViews.get(i).setVisibility(View.VISIBLE); } } @Override public void onTransitionCancel(Transition transition) { } @Override public void onTransitionPause(Transition transition) { } @Override public void onTransitionResume(Transition transition) { } }); } /** * Combines enter, exit, and shared element transition so that they play in the proper * sequence. First the exit transition plays along with the shared element transition. * When the exit transition completes, the enter transition starts. The shared element * transition can continue running while the enter transition plays. * * @return A TransitionSet with all of enter, exit, and shared element transitions in * it (modulo null values), ordered such that they play in the proper sequence. */ public static Object mergeTransitionsInSequence(Object exitTransitionObj, Object enterTransitionObj, Object sharedElementTransitionObj) { // First do exit, then enter, but allow shared element transition to happen // during both. Transition staggered = null; final Transition exitTransition = (Transition) exitTransitionObj; final Transition enterTransition = (Transition) enterTransitionObj; final Transition sharedElementTransition = (Transition) sharedElementTransitionObj; if (exitTransition != null && enterTransition != null) { staggered = new TransitionSet() .addTransition(exitTransition) .addTransition(enterTransition) .setOrdering(TransitionSet.ORDERING_SEQUENTIAL); } else if (exitTransition != null) { staggered = exitTransition; } else if (enterTransition != null) { staggered = enterTransition; } if (sharedElementTransition != null) { TransitionSet together = new TransitionSet(); if (staggered != null) { together.addTransition(staggered); } together.addTransition(sharedElementTransition); return together; } else { return staggered; } } /** * Calls {@link TransitionManager#beginDelayedTransition(ViewGroup, Transition)}. */ public static void beginDelayedTransition(ViewGroup sceneRoot, Object transition) { TransitionManager.beginDelayedTransition(sceneRoot, (Transition) transition); } /** * Prepares for setting the shared element names by gathering the names of the incoming * shared elements and clearing them. {@link #setNameOverridesReordered(View, ArrayList, * ArrayList, ArrayList, Map)} must be called after this to complete setting the shared element * name overrides. This must be called before * {@link #beginDelayedTransition(ViewGroup, Object)}. */ public static ArrayList prepareSetNameOverridesReordered( final ArrayList sharedElementsIn) { final ArrayList names = new ArrayList<>(); final int numSharedElements = sharedElementsIn.size(); for (int i = 0; i < numSharedElements; i++) { final View view = sharedElementsIn.get(i); names.add(view.getTransitionName()); view.setTransitionName(null); } return names; } /** * Changes the shared element names for the incoming shared eleemnts to match those of the * outgoing shared elements. This also temporarily clears the shared element names of the * outgoing shared elements. Must be called after * {@link #beginDelayedTransition(ViewGroup, Object)}. */ public static void setNameOverridesReordered(final View sceneRoot, final ArrayList sharedElementsOut, final ArrayList sharedElementsIn, final ArrayList inNames, final Map nameOverrides) { final int numSharedElements = sharedElementsIn.size(); final ArrayList outNames = new ArrayList<>(); for (int i = 0; i < numSharedElements; i++) { final View view = sharedElementsOut.get(i); final String name = view.getTransitionName(); outNames.add(name); if (name == null) { continue; } view.setTransitionName(null); final String inName = nameOverrides.get(name); for (int j = 0; j < numSharedElements; j++) { if (inName.equals(inNames.get(j))) { sharedElementsIn.get(j).setTransitionName(name); break; } } } OneShotPreDrawListener.add(sceneRoot, new Runnable() { @Override public void run() { for (int i = 0; i < numSharedElements; i++) { sharedElementsIn.get(i).setTransitionName(inNames.get(i)); sharedElementsOut.get(i).setTransitionName(outNames.get(i)); } } }); } /** * Gets the Views in the hierarchy affected by entering and exiting Activity Scene transitions. * @param transitioningViews This View will be added to transitioningViews if it is VISIBLE and * a normal View or a ViewGroup with * {@link android.view.ViewGroup#isTransitionGroup()} true. * @param view The base of the view hierarchy to look in. */ public static void captureTransitioningViews(ArrayList transitioningViews, View view) { if (view.getVisibility() == View.VISIBLE) { if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; if (viewGroup.isTransitionGroup()) { transitioningViews.add(viewGroup); } else { int count = viewGroup.getChildCount(); for (int i = 0; i < count; i++) { View child = viewGroup.getChildAt(i); captureTransitioningViews(transitioningViews, child); } } } else { transitioningViews.add(view); } } } /** * Finds all views that have transition names in the hierarchy under the given view and * stores them in {@code namedViews} map with the name as the key. */ public static void findNamedViews(Map namedViews, View view) { if (view.getVisibility() == View.VISIBLE) { String transitionName = view.getTransitionName(); if (transitionName != null) { namedViews.put(transitionName, view); } if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; int count = viewGroup.getChildCount(); for (int i = 0; i < count; i++) { View child = viewGroup.getChildAt(i); findNamedViews(namedViews, child); } } } } public static void setNameOverridesOrdered(final View sceneRoot, final ArrayList sharedElementsIn, final Map nameOverrides) { OneShotPreDrawListener.add(sceneRoot, new Runnable() { @Override public void run() { final int numSharedElements = sharedElementsIn.size(); for (int i = 0; i < numSharedElements; i++) { View view = sharedElementsIn.get(i); String name = view.getTransitionName(); if (name != null) { String inName = findKeyForValue(nameOverrides, name); view.setTransitionName(inName); } } } }); } /** * Utility to find the String key in {@code map} that maps to {@code value}. */ private static String findKeyForValue(Map map, String value) { for (Map.Entry entry : map.entrySet()) { if (value.equals(entry.getValue())) { return entry.getKey(); } } return null; } /** * After the transition has started, remove all targets that we added to the transitions * so that the transitions are left in a clean state. */ public static void scheduleRemoveTargets(final Object overallTransitionObj, final Object enterTransition, final ArrayList enteringViews, final Object exitTransition, final ArrayList exitingViews, final Object sharedElementTransition, final ArrayList sharedElementsIn) { final Transition overallTransition = (Transition) overallTransitionObj; overallTransition.addListener(new Transition.TransitionListener() { @Override public void onTransitionStart(Transition transition) { if (enterTransition != null) { replaceTargets(enterTransition, enteringViews, null); } if (exitTransition != null) { replaceTargets(exitTransition, exitingViews, null); } if (sharedElementTransition != null) { replaceTargets(sharedElementTransition, sharedElementsIn, null); } } @Override public void onTransitionEnd(Transition transition) { } @Override public void onTransitionCancel(Transition transition) { } @Override public void onTransitionPause(Transition transition) { } @Override public void onTransitionResume(Transition transition) { } }); } /** * Swap the targets for the shared element transition from those Views in sharedElementsOut * to those in sharedElementsIn */ public static void swapSharedElementTargets(Object sharedElementTransitionObj, ArrayList sharedElementsOut, ArrayList sharedElementsIn) { TransitionSet sharedElementTransition = (TransitionSet) sharedElementTransitionObj; if (sharedElementTransition != null) { sharedElementTransition.getTargets().clear(); sharedElementTransition.getTargets().addAll(sharedElementsIn); replaceTargets(sharedElementTransition, sharedElementsOut, sharedElementsIn); } } /** * This method removes the views from transitions that target ONLY those views and * replaces them with the new targets list. * The views list should match those added in addTargets and should contain * one view that is not in the view hierarchy (state.nonExistentView). */ public static void replaceTargets(Object transitionObj, ArrayList oldTargets, ArrayList newTargets) { Transition transition = (Transition) transitionObj; if (transition instanceof TransitionSet) { TransitionSet set = (TransitionSet) transition; int numTransitions = set.getTransitionCount(); for (int i = 0; i < numTransitions; i++) { Transition child = set.getTransitionAt(i); replaceTargets(child, oldTargets, newTargets); } } else if (!hasSimpleTarget(transition)) { List targets = transition.getTargets(); if (targets != null && targets.size() == oldTargets.size() && targets.containsAll(oldTargets)) { // We have an exact match. We must have added these earlier in addTargets final int targetCount = newTargets == null ? 0 : newTargets.size(); for (int i = 0; i < targetCount; i++) { transition.addTarget(newTargets.get(i)); } for (int i = oldTargets.size() - 1; i >= 0; i--) { transition.removeTarget(oldTargets.get(i)); } } } } /** * Adds a View target to a transition. If transitionObj is null, nothing is done. */ public static void addTarget(Object transitionObj, View view) { if (transitionObj != null) { Transition transition = (Transition) transitionObj; transition.addTarget(view); } } /** * Remove a View target to a transition. If transitionObj is null, nothing is done. */ public static void removeTarget(Object transitionObj, View view) { if (transitionObj != null) { Transition transition = (Transition) transitionObj; transition.removeTarget(view); } } /** * Sets the epicenter of a transition to a rect object. The object can be modified until * the transition runs. */ public static void setEpicenter(Object transitionObj, final Rect epicenter) { if (transitionObj != null) { Transition transition = (Transition) transitionObj; transition.setEpicenterCallback(new Transition.EpicenterCallback() { @Override public Rect onGetEpicenter(Transition transition) { if (epicenter == null || epicenter.isEmpty()) { return null; } return epicenter; } }); } } public static void scheduleNameReset(final ViewGroup sceneRoot, final ArrayList sharedElementsIn, final Map nameOverrides) { OneShotPreDrawListener.add(sceneRoot, new Runnable() { @Override public void run() { final int numSharedElements = sharedElementsIn.size(); for (int i = 0; i < numSharedElements; i++) { final View view = sharedElementsIn.get(i); final String name = view.getTransitionName(); final String inName = nameOverrides.get(name); view.setTransitionName(inName); } } }); } }