/* * 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 tempViews = new ArrayList(); 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 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 tempParents, int firstGhost) { if (firstGhost == -1) { viewGroup.addView(wrapper); } else { ArrayList viewParents = new ArrayList(); 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 viewParents, ArrayList 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 viewParents, ArrayList 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 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 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; } }