352 lines
13 KiB
Java
352 lines
13 KiB
Java
![]() |
/*
|
||
|
* Copyright (C) 2014 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 android.view;
|
||
|
|
||
|
import android.compat.annotation.UnsupportedAppUsage;
|
||
|
import android.graphics.Canvas;
|
||
|
import android.graphics.Matrix;
|
||
|
import android.graphics.RecordingCanvas;
|
||
|
import android.graphics.RenderNode;
|
||
|
import android.os.Build;
|
||
|
import android.widget.FrameLayout;
|
||
|
|
||
|
import java.util.ArrayList;
|
||
|
|
||
|
/**
|
||
|
* This view draws another View in an Overlay without changing the parent. It will not be drawn
|
||
|
* by its parent because its visibility is set to INVISIBLE, but will be drawn
|
||
|
* here using its render node. When the GhostView is set to INVISIBLE, the View it is
|
||
|
* shadowing will become VISIBLE and when the GhostView becomes VISIBLE, the shadowed
|
||
|
* view becomes INVISIBLE.
|
||
|
* @hide
|
||
|
*/
|
||
|
public class GhostView extends View {
|
||
|
private final View mView;
|
||
|
private int mReferences;
|
||
|
private boolean mBeingMoved;
|
||
|
|
||
|
private GhostView(View view) {
|
||
|
super(view.getContext());
|
||
|
mView = view;
|
||
|
mView.mGhostView = this;
|
||
|
final ViewGroup parent = (ViewGroup) mView.getParent();
|
||
|
mView.setTransitionVisibility(View.INVISIBLE);
|
||
|
parent.invalidate();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void onDraw(Canvas canvas) {
|
||
|
if (canvas instanceof RecordingCanvas) {
|
||
|
RecordingCanvas dlCanvas = (RecordingCanvas) canvas;
|
||
|
mView.mRecreateDisplayList = true;
|
||
|
RenderNode renderNode = mView.updateDisplayListIfDirty();
|
||
|
if (renderNode.hasDisplayList()) {
|
||
|
dlCanvas.enableZ(); // enable shadow for this rendernode
|
||
|
dlCanvas.drawRenderNode(renderNode);
|
||
|
dlCanvas.disableZ(); // re-disable reordering/shadows
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void setMatrix(Matrix matrix) {
|
||
|
mRenderNode.setAnimationMatrix(matrix);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setVisibility(@Visibility int visibility) {
|
||
|
super.setVisibility(visibility);
|
||
|
if (mView.mGhostView == this) {
|
||
|
int inverseVisibility = (visibility == View.VISIBLE) ? View.INVISIBLE : View.VISIBLE;
|
||
|
mView.setTransitionVisibility(inverseVisibility);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void onDetachedFromWindow() {
|
||
|
super.onDetachedFromWindow();
|
||
|
if (!mBeingMoved) {
|
||
|
mView.setTransitionVisibility(View.VISIBLE);
|
||
|
mView.mGhostView = null;
|
||
|
final ViewGroup parent = (ViewGroup) mView.getParent();
|
||
|
if (parent != null) {
|
||
|
parent.invalidate();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static void calculateMatrix(View view, ViewGroup host, Matrix matrix) {
|
||
|
ViewGroup parent = (ViewGroup) view.getParent();
|
||
|
matrix.reset();
|
||
|
parent.transformMatrixToGlobal(matrix);
|
||
|
matrix.preTranslate(-parent.getScrollX(), -parent.getScrollY());
|
||
|
host.transformMatrixToLocal(matrix);
|
||
|
}
|
||
|
|
||
|
@UnsupportedAppUsage
|
||
|
public static GhostView addGhost(View view, ViewGroup viewGroup, Matrix matrix) {
|
||
|
if (!(view.getParent() instanceof ViewGroup)) {
|
||
|
throw new IllegalArgumentException("Ghosted views must be parented by a ViewGroup");
|
||
|
}
|
||
|
ViewGroupOverlay overlay = viewGroup.getOverlay();
|
||
|
ViewOverlay.OverlayViewGroup overlayViewGroup = overlay.mOverlayViewGroup;
|
||
|
GhostView ghostView = view.mGhostView;
|
||
|
int previousRefCount = 0;
|
||
|
if (ghostView != null) {
|
||
|
View oldParent = (View) ghostView.getParent();
|
||
|
ViewGroup oldGrandParent = (ViewGroup) oldParent.getParent();
|
||
|
if (oldGrandParent != overlayViewGroup) {
|
||
|
previousRefCount = ghostView.mReferences;
|
||
|
oldGrandParent.removeView(oldParent);
|
||
|
ghostView = null;
|
||
|
}
|
||
|
}
|
||
|
if (ghostView == null) {
|
||
|
if (matrix == null) {
|
||
|
matrix = new Matrix();
|
||
|
calculateMatrix(view, viewGroup, matrix);
|
||
|
}
|
||
|
ghostView = new GhostView(view);
|
||
|
ghostView.setMatrix(matrix);
|
||
|
FrameLayout parent = new FrameLayout(view.getContext());
|
||
|
parent.setClipChildren(false);
|
||
|
copySize(viewGroup, parent);
|
||
|
copySize(viewGroup, ghostView);
|
||
|
parent.addView(ghostView);
|
||
|
ArrayList<View> tempViews = new ArrayList<View>();
|
||
|
int firstGhost = moveGhostViewsToTop(overlay.mOverlayViewGroup, tempViews);
|
||
|
insertIntoOverlay(overlay.mOverlayViewGroup, parent, ghostView, tempViews, firstGhost);
|
||
|
ghostView.mReferences = previousRefCount;
|
||
|
} else if (matrix != null) {
|
||
|
ghostView.setMatrix(matrix);
|
||
|
}
|
||
|
ghostView.mReferences++;
|
||
|
return ghostView;
|
||
|
}
|
||
|
|
||
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
|
||
|
public static GhostView addGhost(View view, ViewGroup viewGroup) {
|
||
|
return addGhost(view, viewGroup, null);
|
||
|
}
|
||
|
|
||
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
|
||
|
public static void removeGhost(View view) {
|
||
|
GhostView ghostView = view.mGhostView;
|
||
|
if (ghostView != null) {
|
||
|
ghostView.mReferences--;
|
||
|
if (ghostView.mReferences == 0) {
|
||
|
ViewGroup parent = (ViewGroup) ghostView.getParent();
|
||
|
ViewGroup grandParent = (ViewGroup) parent.getParent();
|
||
|
grandParent.removeView(parent);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static GhostView getGhost(View view) {
|
||
|
return view.mGhostView;
|
||
|
}
|
||
|
|
||
|
private static void copySize(View from, View to) {
|
||
|
to.setLeft(0);
|
||
|
to.setTop(0);
|
||
|
to.setRight(from.getWidth());
|
||
|
to.setBottom(from.getHeight());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Move the GhostViews to the end so that they are on top of other views and it is easier
|
||
|
* to do binary search for the correct location for the GhostViews in insertIntoOverlay.
|
||
|
*
|
||
|
* @return The index of the first GhostView or -1 if no GhostView is in the ViewGroup
|
||
|
*/
|
||
|
private static int moveGhostViewsToTop(ViewGroup viewGroup, ArrayList<View> tempViews) {
|
||
|
final int numChildren = viewGroup.getChildCount();
|
||
|
if (numChildren == 0) {
|
||
|
return -1;
|
||
|
} else if (isGhostWrapper(viewGroup.getChildAt(numChildren - 1))) {
|
||
|
// GhostViews are already at the end
|
||
|
int firstGhost = numChildren - 1;
|
||
|
for (int i = numChildren - 2; i >= 0; i--) {
|
||
|
if (!isGhostWrapper(viewGroup.getChildAt(i))) {
|
||
|
break;
|
||
|
}
|
||
|
firstGhost = i;
|
||
|
}
|
||
|
return firstGhost;
|
||
|
}
|
||
|
|
||
|
// Remove all GhostViews from the middle
|
||
|
for (int i = numChildren - 2; i >= 0; i--) {
|
||
|
View child = viewGroup.getChildAt(i);
|
||
|
if (isGhostWrapper(child)) {
|
||
|
tempViews.add(child);
|
||
|
GhostView ghostView = (GhostView)((ViewGroup)child).getChildAt(0);
|
||
|
ghostView.mBeingMoved = true;
|
||
|
viewGroup.removeViewAt(i);
|
||
|
ghostView.mBeingMoved = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
final int firstGhost;
|
||
|
if (tempViews.isEmpty()) {
|
||
|
firstGhost = -1;
|
||
|
} else {
|
||
|
firstGhost = viewGroup.getChildCount();
|
||
|
// Add the GhostViews to the end
|
||
|
for (int i = tempViews.size() - 1; i >= 0; i--) {
|
||
|
viewGroup.addView(tempViews.get(i));
|
||
|
}
|
||
|
tempViews.clear();
|
||
|
}
|
||
|
return firstGhost;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Inserts a GhostView into the overlay's ViewGroup in the order in which they
|
||
|
* should be displayed by the UI.
|
||
|
*/
|
||
|
private static void insertIntoOverlay(ViewGroup viewGroup, ViewGroup wrapper,
|
||
|
GhostView ghostView, ArrayList<View> tempParents, int firstGhost) {
|
||
|
if (firstGhost == -1) {
|
||
|
viewGroup.addView(wrapper);
|
||
|
} else {
|
||
|
ArrayList<View> viewParents = new ArrayList<View>();
|
||
|
getParents(ghostView.mView, viewParents);
|
||
|
|
||
|
int index = getInsertIndex(viewGroup, viewParents, tempParents, firstGhost);
|
||
|
if (index < 0 || index >= viewGroup.getChildCount()) {
|
||
|
viewGroup.addView(wrapper);
|
||
|
} else {
|
||
|
viewGroup.addView(wrapper, index);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Find the index into the overlay to insert the GhostView based on the order that the
|
||
|
* views should be drawn. This keeps GhostViews layered in the same order
|
||
|
* that they are ordered in the UI.
|
||
|
*/
|
||
|
private static int getInsertIndex(ViewGroup overlayViewGroup, ArrayList<View> viewParents,
|
||
|
ArrayList<View> tempParents, int firstGhost) {
|
||
|
int low = firstGhost;
|
||
|
int high = overlayViewGroup.getChildCount() - 1;
|
||
|
|
||
|
while (low <= high) {
|
||
|
int mid = (low + high) / 2;
|
||
|
ViewGroup wrapper = (ViewGroup) overlayViewGroup.getChildAt(mid);
|
||
|
GhostView midView = (GhostView) wrapper.getChildAt(0);
|
||
|
getParents(midView.mView, tempParents);
|
||
|
if (isOnTop(viewParents, tempParents)) {
|
||
|
low = mid + 1;
|
||
|
} else {
|
||
|
high = mid - 1;
|
||
|
}
|
||
|
tempParents.clear();
|
||
|
}
|
||
|
|
||
|
return low;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns true if view is a GhostView's FrameLayout wrapper.
|
||
|
*/
|
||
|
private static boolean isGhostWrapper(View view) {
|
||
|
if (view instanceof FrameLayout) {
|
||
|
FrameLayout frameLayout = (FrameLayout) view;
|
||
|
if (frameLayout.getChildCount() == 1) {
|
||
|
View child = frameLayout.getChildAt(0);
|
||
|
return child instanceof GhostView;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns true if viewParents is from a View that is on top of the comparedWith's view.
|
||
|
* The ArrayLists contain the ancestors of views in order from top most grandparent, to
|
||
|
* the view itself, in order. The goal is to find the first matching parent and then
|
||
|
* compare the draw order of the siblings.
|
||
|
*/
|
||
|
private static boolean isOnTop(ArrayList<View> viewParents, ArrayList<View> comparedWith) {
|
||
|
if (viewParents.isEmpty() || comparedWith.isEmpty() ||
|
||
|
viewParents.get(0) != comparedWith.get(0)) {
|
||
|
// Not the same decorView -- arbitrary ordering
|
||
|
return true;
|
||
|
}
|
||
|
int depth = Math.min(viewParents.size(), comparedWith.size());
|
||
|
for (int i = 1; i < depth; i++) {
|
||
|
View viewParent = viewParents.get(i);
|
||
|
View comparedWithParent = comparedWith.get(i);
|
||
|
|
||
|
if (viewParent != comparedWithParent) {
|
||
|
// i - 1 is the same parent, but these are different children.
|
||
|
return isOnTop(viewParent, comparedWithParent);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// one of these is the parent of the other
|
||
|
boolean isComparedWithTheParent = (comparedWith.size() == depth);
|
||
|
return isComparedWithTheParent;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Adds all the parents, grandparents, etc. of view to parents.
|
||
|
*/
|
||
|
private static void getParents(View view, ArrayList<View> parents) {
|
||
|
ViewParent parent = view.getParent();
|
||
|
if (parent != null && parent instanceof ViewGroup) {
|
||
|
getParents((View) parent, parents);
|
||
|
}
|
||
|
parents.add(view);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns true if view would be drawn on top of comparedWith or false otherwise.
|
||
|
* view and comparedWith are siblings with the same parent. This uses the logic
|
||
|
* that dispatchDraw uses to determine which View should be drawn first.
|
||
|
*/
|
||
|
private static boolean isOnTop(View view, View comparedWith) {
|
||
|
ViewGroup parent = (ViewGroup) view.getParent();
|
||
|
|
||
|
final int childrenCount = parent.getChildCount();
|
||
|
final ArrayList<View> preorderedList = parent.buildOrderedChildList();
|
||
|
final boolean customOrder = preorderedList == null
|
||
|
&& parent.isChildrenDrawingOrderEnabled();
|
||
|
|
||
|
// This default value shouldn't be used because both view and comparedWith
|
||
|
// should be in the list. If there is an error, then just return an arbitrary
|
||
|
// view is on top.
|
||
|
boolean isOnTop = true;
|
||
|
for (int i = 0; i < childrenCount; i++) {
|
||
|
int childIndex = customOrder ? parent.getChildDrawingOrder(childrenCount, i) : i;
|
||
|
final View child = (preorderedList == null)
|
||
|
? parent.getChildAt(childIndex) : preorderedList.get(childIndex);
|
||
|
if (child == view) {
|
||
|
isOnTop = false;
|
||
|
break;
|
||
|
} else if (child == comparedWith) {
|
||
|
isOnTop = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (preorderedList != null) {
|
||
|
preorderedList.clear();
|
||
|
}
|
||
|
return isOnTop;
|
||
|
}
|
||
|
}
|