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         mView.setTransitionVisibility(View.INVISIBLE);
43         parent.invalidate();
44     }
45 
46     @Override
onDraw(Canvas canvas)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 
setMatrix(Matrix matrix)60     public void setMatrix(Matrix matrix) {
61         mRenderNode.setAnimationMatrix(matrix);
62     }
63 
64     @Override
setVisibility(@isibility int visibility)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
onDetachedFromWindow()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 
calculateMatrix(View view, ViewGroup host, Matrix matrix)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 
addGhost(View view, ViewGroup viewGroup, Matrix matrix)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 
addGhost(View view, ViewGroup viewGroup)134     public static GhostView addGhost(View view, ViewGroup viewGroup) {
135         return addGhost(view, viewGroup, null);
136     }
137 
removeGhost(View view)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 
getGhost(View view)150     public static GhostView getGhost(View view) {
151         return view.mGhostView;
152     }
153 
copySize(View from, View to)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      */
moveGhostViewsToTop(ViewGroup viewGroup, ArrayList<View> tempViews)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      */
insertIntoOverlay(ViewGroup viewGroup, ViewGroup wrapper, GhostView ghostView, ArrayList<View> tempParents, int firstGhost)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      */
getInsertIndex(ViewGroup overlayViewGroup, ArrayList<View> viewParents, ArrayList<View> tempParents, int firstGhost)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      */
isGhostWrapper(View view)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      */
isOnTop(ArrayList<View> viewParents, ArrayList<View> comparedWith)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      */
getParents(View view, ArrayList<View> parents)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      */
isOnTop(View view, View comparedWith)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