blob: a1203b193e813d65fd9144f99fc23d53fd16808e [file] [log] [blame]
/*
* Copyright 2018 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 androidx.fragment.app;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.RestrictTo;
import androidx.core.view.ViewCompat;
import androidx.core.view.ViewGroupCompat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public abstract class FragmentTransitionImpl {
/**
* Returns {@code true} if this implementation can handle the specified {@link transition}.
*/
public abstract boolean canHandle(Object transition);
/**
* Returns a clone of a transition or null if it is null
*/
public abstract Object cloneTransition(Object transition);
/**
* Wraps a transition in a TransitionSet and returns the set. If transition is null, null is
* returned.
*/
public abstract Object wrapTransitionInSet(Object transition);
/**
* 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 abstract void setSharedElementTargets(Object transitionObj,
View nonExistentView, ArrayList<View> sharedViews);
/**
* Sets a transition epicenter to the rectangle of a given View.
*/
public abstract void setEpicenter(Object transitionObj, View view);
/**
* 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.
*/
protected 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 abstract void addTargets(Object transitionObj, ArrayList<View> views);
/**
* 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 abstract Object mergeTransitionsTogether(Object transition1, Object transition2,
Object transition3);
/**
* After the transition completes, the fragment's view is set to GONE and the exiting
* views are set to VISIBLE.
*/
public abstract void scheduleHideFragmentView(Object exitTransitionObj, View fragmentView,
ArrayList<View> exitingViews);
/**
* 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 abstract Object mergeTransitionsInSequence(Object exitTransitionObj,
Object enterTransitionObj, Object sharedElementTransitionObj);
/**
* Calls {@code TransitionManager#beginDelayedTransition(ViewGroup, Transition)}.
*/
public abstract void beginDelayedTransition(ViewGroup sceneRoot, Object 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)}.
*/
ArrayList<String> prepareSetNameOverridesReordered(ArrayList<View> sharedElementsIn) {
final ArrayList<String> names = new ArrayList<>();
final int numSharedElements = sharedElementsIn.size();
for (int i = 0; i < numSharedElements; i++) {
final View view = sharedElementsIn.get(i);
names.add(ViewCompat.getTransitionName(view));
ViewCompat.setTransitionName(view, null);
}
return names;
}
/**
* Changes the shared element names for the incoming shared elements 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)}.
*/
void setNameOverridesReordered(final View sceneRoot,
final ArrayList<View> sharedElementsOut, final ArrayList<View> sharedElementsIn,
final ArrayList<String> inNames, final Map<String, String> nameOverrides) {
final int numSharedElements = sharedElementsIn.size();
final ArrayList<String> outNames = new ArrayList<>();
for (int i = 0; i < numSharedElements; i++) {
final View view = sharedElementsOut.get(i);
final String name = ViewCompat.getTransitionName(view);
outNames.add(name);
if (name == null) {
continue;
}
ViewCompat.setTransitionName(view, null);
final String inName = nameOverrides.get(name);
for (int j = 0; j < numSharedElements; j++) {
if (inName.equals(inNames.get(j))) {
ViewCompat.setTransitionName(sharedElementsIn.get(j), name);
break;
}
}
}
OneShotPreDrawListener.add(sceneRoot, new Runnable() {
@Override
public void run() {
for (int i = 0; i < numSharedElements; i++) {
ViewCompat.setTransitionName(sharedElementsIn.get(i), inNames.get(i));
ViewCompat.setTransitionName(sharedElementsOut.get(i), 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.
*/
void captureTransitioningViews(ArrayList<View> transitioningViews, View view) {
if (view.getVisibility() == View.VISIBLE) {
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
if (ViewGroupCompat.isTransitionGroup(viewGroup)) {
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.
*/
void findNamedViews(Map<String, View> namedViews, View view) {
if (view.getVisibility() == View.VISIBLE) {
String transitionName = ViewCompat.getTransitionName(view);
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);
}
}
}
}
/**
*Applies the prepared {@code nameOverrides} to the view hierarchy.
*/
void setNameOverridesOrdered(final View sceneRoot,
final ArrayList<View> sharedElementsIn, final Map<String, String> 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 = ViewCompat.getTransitionName(view);
if (name != null) {
String inName = findKeyForValue(nameOverrides, name);
ViewCompat.setTransitionName(view, inName);
}
}
}
});
}
/**
* 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 abstract void scheduleRemoveTargets(Object overallTransitionObj,
Object enterTransition, ArrayList<View> enteringViews,
Object exitTransition, ArrayList<View> exitingViews,
Object sharedElementTransition, ArrayList<View> sharedElementsIn);
/**
* Swap the targets for the shared element transition from those Views in sharedElementsOut
* to those in sharedElementsIn
*/
public abstract void swapSharedElementTargets(Object sharedElementTransitionObj,
ArrayList<View> sharedElementsOut, ArrayList<View> 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 abstract void replaceTargets(Object transitionObj, ArrayList<View> oldTargets,
ArrayList<View> newTargets);
/**
* Adds a View target to a transition. If transitionObj is null, nothing is done.
*/
public abstract void addTarget(Object transitionObj, View view);
/**
* Remove a View target to a transition. If transitionObj is null, nothing is done.
*/
public abstract void removeTarget(Object transitionObj, View view);
/**
* Sets the epicenter of a transition to a rect object. The object can be modified until
* the transition runs.
*/
public abstract void setEpicenter(Object transitionObj, Rect epicenter);
void scheduleNameReset(final ViewGroup sceneRoot,
final ArrayList<View> sharedElementsIn, final Map<String, String> 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 = ViewCompat.getTransitionName(view);
final String inName = nameOverrides.get(name);
ViewCompat.setTransitionName(view, inName);
}
}
});
}
/**
* 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.
*/
protected static void bfsAddViewChildren(final List<View> 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<View> views, final View view,
final int maxIndex) {
for (int i = 0; i < maxIndex; i++) {
if (views.get(i) == view) {
return true;
}
}
return false;
}
/**
* Simple utility to detect if a list is null or has no elements.
*/
protected static boolean isNullOrEmpty(List list) {
return list == null || list.isEmpty();
}
/**
* Utility to find the String key in {@code map} that maps to {@code value}.
*/
@SuppressWarnings("WeakerAccess")
static String findKeyForValue(Map<String, String> map, String value) {
for (Map.Entry<String, String> entry : map.entrySet()) {
if (value.equals(entry.getValue())) {
return entry.getKey();
}
}
return null;
}
}