1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.fragment.app; 18 19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 20 21 import android.graphics.Rect; 22 import android.view.View; 23 import android.view.ViewGroup; 24 25 import androidx.annotation.RestrictTo; 26 import androidx.core.view.ViewCompat; 27 import androidx.core.view.ViewGroupCompat; 28 29 import java.util.ArrayList; 30 import java.util.List; 31 import java.util.Map; 32 33 34 /** 35 * @hide 36 */ 37 @RestrictTo(LIBRARY_GROUP) 38 public abstract class FragmentTransitionImpl { 39 40 /** 41 * Returns {@code true} if this implementation can handle the specified {@link transition}. 42 */ 43 public abstract boolean canHandle(Object transition); 44 45 /** 46 * Returns a clone of a transition or null if it is null 47 */ 48 public abstract Object cloneTransition(Object transition); 49 50 /** 51 * Wraps a transition in a TransitionSet and returns the set. If transition is null, null is 52 * returned. 53 */ 54 public abstract Object wrapTransitionInSet(Object transition); 55 56 /** 57 * Finds all children of the shared elements and sets the wrapping TransitionSet 58 * targets to point to those. It also limits transitions that have no targets to the 59 * specific shared elements. This allows developers to target child views of the 60 * shared elements specifically, but this doesn't happen by default. 61 */ 62 public abstract void setSharedElementTargets(Object transitionObj, 63 View nonExistentView, ArrayList<View> sharedViews); 64 65 /** 66 * Sets a transition epicenter to the rectangle of a given View. 67 */ 68 public abstract void setEpicenter(Object transitionObj, View view); 69 70 /** 71 * Replacement for view.getBoundsOnScreen because that is not public. This returns a rect 72 * containing the bounds relative to the screen that the view is in. 73 */ 74 protected void getBoundsOnScreen(View view, Rect epicenter) { 75 int[] loc = new int[2]; 76 view.getLocationOnScreen(loc); 77 epicenter.set(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight()); 78 } 79 80 /** 81 * This method adds views as targets to the transition, but only if the transition 82 * doesn't already have a target. It is best for views to contain one View object 83 * that does not exist in the view hierarchy (state.nonExistentView) so that 84 * when they are removed later, a list match will suffice to remove the targets. 85 * Otherwise, if you happened to have targeted the exact views for the transition, 86 * the replaceTargets call will remove them unexpectedly. 87 */ 88 public abstract void addTargets(Object transitionObj, ArrayList<View> views); 89 90 /** 91 * Creates a TransitionSet that plays all passed transitions together. Any null 92 * transitions passed will not be added to the set. If all are null, then an empty 93 * TransitionSet will be returned. 94 */ 95 public abstract Object mergeTransitionsTogether(Object transition1, Object transition2, 96 Object transition3); 97 98 /** 99 * After the transition completes, the fragment's view is set to GONE and the exiting 100 * views are set to VISIBLE. 101 */ 102 public abstract void scheduleHideFragmentView(Object exitTransitionObj, View fragmentView, 103 ArrayList<View> exitingViews); 104 105 /** 106 * Combines enter, exit, and shared element transition so that they play in the proper 107 * sequence. First the exit transition plays along with the shared element transition. 108 * When the exit transition completes, the enter transition starts. The shared element 109 * transition can continue running while the enter transition plays. 110 * 111 * @return A TransitionSet with all of enter, exit, and shared element transitions in 112 * it (modulo null values), ordered such that they play in the proper sequence. 113 */ 114 public abstract Object mergeTransitionsInSequence(Object exitTransitionObj, 115 Object enterTransitionObj, Object sharedElementTransitionObj); 116 117 /** 118 * Calls {@code TransitionManager#beginDelayedTransition(ViewGroup, Transition)}. 119 */ 120 public abstract void beginDelayedTransition(ViewGroup sceneRoot, Object transition); 121 122 /** 123 * Prepares for setting the shared element names by gathering the names of the incoming 124 * shared elements and clearing them. {@link #setNameOverridesReordered(View, ArrayList, 125 * ArrayList, ArrayList, Map)} must be called after this to complete setting the shared element 126 * name overrides. This must be called before 127 * {@link #beginDelayedTransition(ViewGroup, Object)}. 128 */ 129 ArrayList<String> prepareSetNameOverridesReordered(ArrayList<View> sharedElementsIn) { 130 final ArrayList<String> names = new ArrayList<>(); 131 final int numSharedElements = sharedElementsIn.size(); 132 for (int i = 0; i < numSharedElements; i++) { 133 final View view = sharedElementsIn.get(i); 134 names.add(ViewCompat.getTransitionName(view)); 135 ViewCompat.setTransitionName(view, null); 136 } 137 return names; 138 } 139 140 /** 141 * Changes the shared element names for the incoming shared elements to match those of the 142 * outgoing shared elements. This also temporarily clears the shared element names of the 143 * outgoing shared elements. Must be called after 144 * {@link #beginDelayedTransition(ViewGroup, Object)}. 145 */ 146 void setNameOverridesReordered(final View sceneRoot, 147 final ArrayList<View> sharedElementsOut, final ArrayList<View> sharedElementsIn, 148 final ArrayList<String> inNames, final Map<String, String> nameOverrides) { 149 final int numSharedElements = sharedElementsIn.size(); 150 final ArrayList<String> outNames = new ArrayList<>(); 151 152 for (int i = 0; i < numSharedElements; i++) { 153 final View view = sharedElementsOut.get(i); 154 final String name = ViewCompat.getTransitionName(view); 155 outNames.add(name); 156 if (name == null) { 157 continue; 158 } 159 ViewCompat.setTransitionName(view, null); 160 final String inName = nameOverrides.get(name); 161 for (int j = 0; j < numSharedElements; j++) { 162 if (inName.equals(inNames.get(j))) { 163 ViewCompat.setTransitionName(sharedElementsIn.get(j), name); 164 break; 165 } 166 } 167 } 168 169 OneShotPreDrawListener.add(sceneRoot, new Runnable() { 170 @Override 171 public void run() { 172 for (int i = 0; i < numSharedElements; i++) { 173 ViewCompat.setTransitionName(sharedElementsIn.get(i), inNames.get(i)); 174 ViewCompat.setTransitionName(sharedElementsOut.get(i), outNames.get(i)); 175 } 176 } 177 }); 178 } 179 180 /** 181 * Gets the Views in the hierarchy affected by entering and exiting Activity Scene transitions. 182 * 183 * @param transitioningViews This View will be added to transitioningViews if it is VISIBLE and 184 * a normal View or a ViewGroup with 185 * {@link android.view.ViewGroup#isTransitionGroup()} true. 186 * @param view The base of the view hierarchy to look in. 187 */ 188 void captureTransitioningViews(ArrayList<View> transitioningViews, View view) { 189 if (view.getVisibility() == View.VISIBLE) { 190 if (view instanceof ViewGroup) { 191 ViewGroup viewGroup = (ViewGroup) view; 192 if (ViewGroupCompat.isTransitionGroup(viewGroup)) { 193 transitioningViews.add(viewGroup); 194 } else { 195 int count = viewGroup.getChildCount(); 196 for (int i = 0; i < count; i++) { 197 View child = viewGroup.getChildAt(i); 198 captureTransitioningViews(transitioningViews, child); 199 } 200 } 201 } else { 202 transitioningViews.add(view); 203 } 204 } 205 } 206 207 /** 208 * Finds all views that have transition names in the hierarchy under the given view and 209 * stores them in {@code namedViews} map with the name as the key. 210 */ 211 void findNamedViews(Map<String, View> namedViews, View view) { 212 if (view.getVisibility() == View.VISIBLE) { 213 String transitionName = ViewCompat.getTransitionName(view); 214 if (transitionName != null) { 215 namedViews.put(transitionName, view); 216 } 217 if (view instanceof ViewGroup) { 218 ViewGroup viewGroup = (ViewGroup) view; 219 int count = viewGroup.getChildCount(); 220 for (int i = 0; i < count; i++) { 221 View child = viewGroup.getChildAt(i); 222 findNamedViews(namedViews, child); 223 } 224 } 225 } 226 } 227 228 /** 229 *Applies the prepared {@code nameOverrides} to the view hierarchy. 230 */ 231 void setNameOverridesOrdered(final View sceneRoot, 232 final ArrayList<View> sharedElementsIn, final Map<String, String> nameOverrides) { 233 OneShotPreDrawListener.add(sceneRoot, new Runnable() { 234 @Override 235 public void run() { 236 final int numSharedElements = sharedElementsIn.size(); 237 for (int i = 0; i < numSharedElements; i++) { 238 View view = sharedElementsIn.get(i); 239 String name = ViewCompat.getTransitionName(view); 240 if (name != null) { 241 String inName = findKeyForValue(nameOverrides, name); 242 ViewCompat.setTransitionName(view, inName); 243 } 244 } 245 } 246 }); 247 } 248 249 /** 250 * After the transition has started, remove all targets that we added to the transitions 251 * so that the transitions are left in a clean state. 252 */ 253 public abstract void scheduleRemoveTargets(Object overallTransitionObj, 254 Object enterTransition, ArrayList<View> enteringViews, 255 Object exitTransition, ArrayList<View> exitingViews, 256 Object sharedElementTransition, ArrayList<View> sharedElementsIn); 257 258 /** 259 * Swap the targets for the shared element transition from those Views in sharedElementsOut 260 * to those in sharedElementsIn 261 */ 262 public abstract void swapSharedElementTargets(Object sharedElementTransitionObj, 263 ArrayList<View> sharedElementsOut, ArrayList<View> sharedElementsIn); 264 265 /** 266 * This method removes the views from transitions that target ONLY those views and 267 * replaces them with the new targets list. 268 * The views list should match those added in addTargets and should contain 269 * one view that is not in the view hierarchy (state.nonExistentView). 270 */ 271 public abstract void replaceTargets(Object transitionObj, ArrayList<View> oldTargets, 272 ArrayList<View> newTargets); 273 274 /** 275 * Adds a View target to a transition. If transitionObj is null, nothing is done. 276 */ 277 public abstract void addTarget(Object transitionObj, View view); 278 279 /** 280 * Remove a View target to a transition. If transitionObj is null, nothing is done. 281 */ 282 public abstract void removeTarget(Object transitionObj, View view); 283 284 /** 285 * Sets the epicenter of a transition to a rect object. The object can be modified until 286 * the transition runs. 287 */ 288 public abstract void setEpicenter(Object transitionObj, Rect epicenter); 289 290 void scheduleNameReset(final ViewGroup sceneRoot, 291 final ArrayList<View> sharedElementsIn, final Map<String, String> nameOverrides) { 292 OneShotPreDrawListener.add(sceneRoot, new Runnable() { 293 @Override 294 public void run() { 295 final int numSharedElements = sharedElementsIn.size(); 296 for (int i = 0; i < numSharedElements; i++) { 297 final View view = sharedElementsIn.get(i); 298 final String name = ViewCompat.getTransitionName(view); 299 final String inName = nameOverrides.get(name); 300 ViewCompat.setTransitionName(view, inName); 301 } 302 } 303 }); 304 } 305 306 /** 307 * Uses a breadth-first scheme to add startView and all of its children to views. 308 * It won't add a child if it is already in views. 309 */ 310 protected static void bfsAddViewChildren(final List<View> views, final View startView) { 311 final int startIndex = views.size(); 312 if (containedBeforeIndex(views, startView, startIndex)) { 313 return; // This child is already in the list, so all its children are also. 314 } 315 views.add(startView); 316 for (int index = startIndex; index < views.size(); index++) { 317 final View view = views.get(index); 318 if (view instanceof ViewGroup) { 319 ViewGroup viewGroup = (ViewGroup) view; 320 final int childCount = viewGroup.getChildCount(); 321 for (int childIndex = 0; childIndex < childCount; childIndex++) { 322 final View child = viewGroup.getChildAt(childIndex); 323 if (!containedBeforeIndex(views, child, startIndex)) { 324 views.add(child); 325 } 326 } 327 } 328 } 329 } 330 331 /** 332 * Does a linear search through views for view, limited to maxIndex. 333 */ 334 private static boolean containedBeforeIndex(final List<View> views, final View view, 335 final int maxIndex) { 336 for (int i = 0; i < maxIndex; i++) { 337 if (views.get(i) == view) { 338 return true; 339 } 340 } 341 return false; 342 } 343 344 /** 345 * Simple utility to detect if a list is null or has no elements. 346 */ 347 protected static boolean isNullOrEmpty(List list) { 348 return list == null || list.isEmpty(); 349 } 350 351 /** 352 * Utility to find the String key in {@code map} that maps to {@code value}. 353 */ 354 @SuppressWarnings("WeakerAccess") 355 static String findKeyForValue(Map<String, String> map, String value) { 356 for (Map.Entry<String, String> entry : map.entrySet()) { 357 if (value.equals(entry.getValue())) { 358 return entry.getKey(); 359 } 360 } 361 return null; 362 } 363 364 } 365