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