1 /* 2 * Copyright (C) 2013 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.animation.LayoutTransition; 19 import android.annotation.NonNull; 20 import android.compat.annotation.UnsupportedAppUsage; 21 import android.content.Context; 22 import android.graphics.Canvas; 23 import android.graphics.Rect; 24 import android.graphics.drawable.Drawable; 25 import android.os.Build; 26 27 import java.util.ArrayList; 28 29 /** 30 * An overlay is an extra layer that sits on top of a View (the "host view") 31 * which is drawn after all other content in that view (including children, 32 * if the view is a ViewGroup). Interaction with the overlay layer is done 33 * by adding and removing drawables. 34 * 35 * <p>An overlay requested from a ViewGroup is of type {@link ViewGroupOverlay}, 36 * which also supports adding and removing views.</p> 37 * 38 * @see View#getOverlay() View.getOverlay() 39 * @see ViewGroup#getOverlay() ViewGroup.getOverlay() 40 * @see ViewGroupOverlay 41 */ 42 public class ViewOverlay { 43 44 /** 45 * The actual container for the drawables (and views, if it's a ViewGroupOverlay). 46 * All of the management and rendering details for the overlay are handled in 47 * OverlayViewGroup. 48 */ 49 OverlayViewGroup mOverlayViewGroup; 50 ViewOverlay(Context context, View hostView)51 ViewOverlay(Context context, View hostView) { 52 mOverlayViewGroup = new OverlayViewGroup(context, hostView); 53 } 54 55 /** 56 * Used internally by View and ViewGroup to handle drawing and invalidation 57 * of the overlay 58 * @return 59 */ 60 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) getOverlayView()61 ViewGroup getOverlayView() { 62 return mOverlayViewGroup; 63 } 64 65 /** 66 * Adds a {@link Drawable} to the overlay. The bounds of the drawable should be relative to 67 * the host view. Any drawable added to the overlay should be removed when it is no longer 68 * needed or no longer visible. Adding an already existing {@link Drawable} 69 * is a no-op. Passing <code>null</code> parameter will result in an 70 * {@link IllegalArgumentException} being thrown. 71 * 72 * @param drawable The {@link Drawable} to be added to the overlay. This drawable will be 73 * drawn when the view redraws its overlay. {@link Drawable}s will be drawn in the order that 74 * they were added. 75 * @see #remove(Drawable) 76 */ add(@onNull Drawable drawable)77 public void add(@NonNull Drawable drawable) { 78 mOverlayViewGroup.add(drawable); 79 } 80 81 /** 82 * Removes the specified {@link Drawable} from the overlay. Removing a {@link Drawable} that was 83 * not added with {@link #add(Drawable)} is a no-op. Passing <code>null</code> parameter will 84 * result in an {@link IllegalArgumentException} being thrown. 85 * 86 * @param drawable The {@link Drawable} to be removed from the overlay. 87 * @see #add(Drawable) 88 */ remove(@onNull Drawable drawable)89 public void remove(@NonNull Drawable drawable) { 90 mOverlayViewGroup.remove(drawable); 91 } 92 93 /** 94 * Removes all content from the overlay. 95 */ clear()96 public void clear() { 97 mOverlayViewGroup.clear(); 98 } 99 100 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) isEmpty()101 boolean isEmpty() { 102 return mOverlayViewGroup.isEmpty(); 103 } 104 105 /** 106 * OverlayViewGroup is a container that View and ViewGroup use to host 107 * drawables and views added to their overlays ({@link ViewOverlay} and 108 * {@link ViewGroupOverlay}, respectively). Drawables are added to the overlay 109 * via the add/remove methods in ViewOverlay, Views are added/removed via 110 * ViewGroupOverlay. These drawable and view objects are 111 * drawn whenever the view itself is drawn; first the view draws its own 112 * content (and children, if it is a ViewGroup), then it draws its overlay 113 * (if it has one). 114 * 115 * <p>Besides managing and drawing the list of drawables, this class serves 116 * two purposes: 117 * (1) it noops layout calls because children are absolutely positioned and 118 * (2) it forwards all invalidation calls to its host view. The invalidation 119 * redirect is necessary because the overlay is not a child of the host view 120 * and invalidation cannot therefore follow the normal path up through the 121 * parent hierarchy.</p> 122 * 123 * @see View#getOverlay() 124 * @see ViewGroup#getOverlay() 125 */ 126 static class OverlayViewGroup extends ViewGroup { 127 128 /** 129 * The View for which this is an overlay. Invalidations of the overlay are redirected to 130 * this host view. 131 */ 132 final View mHostView; 133 134 /** 135 * The set of drawables to draw when the overlay is rendered. 136 */ 137 ArrayList<Drawable> mDrawables = null; 138 OverlayViewGroup(Context context, View hostView)139 OverlayViewGroup(Context context, View hostView) { 140 super(context); 141 mHostView = hostView; 142 mAttachInfo = mHostView.mAttachInfo; 143 144 mRight = hostView.getWidth(); 145 mBottom = hostView.getHeight(); 146 // pass right+bottom directly to RenderNode, since not going through setters 147 mRenderNode.setLeftTopRightBottom(0, 0, mRight, mBottom); 148 } 149 add(@onNull Drawable drawable)150 public void add(@NonNull Drawable drawable) { 151 if (drawable == null) { 152 throw new IllegalArgumentException("drawable must be non-null"); 153 } 154 if (mDrawables == null) { 155 mDrawables = new ArrayList<>(); 156 } 157 if (!mDrawables.contains(drawable)) { 158 // Make each drawable unique in the overlay; can't add it more than once 159 mDrawables.add(drawable); 160 invalidate(drawable.getBounds()); 161 drawable.setCallback(this); 162 } 163 } 164 remove(@onNull Drawable drawable)165 public void remove(@NonNull Drawable drawable) { 166 if (drawable == null) { 167 throw new IllegalArgumentException("drawable must be non-null"); 168 } 169 if (mDrawables != null) { 170 mDrawables.remove(drawable); 171 invalidate(drawable.getBounds()); 172 drawable.setCallback(null); 173 } 174 } 175 176 @Override verifyDrawable(@onNull Drawable who)177 protected boolean verifyDrawable(@NonNull Drawable who) { 178 return super.verifyDrawable(who) || (mDrawables != null && mDrawables.contains(who)); 179 } 180 add(@onNull View child)181 public void add(@NonNull View child) { 182 if (child == null) { 183 throw new IllegalArgumentException("view must be non-null"); 184 } 185 186 if (child.getParent() instanceof ViewGroup) { 187 ViewGroup parent = (ViewGroup) child.getParent(); 188 if (parent != mHostView && parent.getParent() != null && 189 parent.mAttachInfo != null) { 190 // Moving to different container; figure out how to position child such that 191 // it is in the same location on the screen 192 int[] parentLocation = new int[2]; 193 int[] hostViewLocation = new int[2]; 194 parent.getLocationOnScreen(parentLocation); 195 mHostView.getLocationOnScreen(hostViewLocation); 196 child.offsetLeftAndRight(parentLocation[0] - hostViewLocation[0]); 197 child.offsetTopAndBottom(parentLocation[1] - hostViewLocation[1]); 198 } 199 parent.removeView(child); 200 if (parent.getLayoutTransition() != null) { 201 // LayoutTransition will cause the child to delay removal - cancel it 202 parent.getLayoutTransition().cancel(LayoutTransition.DISAPPEARING); 203 } 204 // fail-safe if view is still attached for any reason 205 if (child.getParent() != null) { 206 child.mParent = null; 207 } 208 } 209 super.addView(child); 210 } 211 remove(@onNull View view)212 public void remove(@NonNull View view) { 213 if (view == null) { 214 throw new IllegalArgumentException("view must be non-null"); 215 } 216 217 super.removeView(view); 218 } 219 clear()220 public void clear() { 221 removeAllViews(); 222 if (mDrawables != null) { 223 for (Drawable drawable : mDrawables) { 224 drawable.setCallback(null); 225 } 226 mDrawables.clear(); 227 } 228 } 229 isEmpty()230 boolean isEmpty() { 231 if (getChildCount() == 0 && 232 (mDrawables == null || mDrawables.size() == 0)) { 233 return true; 234 } 235 return false; 236 } 237 238 @Override invalidateDrawable(@onNull Drawable drawable)239 public void invalidateDrawable(@NonNull Drawable drawable) { 240 invalidate(drawable.getBounds()); 241 } 242 243 @Override dispatchDraw(Canvas canvas)244 protected void dispatchDraw(Canvas canvas) { 245 /* 246 * The OverlayViewGroup doesn't draw with a DisplayList, because 247 * draw(Canvas, View, long) is never called on it. This is fine, since it doesn't need 248 * RenderNode/DisplayList features, and can just draw into the owner's Canvas. 249 * 250 * This means that we need to insert reorder barriers manually though, so that children 251 * of the OverlayViewGroup can cast shadows and Z reorder with each other. 252 */ 253 canvas.enableZ(); 254 255 super.dispatchDraw(canvas); 256 257 canvas.disableZ(); 258 final int numDrawables = (mDrawables == null) ? 0 : mDrawables.size(); 259 for (int i = 0; i < numDrawables; ++i) { 260 mDrawables.get(i).draw(canvas); 261 } 262 } 263 264 @Override onLayout(boolean changed, int l, int t, int r, int b)265 protected void onLayout(boolean changed, int l, int t, int r, int b) { 266 // Noop: children are positioned absolutely 267 } 268 269 /* 270 The following invalidation overrides exist for the purpose of redirecting invalidation to 271 the host view. The overlay is not parented to the host view (since a View cannot be a 272 parent), so the invalidation cannot proceed through the normal parent hierarchy. 273 There is a built-in assumption that the overlay exactly covers the host view, therefore 274 the invalidation rectangles received do not need to be adjusted when forwarded to 275 the host view. 276 */ 277 278 @Override invalidate(Rect dirty)279 public void invalidate(Rect dirty) { 280 super.invalidate(dirty); 281 if (mHostView != null) { 282 mHostView.invalidate(dirty); 283 } 284 } 285 286 @Override invalidate(int l, int t, int r, int b)287 public void invalidate(int l, int t, int r, int b) { 288 super.invalidate(l, t, r, b); 289 if (mHostView != null) { 290 mHostView.invalidate(l, t, r, b); 291 } 292 } 293 294 @Override invalidate()295 public void invalidate() { 296 super.invalidate(); 297 if (mHostView != null) { 298 mHostView.invalidate(); 299 } 300 } 301 302 /** @hide */ 303 @Override invalidate(boolean invalidateCache)304 public void invalidate(boolean invalidateCache) { 305 super.invalidate(invalidateCache); 306 if (mHostView != null) { 307 mHostView.invalidate(invalidateCache); 308 } 309 } 310 311 @Override invalidateViewProperty(boolean invalidateParent, boolean forceRedraw)312 void invalidateViewProperty(boolean invalidateParent, boolean forceRedraw) { 313 super.invalidateViewProperty(invalidateParent, forceRedraw); 314 if (mHostView != null) { 315 mHostView.invalidateViewProperty(invalidateParent, forceRedraw); 316 } 317 } 318 319 @Override invalidateParentCaches()320 protected void invalidateParentCaches() { 321 super.invalidateParentCaches(); 322 if (mHostView != null) { 323 mHostView.invalidateParentCaches(); 324 } 325 } 326 327 @Override invalidateParentIfNeeded()328 protected void invalidateParentIfNeeded() { 329 super.invalidateParentIfNeeded(); 330 if (mHostView != null) { 331 mHostView.invalidateParentIfNeeded(); 332 } 333 } 334 335 @Override onDescendantInvalidated(@onNull View child, @NonNull View target)336 public void onDescendantInvalidated(@NonNull View child, @NonNull View target) { 337 if (mHostView != null) { 338 if (mHostView instanceof ViewGroup) { 339 // Propagate invalidate through the host... 340 ((ViewGroup) mHostView).onDescendantInvalidated(mHostView, target); 341 342 // ...and also this view, since it will hold the descendant, and must later 343 // propagate the calls to update display lists if dirty 344 super.onDescendantInvalidated(child, target); 345 } else { 346 // Can't use onDescendantInvalidated because host isn't a ViewGroup - fall back 347 // to invalidating. 348 invalidate(); 349 } 350 } 351 } 352 353 @Override invalidateChildInParent(int[] location, Rect dirty)354 public ViewParent invalidateChildInParent(int[] location, Rect dirty) { 355 if (mHostView != null) { 356 dirty.offset(location[0], location[1]); 357 if (mHostView instanceof ViewGroup) { 358 location[0] = 0; 359 location[1] = 0; 360 super.invalidateChildInParent(location, dirty); 361 return ((ViewGroup) mHostView).invalidateChildInParent(location, dirty); 362 } else { 363 invalidate(dirty); 364 } 365 } 366 return null; 367 } 368 } 369 370 } 371