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 package android.view;
17 
18 import android.graphics.Canvas;
19 import android.graphics.Matrix;
20 import android.widget.FrameLayout;
21 
22 import 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  */
32 public class GhostView extends View {
33     private final View mView;
34     private int mReferences;
35     private boolean mBeingMoved;
36 
GhostView(View view)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
onDraw(Canvas canvas)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 
setMatrix(Matrix matrix)61     public void setMatrix(Matrix matrix) {
62         mRenderNode.setAnimationMatrix(matrix);
63     }
64 
65     @Override
setVisibility(@isibility int visibility)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 
setGhostedVisibility(int visibility)74     private void setGhostedVisibility(int visibility) {
75         mView.mViewFlags = (mView.mViewFlags & ~View.VISIBILITY_MASK) | visibility;
76     }
77 
78     @Override
onDetachedFromWindow()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 
calculateMatrix(View view, ViewGroup host, Matrix matrix)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 
addGhost(View view, ViewGroup viewGroup, Matrix matrix)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 
addGhost(View view, ViewGroup viewGroup)140     public static GhostView addGhost(View view, ViewGroup viewGroup) {
141         return addGhost(view, viewGroup, null);
142     }
143 
removeGhost(View view)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 
getGhost(View view)156     public static GhostView getGhost(View view) {
157         return view.mGhostView;
158     }
159 
copySize(View from, View to)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      */
moveGhostViewsToTop(ViewGroup viewGroup, ArrayList<View> tempViews)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      */
insertIntoOverlay(ViewGroup viewGroup, ViewGroup wrapper, GhostView ghostView, ArrayList<View> tempParents, int firstGhost)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      */
getInsertIndex(ViewGroup overlayViewGroup, ArrayList<View> viewParents, ArrayList<View> tempParents, int firstGhost)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      */
isGhostWrapper(View view)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      */
isOnTop(ArrayList<View> viewParents, ArrayList<View> comparedWith)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      */
getParents(View view, ArrayList<View> parents)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      */
isOnTop(View view, View comparedWith)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