| /* |
| * 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.annotation.UnsupportedAppUsage; |
| import android.graphics.Canvas; |
| import android.graphics.Matrix; |
| import android.graphics.RecordingCanvas; |
| import android.graphics.RenderNode; |
| 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.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); |
| } |
| |
| @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 |
| public static GhostView addGhost(View view, ViewGroup viewGroup) { |
| return addGhost(view, viewGroup, null); |
| } |
| |
| @UnsupportedAppUsage |
| 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; |
| } |
| } |