/*
* 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.graphics.Canvas;
import android.graphics.Matrix;
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 DisplayListCanvas) {
DisplayListCanvas dlCanvas = (DisplayListCanvas) canvas;
mView.mRecreateDisplayList = true;
RenderNode renderNode = mView.updateDisplayListIfDirty();
if (renderNode.isValid()) {
dlCanvas.insertReorderBarrier(); // enable shadow for this rendernode
dlCanvas.drawRenderNode(renderNode);
dlCanvas.insertInorderBarrier(); // 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);
}
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;
}
public static GhostView addGhost(View view, ViewGroup viewGroup) {
return addGhost(view, viewGroup, null);
}
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;
}
}