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