GhostViewPort.java
/*
* Copyright 2019 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.ViewTreeObserver;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
/**
* Backport of android.view.GhostView introduced in API level 21.
*/
@SuppressLint("ViewConstructor")
class GhostViewPort extends ViewGroup implements GhostView {
/** 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 target view */
final View mView;
/** The number of references to this ghost view */
int mReferences;
@Nullable
private Matrix mMatrix;
private final ViewTreeObserver.OnPreDrawListener mOnPreDrawListener =
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
// We draw the view.
ViewCompat.postInvalidateOnAnimation(GhostViewPort.this);
if (mStartParent != null && mStartView != null) {
mStartParent.endViewTransition(mStartView);
ViewCompat.postInvalidateOnAnimation(mStartParent);
mStartParent = null;
mStartView = null;
}
return true;
}
};
GhostViewPort(View view) {
super(view.getContext());
mView = view;
setWillNotDraw(false);
setLayerType(LAYER_TYPE_HARDWARE, null);
}
@Override
public void setVisibility(int visibility) {
super.setVisibility(visibility);
if (getGhostView(mView) == this) {
int inverseVisibility = (visibility == View.VISIBLE) ? View.INVISIBLE : View.VISIBLE;
ViewUtils.setTransitionVisibility(mView, inverseVisibility);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
void setMatrix(@NonNull Matrix matrix) {
mMatrix = matrix;
}
@Override
public void reserveEndViewTransition(ViewGroup viewGroup, View view) {
mStartParent = viewGroup;
mStartView = view;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
setGhostView(mView, this);
// Monitor invalidation of the target view.
mView.getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener);
// Make the target view invisible because we draw it instead.
ViewUtils.setTransitionVisibility(mView, INVISIBLE);
if (mView.getParent() != null) {
((View) mView.getParent()).invalidate();
}
}
@Override
protected void onDetachedFromWindow() {
mView.getViewTreeObserver().removeOnPreDrawListener(mOnPreDrawListener);
ViewUtils.setTransitionVisibility(mView, VISIBLE);
setGhostView(mView, null);
if (mView.getParent() != null) {
((View) mView.getParent()).invalidate();
}
super.onDetachedFromWindow();
}
@Override
protected void onDraw(Canvas canvas) {
CanvasUtils.enableZ(canvas, true); // enable shadows
canvas.setMatrix(mMatrix);
// We need to mark mView as invalidated so drawChild() will recreate a RenderNode.
// As invalidate() will do nothing while mView is INVISIBLE we need to silently
// change it to VISIBLE, call invalidate and then change it back to INVISIBLE.
ViewUtils.setTransitionVisibility(mView, VISIBLE);
mView.invalidate();
ViewUtils.setTransitionVisibility(mView, INVISIBLE);
drawChild(canvas, mView, getDrawingTime());
CanvasUtils.enableZ(canvas, false); // re-disable reordering/shadows
}
static void copySize(View from, View to) {
ViewUtils.setLeftTopRightBottom(to,
to.getLeft(),
to.getTop(),
to.getLeft() + from.getWidth(),
to.getTop() + from.getHeight());
}
static GhostViewPort getGhostView(View view) {
return (GhostViewPort) view.getTag(R.id.ghost_view);
}
static void setGhostView(@NonNull View view, @Nullable GhostViewPort ghostView) {
view.setTag(R.id.ghost_view, ghostView);
}
static void calculateMatrix(View view, ViewGroup host, Matrix matrix) {
ViewGroup parent = (ViewGroup) view.getParent();
matrix.reset();
ViewUtils.transformMatrixToGlobal(parent, matrix);
matrix.preTranslate(-parent.getScrollX(), -parent.getScrollY());
ViewUtils.transformMatrixToLocal(host, matrix);
}
static GhostViewPort addGhost(View view, ViewGroup viewGroup, Matrix matrix) {
if (!(view.getParent() instanceof ViewGroup)) {
throw new IllegalArgumentException("Ghosted views must be parented by a ViewGroup");
}
GhostViewHolder holder = GhostViewHolder.getHolder(viewGroup);
GhostViewPort ghostView = getGhostView(view);
int previousRefCount = 0;
if (ghostView != null) {
GhostViewHolder oldHolder = (GhostViewHolder) ghostView.getParent();
if (oldHolder != holder) {
previousRefCount = ghostView.mReferences;
oldHolder.removeView(ghostView);
ghostView = null;
}
}
if (ghostView == null) {
if (matrix == null) {
matrix = new Matrix();
calculateMatrix(view, viewGroup, matrix);
}
ghostView = new GhostViewPort(view);
ghostView.setMatrix(matrix);
if (holder == null) {
holder = new GhostViewHolder(viewGroup);
} else {
holder.popToOverlayTop();
}
copySize(viewGroup, holder);
copySize(viewGroup, ghostView);
holder.addGhostView(ghostView);
ghostView.mReferences = previousRefCount;
} else if (matrix != null) {
ghostView.setMatrix(matrix);
}
ghostView.mReferences++;
return ghostView;
}
static void removeGhost(View view) {
GhostViewPort ghostView = getGhostView(view);
if (ghostView != null) {
ghostView.mReferences--;
if (ghostView.mReferences <= 0) {
GhostViewHolder holder = (GhostViewHolder) ghostView.getParent();
holder.removeView(ghostView);
}
}
}
}