1/*
2 * Copyright (C) 2014 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
17package android.support.v4.app;
18
19import android.graphics.Rect;
20import android.transition.Transition;
21import android.transition.TransitionManager;
22import android.transition.TransitionSet;
23import android.view.View;
24import android.view.ViewGroup;
25import android.view.ViewTreeObserver;
26
27import java.util.ArrayList;
28import java.util.List;
29import java.util.Map;
30
31class FragmentTransitionCompat21 {
32    public static String getTransitionName(View view) {
33        return view.getTransitionName();
34    }
35
36    public static Object cloneTransition(Object transition) {
37        if (transition != null) {
38            transition = ((Transition)transition).clone();
39        }
40        return transition;
41    }
42
43    public static Object captureExitingViews(Object exitTransition, View root,
44            ArrayList<View> viewList, Map<String, View> namedViews, View nonExistentView) {
45        if (exitTransition != null) {
46            captureTransitioningViews(viewList, root);
47            if (namedViews != null) {
48                viewList.removeAll(namedViews.values());
49            }
50            if (viewList.isEmpty()) {
51                exitTransition = null;
52            } else {
53                viewList.add(nonExistentView);
54                addTargets((Transition) exitTransition, viewList);
55            }
56        }
57        return exitTransition;
58    }
59
60    public static void excludeTarget(Object transitionObject, View view, boolean exclude) {
61        Transition transition = (Transition) transitionObject;
62        transition.excludeTarget(view, exclude);
63    }
64
65    public static void beginDelayedTransition(ViewGroup sceneRoot, Object transitionObject) {
66        Transition transition = (Transition) transitionObject;
67        TransitionManager.beginDelayedTransition(sceneRoot, transition);
68    }
69
70    public static void setEpicenter(Object transitionObject, View view) {
71        Transition transition = (Transition) transitionObject;
72        final Rect epicenter = getBoundsOnScreen(view);
73
74        transition.setEpicenterCallback(new Transition.EpicenterCallback() {
75            @Override
76            public Rect onGetEpicenter(Transition transition) {
77                return epicenter;
78            }
79        });
80    }
81
82    public static Object wrapSharedElementTransition(Object transitionObj) {
83        if (transitionObj == null) {
84            return null;
85        }
86        Transition transition = (Transition) transitionObj;
87        if (transition == null) {
88            return null;
89        }
90        TransitionSet transitionSet = new TransitionSet();
91        transitionSet.addTransition(transition);
92        return transitionSet;
93    }
94
95    private static void excludeViews(Transition transition, Transition fromTransition,
96            ArrayList<View> views, boolean exclude) {
97        if (transition != null) {
98            final int viewCount = fromTransition == null ? 0 : views.size();
99            for (int i = 0; i < viewCount; i++) {
100                transition.excludeTarget(views.get(i), exclude);
101            }
102        }
103    }
104
105    /**
106     * Exclude (or remove the exclude) of shared element views from the enter and exit transitions.
107     *
108     * @param enterTransitionObj The enter transition
109     * @param exitTransitionObj The exit transition
110     * @param sharedElementTransitionObj The shared element transition
111     * @param views The shared element target views.
112     * @param exclude <code>true</code> to exclude or <code>false</code> to remove the excluded
113     *                views.
114     */
115    public static void excludeSharedElementViews(Object enterTransitionObj,
116            Object exitTransitionObj, Object sharedElementTransitionObj, ArrayList<View> views,
117            boolean exclude) {
118        Transition enterTransition = (Transition) enterTransitionObj;
119        Transition exitTransition = (Transition) exitTransitionObj;
120        Transition sharedElementTransition = (Transition) sharedElementTransitionObj;
121        excludeViews(enterTransition, sharedElementTransition, views, exclude);
122        excludeViews(exitTransition, sharedElementTransition, views, exclude);
123    }
124
125    /**
126     * Prepares the enter transition by adding a non-existent view to the transition's target list
127     * and setting it epicenter callback. By adding a non-existent view to the target list,
128     * we can prevent any view from being targeted at the beginning of the transition.
129     * We will add to the views before the end state of the transition is captured so that the
130     * views will appear. At the start of the transition, we clear the list of targets so that
131     * we can restore the state of the transition and use it again.
132     *
133     * <p>The shared element transition maps its shared elements immediately prior to
134     *  capturing the final state of the Transition.</p>
135     */
136    public static void addTransitionTargets(Object enterTransitionObject,
137            Object sharedElementTransitionObject, Object exitTransitionObject, final View container,
138            final ViewRetriever inFragment, final View nonExistentView,
139            EpicenterView epicenterView, final Map<String, String> nameOverrides,
140            final ArrayList<View> enteringViews, final ArrayList<View> exitingViews,
141            final Map<String, View> namedViews, final Map<String, View> renamedViews,
142            final ArrayList<View> sharedElementTargets) {
143        final Transition enterTransition = (Transition) enterTransitionObject;
144        final Transition exitTransition = (Transition) exitTransitionObject;
145        final Transition sharedElementTransition = (Transition) sharedElementTransitionObject;
146        excludeViews(enterTransition, exitTransition, exitingViews, true);
147        if (enterTransitionObject != null || sharedElementTransitionObject != null) {
148            if (enterTransition != null) {
149                enterTransition.addTarget(nonExistentView);
150            }
151            if (sharedElementTransitionObject != null) {
152                setSharedElementTargets(sharedElementTransition, nonExistentView,
153                        namedViews, sharedElementTargets);
154                excludeViews(enterTransition, sharedElementTransition, sharedElementTargets, true);
155                excludeViews(exitTransition, sharedElementTransition, sharedElementTargets, true);
156            }
157
158            container.getViewTreeObserver().addOnPreDrawListener(
159                    new ViewTreeObserver.OnPreDrawListener() {
160                        public boolean onPreDraw() {
161                            container.getViewTreeObserver().removeOnPreDrawListener(this);
162                            if (enterTransition != null) {
163                                enterTransition.removeTarget(nonExistentView);
164                            }
165                            if (inFragment != null) {
166                                View fragmentView = inFragment.getView();
167                                if (fragmentView != null) {
168                                    if (!nameOverrides.isEmpty()) {
169                                        findNamedViews(renamedViews, fragmentView);
170                                        renamedViews.keySet().retainAll(nameOverrides.values());
171                                        for (Map.Entry<String, String> entry : nameOverrides
172                                                .entrySet()) {
173                                            String to = entry.getValue();
174                                            View view = renamedViews.get(to);
175                                            if (view != null) {
176                                                String from = entry.getKey();
177                                                view.setTransitionName(from);
178                                            }
179                                        }
180                                    }
181                                    if (enterTransition != null) {
182                                        captureTransitioningViews(enteringViews, fragmentView);
183                                        enteringViews.removeAll(renamedViews.values());
184                                        enteringViews.add(nonExistentView);
185                                        addTargets(enterTransition, enteringViews);
186                                    }
187                                }
188                            }
189                            excludeViews(exitTransition, enterTransition, enteringViews, true);
190
191                            return true;
192                        }
193                    });
194            setSharedElementEpicenter(enterTransition, epicenterView);
195        }
196    }
197
198    public static Object mergeTransitions(Object enterTransitionObject,
199            Object exitTransitionObject, Object sharedElementTransitionObject,
200            boolean allowOverlap) {
201        boolean overlap = true;
202        Transition enterTransition = (Transition) enterTransitionObject;
203        Transition exitTransition = (Transition) exitTransitionObject;
204        Transition sharedElementTransition = (Transition) sharedElementTransitionObject;
205
206        if (enterTransition != null && exitTransition != null) {
207            overlap = allowOverlap;
208        }
209
210        // Wrap the transitions. Explicit targets like in enter and exit will cause the
211        // views to be targeted regardless of excluded views. If that happens, then the
212        // excluded fragments views (hidden fragments) will still be in the transition.
213
214        Transition transition;
215        if (overlap) {
216            // Regular transition -- do it all together
217            TransitionSet transitionSet = new TransitionSet();
218            if (enterTransition != null) {
219                transitionSet.addTransition(enterTransition);
220            }
221            if (exitTransition != null) {
222                transitionSet.addTransition(exitTransition);
223            }
224            if (sharedElementTransition != null) {
225                transitionSet.addTransition(sharedElementTransition);
226            }
227            transition = transitionSet;
228        } else {
229            // First do exit, then enter, but allow shared element transition to happen
230            // during both.
231            Transition staggered = null;
232            if (exitTransition != null && enterTransition != null) {
233                staggered = new TransitionSet()
234                        .addTransition(exitTransition)
235                        .addTransition(enterTransition)
236                        .setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
237            } else if (exitTransition != null) {
238                staggered = exitTransition;
239            } else if (enterTransition != null) {
240                staggered = enterTransition;
241            }
242            if (sharedElementTransition != null) {
243                TransitionSet together = new TransitionSet();
244                if (staggered != null) {
245                    together.addTransition(staggered);
246                }
247                together.addTransition(sharedElementTransition);
248                transition = together;
249            } else {
250                transition = staggered;
251            }
252        }
253        return transition;
254    }
255
256    /**
257     * Finds all children of the shared elements and sets the wrapping TransitionSet
258     * targets to point to those. It also limits transitions that have no targets to the
259     * specific shared elements. This allows developers to target child views of the
260     * shared elements specifically, but this doesn't happen by default.
261     */
262    public static void setSharedElementTargets(Object transitionObj,
263            View nonExistentView, Map<String, View> namedViews,
264            ArrayList<View> sharedElementTargets) {
265        TransitionSet transition = (TransitionSet) transitionObj;
266        sharedElementTargets.clear();
267        sharedElementTargets.addAll(namedViews.values());
268
269        final List<View> views = transition.getTargets();
270        views.clear();
271        final int count = sharedElementTargets.size();
272        for (int i = 0; i < count; i++) {
273            final View view = sharedElementTargets.get(i);
274            bfsAddViewChildren(views, view);
275        }
276        sharedElementTargets.add(nonExistentView);
277        addTargets(transition, sharedElementTargets);
278    }
279
280    /**
281     * Uses a breadth-first scheme to add startView and all of its children to views.
282     * It won't add a child if it is already in views.
283     */
284    private static void bfsAddViewChildren(final List<View> views, final View startView) {
285        final int startIndex = views.size();
286        if (containedBeforeIndex(views, startView, startIndex)) {
287            return; // This child is already in the list, so all its children are also.
288        }
289        views.add(startView);
290        for (int index = startIndex; index < views.size(); index++) {
291            final View view = views.get(index);
292            if (view instanceof ViewGroup) {
293                ViewGroup viewGroup = (ViewGroup) view;
294                final int childCount =  viewGroup.getChildCount();
295                for (int childIndex = 0; childIndex < childCount; childIndex++) {
296                    final View child = viewGroup.getChildAt(childIndex);
297                    if (!containedBeforeIndex(views, child, startIndex)) {
298                        views.add(child);
299                    }
300                }
301            }
302        }
303    }
304
305    /**
306     * Does a linear search through views for view, limited to maxIndex.
307     */
308    private static boolean containedBeforeIndex(final List<View> views, final View view,
309            final int maxIndex) {
310        for (int i = 0; i < maxIndex; i++) {
311            if (views.get(i) == view) {
312                return true;
313            }
314        }
315        return false;
316    }
317
318    private static void setSharedElementEpicenter(Transition transition,
319            final EpicenterView epicenterView) {
320        if (transition != null) {
321            transition.setEpicenterCallback(new Transition.EpicenterCallback() {
322                private Rect mEpicenter;
323
324                @Override
325                public Rect onGetEpicenter(Transition transition) {
326                    if (mEpicenter == null && epicenterView.epicenter != null) {
327                        mEpicenter = getBoundsOnScreen(epicenterView.epicenter);
328                    }
329                    return mEpicenter;
330                }
331            });
332        }
333    }
334
335    private static Rect getBoundsOnScreen(View view) {
336        Rect epicenter = new Rect();
337        int[] loc = new int[2];
338        view.getLocationOnScreen(loc);
339        // not as good as View.getBoundsOnScreen, but that's not public
340        epicenter.set(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight());
341        return epicenter;
342    }
343
344    private static void captureTransitioningViews(ArrayList<View> transitioningViews, View view) {
345        if (view.getVisibility() == View.VISIBLE) {
346            if (view instanceof ViewGroup) {
347                ViewGroup viewGroup = (ViewGroup) view;
348                if (viewGroup.isTransitionGroup()) {
349                    transitioningViews.add(viewGroup);
350                } else {
351                    int count = viewGroup.getChildCount();
352                    for (int i = 0; i < count; i++) {
353                        View child = viewGroup.getChildAt(i);
354                        captureTransitioningViews(transitioningViews, child);
355                    }
356                }
357            } else {
358                transitioningViews.add(view);
359            }
360        }
361    }
362
363    public static void findNamedViews(Map<String, View> namedViews, View view) {
364        if (view.getVisibility() == View.VISIBLE) {
365            String transitionName = view.getTransitionName();
366            if (transitionName != null) {
367                namedViews.put(transitionName, view);
368            }
369            if (view instanceof ViewGroup) {
370                ViewGroup viewGroup = (ViewGroup) view;
371                int count = viewGroup.getChildCount();
372                for (int i = 0; i < count; i++) {
373                    View child = viewGroup.getChildAt(i);
374                    findNamedViews(namedViews, child);
375                }
376            }
377        }
378    }
379
380    public static void cleanupTransitions(final View sceneRoot, final View nonExistentView,
381            Object enterTransitionObject, final ArrayList<View> enteringViews,
382            Object exitTransitionObject, final ArrayList<View> exitingViews,
383            Object sharedElementTransitionObject, final ArrayList<View> sharedElementTargets,
384            Object overallTransitionObject, final ArrayList<View> hiddenViews,
385            final Map<String, View> renamedViews) {
386        final Transition enterTransition = (Transition) enterTransitionObject;
387        final Transition exitTransition = (Transition) exitTransitionObject;
388        final Transition sharedElementTransition = (Transition) sharedElementTransitionObject;
389        final Transition overallTransition = (Transition) overallTransitionObject;
390        if (overallTransition != null) {
391            sceneRoot.getViewTreeObserver().addOnPreDrawListener(
392                    new ViewTreeObserver.OnPreDrawListener() {
393                public boolean onPreDraw() {
394                    sceneRoot.getViewTreeObserver().removeOnPreDrawListener(this);
395                    if (enterTransition != null) {
396                        removeTargets(enterTransition, enteringViews);
397                        excludeViews(enterTransition, exitTransition, exitingViews, false);
398                        excludeViews(enterTransition, sharedElementTransition, sharedElementTargets,
399                                false);
400                    }
401                    if (exitTransition != null) {
402                        removeTargets(exitTransition, exitingViews);
403                        excludeViews(exitTransition, enterTransition, enteringViews, false);
404                        excludeViews(exitTransition, sharedElementTransition, sharedElementTargets,
405                                false);
406                    }
407                    if (sharedElementTransition != null) {
408                        removeTargets(sharedElementTransition, sharedElementTargets);
409                    }
410                    for (Map.Entry<String, View> entry : renamedViews.entrySet()) {
411                        View view = entry.getValue();
412                        String name = entry.getKey();
413                        view.setTransitionName(name);
414                    }
415                    int numViews = hiddenViews.size();
416                    for (int i = 0; i < numViews; i++) {
417                        overallTransition.excludeTarget(hiddenViews.get(i), false);
418                    }
419                    overallTransition.excludeTarget(nonExistentView, false);
420                    return true;
421                }
422            });
423        }
424    }
425
426    /**
427     * This method removes the views from transitions that target ONLY those views.
428     * The views list should match those added in addTargets and should contain
429     * one view that is not in the view hierarchy (state.nonExistentView).
430     */
431    public static void removeTargets(Object transitionObject, ArrayList<View> views) {
432        Transition transition = (Transition) transitionObject;
433        if (transition instanceof TransitionSet) {
434            TransitionSet set = (TransitionSet) transition;
435            int numTransitions = set.getTransitionCount();
436            for (int i = 0; i < numTransitions; i++) {
437                Transition child = set.getTransitionAt(i);
438                removeTargets(child, views);
439            }
440        } else if (!hasSimpleTarget(transition)) {
441            List<View> targets = transition.getTargets();
442            if (targets != null && targets.size() == views.size() &&
443                    targets.containsAll(views)) {
444                // We have an exact match. We must have added these earlier in addTargets
445                for (int i = views.size() - 1; i >= 0; i--) {
446                    transition.removeTarget(views.get(i));
447                }
448            }
449        }
450    }
451
452    /**
453     * This method adds views as targets to the transition, but only if the transition
454     * doesn't already have a target. It is best for views to contain one View object
455     * that does not exist in the view hierarchy (state.nonExistentView) so that
456     * when they are removed later, a list match will suffice to remove the targets.
457     * Otherwise, if you happened to have targeted the exact views for the transition,
458     * the removeTargets call will remove them unexpectedly.
459     */
460    public static void addTargets(Object transitionObject, ArrayList<View> views) {
461        Transition transition = (Transition) transitionObject;
462        if (transition instanceof TransitionSet) {
463            TransitionSet set = (TransitionSet) transition;
464            int numTransitions = set.getTransitionCount();
465            for (int i = 0; i < numTransitions; i++) {
466                Transition child = set.getTransitionAt(i);
467                addTargets(child, views);
468            }
469        } else if (!hasSimpleTarget(transition)) {
470            List<View> targets = transition.getTargets();
471            if (isNullOrEmpty(targets)) {
472                // We can just add the target views
473                int numViews = views.size();
474                for (int i = 0; i < numViews; i++) {
475                    transition.addTarget(views.get(i));
476                }
477            }
478        }
479    }
480
481    private static boolean hasSimpleTarget(Transition transition) {
482        return !isNullOrEmpty(transition.getTargetIds()) ||
483                !isNullOrEmpty(transition.getTargetNames()) ||
484                !isNullOrEmpty(transition.getTargetTypes());
485    }
486
487    private static boolean isNullOrEmpty(List list) {
488        return list == null || list.isEmpty();
489    }
490
491    public interface ViewRetriever {
492        View getView();
493    }
494
495    public static class EpicenterView {
496        public View epicenter;
497    }
498}
499