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 */
16package android.view;
17
18import android.graphics.Canvas;
19import android.graphics.Matrix;
20import android.widget.FrameLayout;
21
22import java.util.ArrayList;
23
24/**
25 * This view draws another View in an Overlay without changing the parent. It will not be drawn
26 * by its parent because its visibility is set to INVISIBLE, but will be drawn
27 * here using its render node. When the GhostView is set to INVISIBLE, the View it is
28 * shadowing will become VISIBLE and when the GhostView becomes VISIBLE, the shadowed
29 * view becomes INVISIBLE.
30 * @hide
31 */
32public class GhostView extends View {
33    private final View mView;
34    private int mReferences;
35    private boolean mBeingMoved;
36
37    private GhostView(View view) {
38        super(view.getContext());
39        mView = view;
40        mView.mGhostView = this;
41        final ViewGroup parent = (ViewGroup) mView.getParent();
42        mView.setTransitionVisibility(View.INVISIBLE);
43        parent.invalidate();
44    }
45
46    @Override
47    protected void onDraw(Canvas canvas) {
48        if (canvas instanceof DisplayListCanvas) {
49            DisplayListCanvas dlCanvas = (DisplayListCanvas) canvas;
50            mView.mRecreateDisplayList = true;
51            RenderNode renderNode = mView.updateDisplayListIfDirty();
52            if (renderNode.isValid()) {
53                dlCanvas.insertReorderBarrier(); // enable shadow for this rendernode
54                dlCanvas.drawRenderNode(renderNode);
55                dlCanvas.insertInorderBarrier(); // re-disable reordering/shadows
56            }
57        }
58    }
59
60    public void setMatrix(Matrix matrix) {
61        mRenderNode.setAnimationMatrix(matrix);
62    }
63
64    @Override
65    public void setVisibility(@Visibility int visibility) {
66        super.setVisibility(visibility);
67        if (mView.mGhostView == this) {
68            int inverseVisibility = (visibility == View.VISIBLE) ? View.INVISIBLE : View.VISIBLE;
69            mView.setTransitionVisibility(inverseVisibility);
70        }
71    }
72
73    @Override
74    protected void onDetachedFromWindow() {
75        super.onDetachedFromWindow();
76        if (!mBeingMoved) {
77            mView.setTransitionVisibility(View.VISIBLE);
78            mView.mGhostView = null;
79            final ViewGroup parent = (ViewGroup) mView.getParent();
80            if (parent != null) {
81                parent.invalidate();
82            }
83        }
84    }
85
86    public static void calculateMatrix(View view, ViewGroup host, Matrix matrix) {
87        ViewGroup parent = (ViewGroup) view.getParent();
88        matrix.reset();
89        parent.transformMatrixToGlobal(matrix);
90        matrix.preTranslate(-parent.getScrollX(), -parent.getScrollY());
91        host.transformMatrixToLocal(matrix);
92    }
93
94    public static GhostView addGhost(View view, ViewGroup viewGroup, Matrix matrix) {
95        if (!(view.getParent() instanceof ViewGroup)) {
96            throw new IllegalArgumentException("Ghosted views must be parented by a ViewGroup");
97        }
98        ViewGroupOverlay overlay = viewGroup.getOverlay();
99        ViewOverlay.OverlayViewGroup overlayViewGroup = overlay.mOverlayViewGroup;
100        GhostView ghostView = view.mGhostView;
101        int previousRefCount = 0;
102        if (ghostView != null) {
103            View oldParent = (View) ghostView.getParent();
104            ViewGroup oldGrandParent = (ViewGroup) oldParent.getParent();
105            if (oldGrandParent != overlayViewGroup) {
106                previousRefCount = ghostView.mReferences;
107                oldGrandParent.removeView(oldParent);
108                ghostView = null;
109            }
110        }
111        if (ghostView == null) {
112            if (matrix == null) {
113                matrix = new Matrix();
114                calculateMatrix(view, viewGroup, matrix);
115            }
116            ghostView = new GhostView(view);
117            ghostView.setMatrix(matrix);
118            FrameLayout parent = new FrameLayout(view.getContext());
119            parent.setClipChildren(false);
120            copySize(viewGroup, parent);
121            copySize(viewGroup, ghostView);
122            parent.addView(ghostView);
123            ArrayList<View> tempViews = new ArrayList<View>();
124            int firstGhost = moveGhostViewsToTop(overlay.mOverlayViewGroup, tempViews);
125            insertIntoOverlay(overlay.mOverlayViewGroup, parent, ghostView, tempViews, firstGhost);
126            ghostView.mReferences = previousRefCount;
127        } else if (matrix != null) {
128            ghostView.setMatrix(matrix);
129        }
130        ghostView.mReferences++;
131        return ghostView;
132    }
133
134    public static GhostView addGhost(View view, ViewGroup viewGroup) {
135        return addGhost(view, viewGroup, null);
136    }
137
138    public static void removeGhost(View view) {
139        GhostView ghostView = view.mGhostView;
140        if (ghostView != null) {
141            ghostView.mReferences--;
142            if (ghostView.mReferences == 0) {
143                ViewGroup parent = (ViewGroup) ghostView.getParent();
144                ViewGroup grandParent = (ViewGroup) parent.getParent();
145                grandParent.removeView(parent);
146            }
147        }
148    }
149
150    public static GhostView getGhost(View view) {
151        return view.mGhostView;
152    }
153
154    private static void copySize(View from, View to) {
155        to.setLeft(0);
156        to.setTop(0);
157        to.setRight(from.getWidth());
158        to.setBottom(from.getHeight());
159    }
160
161    /**
162     * Move the GhostViews to the end so that they are on top of other views and it is easier
163     * to do binary search for the correct location for the GhostViews in insertIntoOverlay.
164     *
165     * @return The index of the first GhostView or -1 if no GhostView is in the ViewGroup
166     */
167    private static int moveGhostViewsToTop(ViewGroup viewGroup, ArrayList<View> tempViews) {
168        final int numChildren = viewGroup.getChildCount();
169        if (numChildren == 0) {
170            return -1;
171        } else if (isGhostWrapper(viewGroup.getChildAt(numChildren - 1))) {
172            // GhostViews are already at the end
173            int firstGhost = numChildren - 1;
174            for (int i = numChildren - 2; i >= 0; i--) {
175                if (!isGhostWrapper(viewGroup.getChildAt(i))) {
176                    break;
177                }
178                firstGhost = i;
179            }
180            return firstGhost;
181        }
182
183        // Remove all GhostViews from the middle
184        for (int i = numChildren - 2; i >= 0; i--) {
185            View child = viewGroup.getChildAt(i);
186            if (isGhostWrapper(child)) {
187                tempViews.add(child);
188                GhostView ghostView = (GhostView)((ViewGroup)child).getChildAt(0);
189                ghostView.mBeingMoved = true;
190                viewGroup.removeViewAt(i);
191                ghostView.mBeingMoved = false;
192            }
193        }
194
195        final int firstGhost;
196        if (tempViews.isEmpty()) {
197            firstGhost = -1;
198        } else {
199            firstGhost = viewGroup.getChildCount();
200            // Add the GhostViews to the end
201            for (int i = tempViews.size() - 1; i >= 0; i--) {
202                viewGroup.addView(tempViews.get(i));
203            }
204            tempViews.clear();
205        }
206        return firstGhost;
207    }
208
209    /**
210     * Inserts a GhostView into the overlay's ViewGroup in the order in which they
211     * should be displayed by the UI.
212     */
213    private static void insertIntoOverlay(ViewGroup viewGroup, ViewGroup wrapper,
214            GhostView ghostView, ArrayList<View> tempParents, int firstGhost) {
215        if (firstGhost == -1) {
216            viewGroup.addView(wrapper);
217        } else {
218            ArrayList<View> viewParents = new ArrayList<View>();
219            getParents(ghostView.mView, viewParents);
220
221            int index = getInsertIndex(viewGroup, viewParents, tempParents, firstGhost);
222            if (index < 0 || index >= viewGroup.getChildCount()) {
223                viewGroup.addView(wrapper);
224            } else {
225                viewGroup.addView(wrapper, index);
226            }
227        }
228    }
229
230    /**
231     * Find the index into the overlay to insert the GhostView based on the order that the
232     * views should be drawn. This keeps GhostViews layered in the same order
233     * that they are ordered in the UI.
234     */
235    private static int getInsertIndex(ViewGroup overlayViewGroup, ArrayList<View> viewParents,
236            ArrayList<View> tempParents, int firstGhost) {
237        int low = firstGhost;
238        int high = overlayViewGroup.getChildCount() - 1;
239
240        while (low <= high) {
241            int mid = (low + high) / 2;
242            ViewGroup wrapper = (ViewGroup) overlayViewGroup.getChildAt(mid);
243            GhostView midView = (GhostView) wrapper.getChildAt(0);
244            getParents(midView.mView, tempParents);
245            if (isOnTop(viewParents, tempParents)) {
246                low = mid + 1;
247            } else {
248                high = mid - 1;
249            }
250            tempParents.clear();
251        }
252
253        return low;
254    }
255
256    /**
257     * Returns true if view is a GhostView's FrameLayout wrapper.
258     */
259    private static boolean isGhostWrapper(View view) {
260        if (view instanceof FrameLayout) {
261            FrameLayout frameLayout = (FrameLayout) view;
262            if (frameLayout.getChildCount() == 1) {
263                View child = frameLayout.getChildAt(0);
264                return child instanceof GhostView;
265            }
266        }
267        return false;
268    }
269
270    /**
271     * Returns true if viewParents is from a View that is on top of the comparedWith's view.
272     * The ArrayLists contain the ancestors of views in order from top most grandparent, to
273     * the view itself, in order. The goal is to find the first matching parent and then
274     * compare the draw order of the siblings.
275     */
276    private static boolean isOnTop(ArrayList<View> viewParents, ArrayList<View> comparedWith) {
277        if (viewParents.isEmpty() || comparedWith.isEmpty() ||
278                viewParents.get(0) != comparedWith.get(0)) {
279            // Not the same decorView -- arbitrary ordering
280            return true;
281        }
282        int depth = Math.min(viewParents.size(), comparedWith.size());
283        for (int i = 1; i < depth; i++) {
284            View viewParent = viewParents.get(i);
285            View comparedWithParent = comparedWith.get(i);
286
287            if (viewParent != comparedWithParent) {
288                // i - 1 is the same parent, but these are different children.
289                return isOnTop(viewParent, comparedWithParent);
290            }
291        }
292
293        // one of these is the parent of the other
294        boolean isComparedWithTheParent = (comparedWith.size() == depth);
295        return isComparedWithTheParent;
296    }
297
298    /**
299     * Adds all the parents, grandparents, etc. of view to parents.
300     */
301    private static void getParents(View view, ArrayList<View> parents) {
302        ViewParent parent = view.getParent();
303        if (parent != null && parent instanceof ViewGroup) {
304            getParents((View) parent, parents);
305        }
306        parents.add(view);
307    }
308
309    /**
310     * Returns true if view would be drawn on top of comparedWith or false otherwise.
311     * view and comparedWith are siblings with the same parent. This uses the logic
312     * that dispatchDraw uses to determine which View should be drawn first.
313     */
314    private static boolean isOnTop(View view, View comparedWith) {
315        ViewGroup parent = (ViewGroup) view.getParent();
316
317        final int childrenCount = parent.getChildCount();
318        final ArrayList<View> preorderedList = parent.buildOrderedChildList();
319        final boolean customOrder = preorderedList == null
320                && parent.isChildrenDrawingOrderEnabled();
321
322        // This default value shouldn't be used because both view and comparedWith
323        // should be in the list. If there is an error, then just return an arbitrary
324        // view is on top.
325        boolean isOnTop = true;
326        for (int i = 0; i < childrenCount; i++) {
327            int childIndex = customOrder ? parent.getChildDrawingOrder(childrenCount, i) : i;
328            final View child = (preorderedList == null)
329                    ? parent.getChildAt(childIndex) : preorderedList.get(childIndex);
330            if (child == view) {
331                isOnTop = false;
332                break;
333            } else if (child == comparedWith) {
334                isOnTop = true;
335                break;
336            }
337        }
338
339        if (preorderedList != null) {
340            preorderedList.clear();
341        }
342        return isOnTop;
343    }
344}
345