1/*
2 * Copyright (C) 2017 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 androidx.transition;
18
19import android.annotation.SuppressLint;
20import android.graphics.Canvas;
21import android.graphics.Matrix;
22import android.view.View;
23import android.view.ViewGroup;
24import android.view.ViewParent;
25import android.view.ViewTreeObserver;
26import android.widget.FrameLayout;
27
28import androidx.annotation.NonNull;
29import androidx.core.view.ViewCompat;
30
31/**
32 * Backport of android.view.GhostView introduced in API level 21.
33 * <p>
34 * While the platform version uses ViewOverlay, this ghost view finds the closest FrameLayout in
35 * the hierarchy and adds itself there.
36 * <p>
37 * Since we cannot use RenderNode to delegate drawing, we instead use {@link View#draw(Canvas)} to
38 * draw the target view. We apply the same transformation matrix applied to the target view. For
39 * that, this view is sized as large as the parent FrameLayout (except padding) while the platform
40 * version becomes as large as the target view.
41 */
42@SuppressLint("ViewConstructor")
43class GhostViewApi14 extends View implements GhostViewImpl {
44
45    static GhostViewImpl addGhost(View view, ViewGroup viewGroup) {
46        GhostViewApi14 ghostView = getGhostView(view);
47        if (ghostView == null) {
48            FrameLayout frameLayout = findFrameLayout(viewGroup);
49            if (frameLayout == null) {
50                return null;
51            }
52            ghostView = new GhostViewApi14(view);
53            frameLayout.addView(ghostView);
54        }
55        ghostView.mReferences++;
56        return ghostView;
57    }
58
59    static void removeGhost(View view) {
60        GhostViewApi14 ghostView = getGhostView(view);
61        if (ghostView != null) {
62            ghostView.mReferences--;
63            if (ghostView.mReferences <= 0) {
64                ViewParent parent = ghostView.getParent();
65                if (parent instanceof ViewGroup) {
66                    ViewGroup group = (ViewGroup) parent;
67                    group.endViewTransition(ghostView);
68                    group.removeView(ghostView);
69                }
70            }
71        }
72    }
73
74    /**
75     * Find the closest FrameLayout in the ascendant hierarchy from the specified {@code
76     * viewGroup}.
77     */
78    private static FrameLayout findFrameLayout(ViewGroup viewGroup) {
79        while (!(viewGroup instanceof FrameLayout)) {
80            ViewParent parent = viewGroup.getParent();
81            if (!(parent instanceof ViewGroup)) {
82                return null;
83            }
84            viewGroup = (ViewGroup) parent;
85        }
86        return (FrameLayout) viewGroup;
87    }
88
89    /** The target view */
90    final View mView;
91
92    /** The parent of the view that is disappearing at the beginning of the animation */
93    ViewGroup mStartParent;
94
95    /** The view that is disappearing at the beginning of the animation */
96    View mStartView;
97
98    /** The number of references to this ghost view */
99    int mReferences;
100
101    /** The horizontal distance from the ghost view to the target view */
102    private int mDeltaX;
103
104    /** The horizontal distance from the ghost view to the target view */
105    private int mDeltaY;
106
107    /** The current transformation matrix of the target view */
108    Matrix mCurrentMatrix;
109
110    /** The matrix applied to the ghost view canvas */
111    private final Matrix mMatrix = new Matrix();
112
113    private final ViewTreeObserver.OnPreDrawListener mOnPreDrawListener =
114            new ViewTreeObserver.OnPreDrawListener() {
115                @Override
116                public boolean onPreDraw() {
117                    // The target view was invalidated; get the transformation.
118                    mCurrentMatrix = mView.getMatrix();
119                    // We draw the view.
120                    ViewCompat.postInvalidateOnAnimation(GhostViewApi14.this);
121                    if (mStartParent != null && mStartView != null) {
122                        mStartParent.endViewTransition(mStartView);
123                        ViewCompat.postInvalidateOnAnimation(mStartParent);
124                        mStartParent = null;
125                        mStartView = null;
126                    }
127                    return true;
128                }
129            };
130
131    GhostViewApi14(View view) {
132        super(view.getContext());
133        mView = view;
134        setLayerType(LAYER_TYPE_HARDWARE, null);
135    }
136
137    @Override
138    protected void onAttachedToWindow() {
139        super.onAttachedToWindow();
140        setGhostView(mView, this);
141        // Calculate the deltas
142        final int[] location = new int[2];
143        final int[] viewLocation = new int[2];
144        getLocationOnScreen(location);
145        mView.getLocationOnScreen(viewLocation);
146        viewLocation[0] = (int) (viewLocation[0] - mView.getTranslationX());
147        viewLocation[1] = (int) (viewLocation[1] - mView.getTranslationY());
148        mDeltaX = viewLocation[0] - location[0];
149        mDeltaY = viewLocation[1] - location[1];
150        // Monitor invalidation of the target view.
151        mView.getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener);
152        // Make the target view invisible because we draw it instead.
153        mView.setVisibility(INVISIBLE);
154    }
155
156    @Override
157    protected void onDetachedFromWindow() {
158        mView.getViewTreeObserver().removeOnPreDrawListener(mOnPreDrawListener);
159        mView.setVisibility(VISIBLE);
160        setGhostView(mView, null);
161        super.onDetachedFromWindow();
162    }
163
164    @Override
165    protected void onDraw(Canvas canvas) {
166        // Apply the matrix while adjusting the coordinates
167        mMatrix.set(mCurrentMatrix);
168        mMatrix.postTranslate(mDeltaX, mDeltaY);
169        canvas.setMatrix(mMatrix);
170        // Draw the target
171        mView.draw(canvas);
172    }
173
174    @Override
175    public void setVisibility(int visibility) {
176        super.setVisibility(visibility);
177        mView.setVisibility(visibility == VISIBLE ? INVISIBLE : VISIBLE);
178    }
179
180    @Override
181    public void reserveEndViewTransition(ViewGroup viewGroup, View view) {
182        mStartParent = viewGroup;
183        mStartView = view;
184    }
185
186    private static void setGhostView(@NonNull View view, GhostViewApi14 ghostView) {
187        view.setTag(R.id.ghost_view, ghostView);
188    }
189
190    static GhostViewApi14 getGhostView(@NonNull View view) {
191        return (GhostViewApi14) view.getTag(R.id.ghost_view);
192    }
193
194}
195