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