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