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                        @Override
161                        public boolean onPreDraw() {
162                            container.getViewTreeObserver().removeOnPreDrawListener(this);
163                            if (enterTransition != null) {
164                                enterTransition.removeTarget(nonExistentView);
165                            }
166                            if (inFragment != null) {
167                                View fragmentView = inFragment.getView();
168                                if (fragmentView != null) {
169                                    if (!nameOverrides.isEmpty()) {
170                                        findNamedViews(renamedViews, fragmentView);
171                                        renamedViews.keySet().retainAll(nameOverrides.values());
172                                        for (Map.Entry<String, String> entry : nameOverrides
173                                                .entrySet()) {
174                                            String to = entry.getValue();
175                                            View view = renamedViews.get(to);
176                                            if (view != null) {
177                                                String from = entry.getKey();
178                                                view.setTransitionName(from);
179                                            }
180                                        }
181                                    }
182                                    if (enterTransition != null) {
183                                        captureTransitioningViews(enteringViews, fragmentView);
184                                        enteringViews.removeAll(renamedViews.values());
185                                        enteringViews.add(nonExistentView);
186                                        addTargets(enterTransition, enteringViews);
187                                    }
188                                }
189                            }
190                            excludeViews(exitTransition, enterTransition, enteringViews, true);
191
192                            return true;
193                        }
194                    });
195            setSharedElementEpicenter(enterTransition, epicenterView);
196        }
197    }
198
199    public static Object mergeTransitions(Object enterTransitionObject,
200            Object exitTransitionObject, Object sharedElementTransitionObject,
201            boolean allowOverlap) {
202        boolean overlap = true;
203        Transition enterTransition = (Transition) enterTransitionObject;
204        Transition exitTransition = (Transition) exitTransitionObject;
205        Transition sharedElementTransition = (Transition) sharedElementTransitionObject;
206
207        if (enterTransition != null && exitTransition != null) {
208            overlap = allowOverlap;
209        }
210
211        // Wrap the transitions. Explicit targets like in enter and exit will cause the
212        // views to be targeted regardless of excluded views. If that happens, then the
213        // excluded fragments views (hidden fragments) will still be in the transition.
214
215        Transition transition;
216        if (overlap) {
217            // Regular transition -- do it all together
218            TransitionSet transitionSet = new TransitionSet();
219            if (enterTransition != null) {
220                transitionSet.addTransition(enterTransition);
221            }
222            if (exitTransition != null) {
223                transitionSet.addTransition(exitTransition);
224            }
225            if (sharedElementTransition != null) {
226                transitionSet.addTransition(sharedElementTransition);
227            }
228            transition = transitionSet;
229        } else {
230            // First do exit, then enter, but allow shared element transition to happen
231            // during both.
232            Transition staggered = null;
233            if (exitTransition != null && enterTransition != null) {
234                staggered = new TransitionSet()
235                        .addTransition(exitTransition)
236                        .addTransition(enterTransition)
237                        .setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
238            } else if (exitTransition != null) {
239                staggered = exitTransition;
240            } else if (enterTransition != null) {
241                staggered = enterTransition;
242            }
243            if (sharedElementTransition != null) {
244                TransitionSet together = new TransitionSet();
245                if (staggered != null) {
246                    together.addTransition(staggered);
247                }
248                together.addTransition(sharedElementTransition);
249                transition = together;
250            } else {
251                transition = staggered;
252            }
253        }
254        return transition;
255    }
256
257    /**
258     * Finds all children of the shared elements and sets the wrapping TransitionSet
259     * targets to point to those. It also limits transitions that have no targets to the
260     * specific shared elements. This allows developers to target child views of the
261     * shared elements specifically, but this doesn't happen by default.
262     */
263    public static void setSharedElementTargets(Object transitionObj,
264            View nonExistentView, Map<String, View> namedViews,
265            ArrayList<View> sharedElementTargets) {
266        TransitionSet transition = (TransitionSet) transitionObj;
267        sharedElementTargets.clear();
268        sharedElementTargets.addAll(namedViews.values());
269
270        final List<View> views = transition.getTargets();
271        views.clear();
272        final int count = sharedElementTargets.size();
273        for (int i = 0; i < count; i++) {
274            final View view = sharedElementTargets.get(i);
275            bfsAddViewChildren(views, view);
276        }
277        sharedElementTargets.add(nonExistentView);
278        addTargets(transition, sharedElementTargets);
279    }
280
281    /**
282     * Uses a breadth-first scheme to add startView and all of its children to views.
283     * It won't add a child if it is already in views.
284     */
285    private static void bfsAddViewChildren(final List<View> views, final View startView) {
286        final int startIndex = views.size();
287        if (containedBeforeIndex(views, startView, startIndex)) {
288            return; // This child is already in the list, so all its children are also.
289        }
290        views.add(startView);
291        for (int index = startIndex; index < views.size(); index++) {
292            final View view = views.get(index);
293            if (view instanceof ViewGroup) {
294                ViewGroup viewGroup = (ViewGroup) view;
295                final int childCount =  viewGroup.getChildCount();
296                for (int childIndex = 0; childIndex < childCount; childIndex++) {
297                    final View child = viewGroup.getChildAt(childIndex);
298                    if (!containedBeforeIndex(views, child, startIndex)) {
299                        views.add(child);
300                    }
301                }
302            }
303        }
304    }
305
306    /**
307     * Does a linear search through views for view, limited to maxIndex.
308     */
309    private static boolean containedBeforeIndex(final List<View> views, final View view,
310            final int maxIndex) {
311        for (int i = 0; i < maxIndex; i++) {
312            if (views.get(i) == view) {
313                return true;
314            }
315        }
316        return false;
317    }
318
319    private static void setSharedElementEpicenter(Transition transition,
320            final EpicenterView epicenterView) {
321        if (transition != null) {
322            transition.setEpicenterCallback(new Transition.EpicenterCallback() {
323                private Rect mEpicenter;
324
325                @Override
326                public Rect onGetEpicenter(Transition transition) {
327                    if (mEpicenter == null && epicenterView.epicenter != null) {
328                        mEpicenter = getBoundsOnScreen(epicenterView.epicenter);
329                    }
330                    return mEpicenter;
331                }
332            });
333        }
334    }
335
336    private static Rect getBoundsOnScreen(View view) {
337        Rect epicenter = new Rect();
338        int[] loc = new int[2];
339        view.getLocationOnScreen(loc);
340        // not as good as View.getBoundsOnScreen, but that's not public
341        epicenter.set(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight());
342        return epicenter;
343    }
344
345    private static void captureTransitioningViews(ArrayList<View> transitioningViews, View view) {
346        if (view.getVisibility() == View.VISIBLE) {
347            if (view instanceof ViewGroup) {
348                ViewGroup viewGroup = (ViewGroup) view;
349                if (viewGroup.isTransitionGroup()) {
350                    transitioningViews.add(viewGroup);
351                } else {
352                    int count = viewGroup.getChildCount();
353                    for (int i = 0; i < count; i++) {
354                        View child = viewGroup.getChildAt(i);
355                        captureTransitioningViews(transitioningViews, child);
356                    }
357                }
358            } else {
359                transitioningViews.add(view);
360            }
361        }
362    }
363
364    public static void findNamedViews(Map<String, View> namedViews, View view) {
365        if (view.getVisibility() == View.VISIBLE) {
366            String transitionName = view.getTransitionName();
367            if (transitionName != null) {
368                namedViews.put(transitionName, view);
369            }
370            if (view instanceof ViewGroup) {
371                ViewGroup viewGroup = (ViewGroup) view;
372                int count = viewGroup.getChildCount();
373                for (int i = 0; i < count; i++) {
374                    View child = viewGroup.getChildAt(i);
375                    findNamedViews(namedViews, child);
376                }
377            }
378        }
379    }
380
381    public static void cleanupTransitions(final View sceneRoot, final View nonExistentView,
382            Object enterTransitionObject, final ArrayList<View> enteringViews,
383            Object exitTransitionObject, final ArrayList<View> exitingViews,
384            Object sharedElementTransitionObject, final ArrayList<View> sharedElementTargets,
385            Object overallTransitionObject, final ArrayList<View> hiddenViews,
386            final Map<String, View> renamedViews) {
387        final Transition enterTransition = (Transition) enterTransitionObject;
388        final Transition exitTransition = (Transition) exitTransitionObject;
389        final Transition sharedElementTransition = (Transition) sharedElementTransitionObject;
390        final Transition overallTransition = (Transition) overallTransitionObject;
391        if (overallTransition != null) {
392            sceneRoot.getViewTreeObserver().addOnPreDrawListener(
393                    new ViewTreeObserver.OnPreDrawListener() {
394                @Override
395                public boolean onPreDraw() {
396                    sceneRoot.getViewTreeObserver().removeOnPreDrawListener(this);
397                    if (enterTransition != null) {
398                        removeTargets(enterTransition, enteringViews);
399                        excludeViews(enterTransition, exitTransition, exitingViews, false);
400                        excludeViews(enterTransition, sharedElementTransition, sharedElementTargets,
401                                false);
402                    }
403                    if (exitTransition != null) {
404                        removeTargets(exitTransition, exitingViews);
405                        excludeViews(exitTransition, enterTransition, enteringViews, false);
406                        excludeViews(exitTransition, sharedElementTransition, sharedElementTargets,
407                                false);
408                    }
409                    if (sharedElementTransition != null) {
410                        removeTargets(sharedElementTransition, sharedElementTargets);
411                    }
412                    for (Map.Entry<String, View> entry : renamedViews.entrySet()) {
413                        View view = entry.getValue();
414                        String name = entry.getKey();
415                        view.setTransitionName(name);
416                    }
417                    int numViews = hiddenViews.size();
418                    for (int i = 0; i < numViews; i++) {
419                        overallTransition.excludeTarget(hiddenViews.get(i), false);
420                    }
421                    overallTransition.excludeTarget(nonExistentView, false);
422                    return true;
423                }
424            });
425        }
426    }
427
428    /**
429     * This method removes the views from transitions that target ONLY those views.
430     * The views list should match those added in addTargets and should contain
431     * one view that is not in the view hierarchy (state.nonExistentView).
432     */
433    public static void removeTargets(Object transitionObject, ArrayList<View> views) {
434        Transition transition = (Transition) transitionObject;
435        if (transition instanceof TransitionSet) {
436            TransitionSet set = (TransitionSet) transition;
437            int numTransitions = set.getTransitionCount();
438            for (int i = 0; i < numTransitions; i++) {
439                Transition child = set.getTransitionAt(i);
440                removeTargets(child, views);
441            }
442        } else if (!hasSimpleTarget(transition)) {
443            List<View> targets = transition.getTargets();
444            if (targets != null && targets.size() == views.size() &&
445                    targets.containsAll(views)) {
446                // We have an exact match. We must have added these earlier in addTargets
447                for (int i = views.size() - 1; i >= 0; i--) {
448                    transition.removeTarget(views.get(i));
449                }
450            }
451        }
452    }
453
454    /**
455     * This method adds views as targets to the transition, but only if the transition
456     * doesn't already have a target. It is best for views to contain one View object
457     * that does not exist in the view hierarchy (state.nonExistentView) so that
458     * when they are removed later, a list match will suffice to remove the targets.
459     * Otherwise, if you happened to have targeted the exact views for the transition,
460     * the removeTargets call will remove them unexpectedly.
461     */
462    public static void addTargets(Object transitionObject, ArrayList<View> views) {
463        Transition transition = (Transition) transitionObject;
464        if (transition instanceof TransitionSet) {
465            TransitionSet set = (TransitionSet) transition;
466            int numTransitions = set.getTransitionCount();
467            for (int i = 0; i < numTransitions; i++) {
468                Transition child = set.getTransitionAt(i);
469                addTargets(child, views);
470            }
471        } else if (!hasSimpleTarget(transition)) {
472            List<View> targets = transition.getTargets();
473            if (isNullOrEmpty(targets)) {
474                // We can just add the target views
475                int numViews = views.size();
476                for (int i = 0; i < numViews; i++) {
477                    transition.addTarget(views.get(i));
478                }
479            }
480        }
481    }
482
483    private static boolean hasSimpleTarget(Transition transition) {
484        return !isNullOrEmpty(transition.getTargetIds()) ||
485                !isNullOrEmpty(transition.getTargetNames()) ||
486                !isNullOrEmpty(transition.getTargetTypes());
487    }
488
489    private static boolean isNullOrEmpty(List list) {
490        return list == null || list.isEmpty();
491    }
492
493    public interface ViewRetriever {
494        View getView();
495    }
496
497    public static class EpicenterView {
498        public View epicenter;
499    }
500}
501