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