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