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