GhostViewApi14.java

/*
 * Copyright (C) 2017 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 androidx.transition;

import android.annotation.SuppressLint;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;

/**
 * Backport of android.view.GhostView introduced in API level 21.
 * <p>
 * While the platform version uses ViewOverlay, this ghost view finds the closest FrameLayout in
 * the hierarchy and adds itself there.
 * <p>
 * Since we cannot use RenderNode to delegate drawing, we instead use {@link View#draw(Canvas)} to
 * draw the target view. We apply the same transformation matrix applied to the target view. For
 * that, this view is sized as large as the parent FrameLayout (except padding) while the platform
 * version becomes as large as the target view.
 */
@SuppressLint("ViewConstructor")
class GhostViewApi14 extends View implements GhostViewImpl {

    static GhostViewImpl addGhost(View view, ViewGroup viewGroup) {
        GhostViewApi14 ghostView = getGhostView(view);
        if (ghostView == null) {
            FrameLayout frameLayout = findFrameLayout(viewGroup);
            if (frameLayout == null) {
                return null;
            }
            ghostView = new GhostViewApi14(view);
            frameLayout.addView(ghostView);
        }
        ghostView.mReferences++;
        return ghostView;
    }

    static void removeGhost(View view) {
        GhostViewApi14 ghostView = getGhostView(view);
        if (ghostView != null) {
            ghostView.mReferences--;
            if (ghostView.mReferences <= 0) {
                ViewParent parent = ghostView.getParent();
                if (parent instanceof ViewGroup) {
                    ViewGroup group = (ViewGroup) parent;
                    group.endViewTransition(ghostView);
                    group.removeView(ghostView);
                }
            }
        }
    }

    /**
     * Find the closest FrameLayout in the ascendant hierarchy from the specified {@code
     * viewGroup}.
     */
    private static FrameLayout findFrameLayout(ViewGroup viewGroup) {
        while (!(viewGroup instanceof FrameLayout)) {
            ViewParent parent = viewGroup.getParent();
            if (!(parent instanceof ViewGroup)) {
                return null;
            }
            viewGroup = (ViewGroup) parent;
        }
        return (FrameLayout) viewGroup;
    }

    /** The target view */
    final View mView;

    /** The parent of the view that is disappearing at the beginning of the animation */
    ViewGroup mStartParent;

    /** The view that is disappearing at the beginning of the animation */
    View mStartView;

    /** The number of references to this ghost view */
    int mReferences;

    /** The horizontal distance from the ghost view to the target view */
    private int mDeltaX;

    /** The horizontal distance from the ghost view to the target view */
    private int mDeltaY;

    /** The current transformation matrix of the target view */
    Matrix mCurrentMatrix;

    /** The matrix applied to the ghost view canvas */
    private final Matrix mMatrix = new Matrix();

    private final ViewTreeObserver.OnPreDrawListener mOnPreDrawListener =
            new ViewTreeObserver.OnPreDrawListener() {
                @Override
                public boolean onPreDraw() {
                    // The target view was invalidated; get the transformation.
                    mCurrentMatrix = mView.getMatrix();
                    // We draw the view.
                    ViewCompat.postInvalidateOnAnimation(GhostViewApi14.this);
                    if (mStartParent != null && mStartView != null) {
                        mStartParent.endViewTransition(mStartView);
                        ViewCompat.postInvalidateOnAnimation(mStartParent);
                        mStartParent = null;
                        mStartView = null;
                    }
                    return true;
                }
            };

    GhostViewApi14(View view) {
        super(view.getContext());
        mView = view;
        setLayerType(LAYER_TYPE_HARDWARE, null);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        setGhostView(mView, this);
        // Calculate the deltas
        final int[] location = new int[2];
        final int[] viewLocation = new int[2];
        getLocationOnScreen(location);
        mView.getLocationOnScreen(viewLocation);
        viewLocation[0] = (int) (viewLocation[0] - mView.getTranslationX());
        viewLocation[1] = (int) (viewLocation[1] - mView.getTranslationY());
        mDeltaX = viewLocation[0] - location[0];
        mDeltaY = viewLocation[1] - location[1];
        // Monitor invalidation of the target view.
        mView.getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener);
        // Make the target view invisible because we draw it instead.
        mView.setVisibility(INVISIBLE);
    }

    @Override
    protected void onDetachedFromWindow() {
        mView.getViewTreeObserver().removeOnPreDrawListener(mOnPreDrawListener);
        mView.setVisibility(VISIBLE);
        setGhostView(mView, null);
        super.onDetachedFromWindow();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // Apply the matrix while adjusting the coordinates
        mMatrix.set(mCurrentMatrix);
        mMatrix.postTranslate(mDeltaX, mDeltaY);
        canvas.setMatrix(mMatrix);
        // Draw the target
        mView.draw(canvas);
    }

    @Override
    public void setVisibility(int visibility) {
        super.setVisibility(visibility);
        mView.setVisibility(visibility == VISIBLE ? INVISIBLE : VISIBLE);
    }

    @Override
    public void reserveEndViewTransition(ViewGroup viewGroup, View view) {
        mStartParent = viewGroup;
        mStartView = view;
    }

    private static void setGhostView(@NonNull View view, GhostViewApi14 ghostView) {
        view.setTag(R.id.ghost_view, ghostView);
    }

    static GhostViewApi14 getGhostView(@NonNull View view) {
        return (GhostViewApi14) view.getTag(R.id.ghost_view);
    }

}