summaryrefslogtreecommitdiff
path: root/android/view/ViewOverlay.java
blob: 21123c167792bd3a4e8ff7ffe610d29fb5456ded (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package android.view;

import android.animation.LayoutTransition;
import android.annotation.NonNull;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;

import java.util.ArrayList;

/**
 * An overlay is an extra layer that sits on top of a View (the "host view")
 * which is drawn after all other content in that view (including children,
 * if the view is a ViewGroup). Interaction with the overlay layer is done
 * by adding and removing drawables.
 *
 * <p>An overlay requested from a ViewGroup is of type {@link ViewGroupOverlay},
 * which also supports adding and removing views.</p>
 *
 * @see View#getOverlay() View.getOverlay()
 * @see ViewGroup#getOverlay() ViewGroup.getOverlay()
 * @see ViewGroupOverlay
 */
public class ViewOverlay {

    /**
     * The actual container for the drawables (and views, if it's a ViewGroupOverlay).
     * All of the management and rendering details for the overlay are handled in
     * OverlayViewGroup.
     */
    OverlayViewGroup mOverlayViewGroup;

    ViewOverlay(Context context, View hostView) {
        mOverlayViewGroup = new OverlayViewGroup(context, hostView);
    }

    /**
     * Used internally by View and ViewGroup to handle drawing and invalidation
     * of the overlay
     * @return
     */
    ViewGroup getOverlayView() {
        return mOverlayViewGroup;
    }

    /**
     * Adds a {@link Drawable} to the overlay. The bounds of the drawable should be relative to
     * the host view. Any drawable added to the overlay should be removed when it is no longer
     * needed or no longer visible. Adding an already existing {@link Drawable}
     * is a no-op. Passing <code>null</code> parameter will result in an
     * {@link IllegalArgumentException} being thrown.
     *
     * @param drawable The {@link Drawable} to be added to the overlay. This drawable will be
     * drawn when the view redraws its overlay. {@link Drawable}s will be drawn in the order that
     * they were added.
     * @see #remove(Drawable)
     */
    public void add(@NonNull Drawable drawable) {
        mOverlayViewGroup.add(drawable);
    }

    /**
     * Removes the specified {@link Drawable} from the overlay. Removing a {@link Drawable} that was
     * not added with {@link #add(Drawable)} is a no-op. Passing <code>null</code> parameter will
     * result in an {@link IllegalArgumentException} being thrown.
     *
     * @param drawable The {@link Drawable} to be removed from the overlay.
     * @see #add(Drawable)
     */
    public void remove(@NonNull Drawable drawable) {
        mOverlayViewGroup.remove(drawable);
    }

    /**
     * Removes all content from the overlay.
     */
    public void clear() {
        mOverlayViewGroup.clear();
    }

    boolean isEmpty() {
        return mOverlayViewGroup.isEmpty();
    }

    /**
     * OverlayViewGroup is a container that View and ViewGroup use to host
     * drawables and views added to their overlays  ({@link ViewOverlay} and
     * {@link ViewGroupOverlay}, respectively). Drawables are added to the overlay
     * via the add/remove methods in ViewOverlay, Views are added/removed via
     * ViewGroupOverlay. These drawable and view objects are
     * drawn whenever the view itself is drawn; first the view draws its own
     * content (and children, if it is a ViewGroup), then it draws its overlay
     * (if it has one).
     *
     * <p>Besides managing and drawing the list of drawables, this class serves
     * two purposes:
     * (1) it noops layout calls because children are absolutely positioned and
     * (2) it forwards all invalidation calls to its host view. The invalidation
     * redirect is necessary because the overlay is not a child of the host view
     * and invalidation cannot therefore follow the normal path up through the
     * parent hierarchy.</p>
     *
     * @see View#getOverlay()
     * @see ViewGroup#getOverlay()
     */
    static class OverlayViewGroup extends ViewGroup {

        /**
         * The View for which this is an overlay. Invalidations of the overlay are redirected to
         * this host view.
         */
        final View mHostView;

        /**
         * The set of drawables to draw when the overlay is rendered.
         */
        ArrayList<Drawable> mDrawables = null;

        OverlayViewGroup(Context context, View hostView) {
            super(context);
            mHostView = hostView;
            mAttachInfo = mHostView.mAttachInfo;

            mRight = hostView.getWidth();
            mBottom = hostView.getHeight();
            // pass right+bottom directly to RenderNode, since not going through setters
            mRenderNode.setLeftTopRightBottom(0, 0, mRight, mBottom);
        }

        public void add(@NonNull Drawable drawable) {
            if (drawable == null) {
                throw new IllegalArgumentException("drawable must be non-null");
            }
            if (mDrawables == null) {
                mDrawables = new ArrayList<>();
            }
            if (!mDrawables.contains(drawable)) {
                // Make each drawable unique in the overlay; can't add it more than once
                mDrawables.add(drawable);
                invalidate(drawable.getBounds());
                drawable.setCallback(this);
            }
        }

        public void remove(@NonNull Drawable drawable) {
            if (drawable == null) {
                throw new IllegalArgumentException("drawable must be non-null");
            }
            if (mDrawables != null) {
                mDrawables.remove(drawable);
                invalidate(drawable.getBounds());
                drawable.setCallback(null);
            }
        }

        @Override
        protected boolean verifyDrawable(@NonNull Drawable who) {
            return super.verifyDrawable(who) || (mDrawables != null && mDrawables.contains(who));
        }

        public void add(@NonNull View child) {
            if (child == null) {
                throw new IllegalArgumentException("view must be non-null");
            }

            if (child.getParent() instanceof ViewGroup) {
                ViewGroup parent = (ViewGroup) child.getParent();
                if (parent != mHostView && parent.getParent() != null &&
                        parent.mAttachInfo != null) {
                    // Moving to different container; figure out how to position child such that
                    // it is in the same location on the screen
                    int[] parentLocation = new int[2];
                    int[] hostViewLocation = new int[2];
                    parent.getLocationOnScreen(parentLocation);
                    mHostView.getLocationOnScreen(hostViewLocation);
                    child.offsetLeftAndRight(parentLocation[0] - hostViewLocation[0]);
                    child.offsetTopAndBottom(parentLocation[1] - hostViewLocation[1]);
                }
                parent.removeView(child);
                if (parent.getLayoutTransition() != null) {
                    // LayoutTransition will cause the child to delay removal - cancel it
                    parent.getLayoutTransition().cancel(LayoutTransition.DISAPPEARING);
                }
                // fail-safe if view is still attached for any reason
                if (child.getParent() != null) {
                    child.mParent = null;
                }
            }
            super.addView(child);
        }

        public void remove(@NonNull View view) {
            if (view == null) {
                throw new IllegalArgumentException("view must be non-null");
            }

            super.removeView(view);
        }

        public void clear() {
            removeAllViews();
            if (mDrawables != null) {
                for (Drawable drawable : mDrawables) {
                    drawable.setCallback(null);
                }
                mDrawables.clear();
            }
        }

        boolean isEmpty() {
            if (getChildCount() == 0 &&
                    (mDrawables == null || mDrawables.size() == 0)) {
                return true;
            }
            return false;
        }

        @Override
        public void invalidateDrawable(@NonNull Drawable drawable) {
            invalidate(drawable.getBounds());
        }

        @Override
        protected void dispatchDraw(Canvas canvas) {
            /*
             * The OverlayViewGroup doesn't draw with a DisplayList, because
             * draw(Canvas, View, long) is never called on it. This is fine, since it doesn't need
             * RenderNode/DisplayList features, and can just draw into the owner's Canvas.
             *
             * This means that we need to insert reorder barriers manually though, so that children
             * of the OverlayViewGroup can cast shadows and Z reorder with each other.
             */
            canvas.insertReorderBarrier();

            super.dispatchDraw(canvas);

            canvas.insertInorderBarrier();
            final int numDrawables = (mDrawables == null) ? 0 : mDrawables.size();
            for (int i = 0; i < numDrawables; ++i) {
                mDrawables.get(i).draw(canvas);
            }
        }

        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            // Noop: children are positioned absolutely
        }

        /*
         The following invalidation overrides exist for the purpose of redirecting invalidation to
         the host view. The overlay is not parented to the host view (since a View cannot be a
         parent), so the invalidation cannot proceed through the normal parent hierarchy.
         There is a built-in assumption that the overlay exactly covers the host view, therefore
         the invalidation rectangles received do not need to be adjusted when forwarded to
         the host view.
         */

        @Override
        public void invalidate(Rect dirty) {
            super.invalidate(dirty);
            if (mHostView != null) {
                mHostView.invalidate(dirty);
            }
        }

        @Override
        public void invalidate(int l, int t, int r, int b) {
            super.invalidate(l, t, r, b);
            if (mHostView != null) {
                mHostView.invalidate(l, t, r, b);
            }
        }

        @Override
        public void invalidate() {
            super.invalidate();
            if (mHostView != null) {
                mHostView.invalidate();
            }
        }

        /** @hide */
        @Override
        public void invalidate(boolean invalidateCache) {
            super.invalidate(invalidateCache);
            if (mHostView != null) {
                mHostView.invalidate(invalidateCache);
            }
        }

        @Override
        void invalidateViewProperty(boolean invalidateParent, boolean forceRedraw) {
            super.invalidateViewProperty(invalidateParent, forceRedraw);
            if (mHostView != null) {
                mHostView.invalidateViewProperty(invalidateParent, forceRedraw);
            }
        }

        @Override
        protected void invalidateParentCaches() {
            super.invalidateParentCaches();
            if (mHostView != null) {
                mHostView.invalidateParentCaches();
            }
        }

        @Override
        protected void invalidateParentIfNeeded() {
            super.invalidateParentIfNeeded();
            if (mHostView != null) {
                mHostView.invalidateParentIfNeeded();
            }
        }

        @Override
        public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
            if (mHostView != null) {
                if (mHostView instanceof ViewGroup) {
                    // Propagate invalidate through the host...
                    ((ViewGroup) mHostView).onDescendantInvalidated(mHostView, target);

                    // ...and also this view, since it will hold the descendant, and must later
                    // propagate the calls to update display lists if dirty
                    super.onDescendantInvalidated(child, target);
                } else {
                    // Can't use onDescendantInvalidated because host isn't a ViewGroup - fall back
                    // to invalidating.
                    invalidate();
                }
            }
        }

        @Override
        public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
            if (mHostView != null) {
                dirty.offset(location[0], location[1]);
                if (mHostView instanceof ViewGroup) {
                    location[0] = 0;
                    location[1] = 0;
                    super.invalidateChildInParent(location, dirty);
                    return ((ViewGroup) mHostView).invalidateChildInParent(location, dirty);
                } else {
                    invalidate(dirty);
                }
            }
            return null;
        }
    }

}