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