Home | History | Annotate | Download | only in view
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package android.view;
     17 
     18 import android.annotation.UnsupportedAppUsage;
     19 import android.graphics.Canvas;
     20 import android.graphics.Matrix;
     21 import android.graphics.RecordingCanvas;
     22 import android.graphics.RenderNode;
     23 import android.os.Build;
     24 import android.widget.FrameLayout;
     25 
     26 import java.util.ArrayList;
     27 
     28 /**
     29  * This view draws another View in an Overlay without changing the parent. It will not be drawn
     30  * by its parent because its visibility is set to INVISIBLE, but will be drawn
     31  * here using its render node. When the GhostView is set to INVISIBLE, the View it is
     32  * shadowing will become VISIBLE and when the GhostView becomes VISIBLE, the shadowed
     33  * view becomes INVISIBLE.
     34  * @hide
     35  */
     36 public class GhostView extends View {
     37     private final View mView;
     38     private int mReferences;
     39     private boolean mBeingMoved;
     40 
     41     private GhostView(View view) {
     42         super(view.getContext());
     43         mView = view;
     44         mView.mGhostView = this;
     45         final ViewGroup parent = (ViewGroup) mView.getParent();
     46         mView.setTransitionVisibility(View.INVISIBLE);
     47         parent.invalidate();
     48     }
     49 
     50     @Override
     51     protected void onDraw(Canvas canvas) {
     52         if (canvas instanceof RecordingCanvas) {
     53             RecordingCanvas dlCanvas = (RecordingCanvas) canvas;
     54             mView.mRecreateDisplayList = true;
     55             RenderNode renderNode = mView.updateDisplayListIfDirty();
     56             if (renderNode.hasDisplayList()) {
     57                 dlCanvas.insertReorderBarrier(); // enable shadow for this rendernode
     58                 dlCanvas.drawRenderNode(renderNode);
     59                 dlCanvas.insertInorderBarrier(); // re-disable reordering/shadows
     60             }
     61         }
     62     }
     63 
     64     public void setMatrix(Matrix matrix) {
     65         mRenderNode.setAnimationMatrix(matrix);
     66     }
     67 
     68     @Override
     69     public void setVisibility(@Visibility int visibility) {
     70         super.setVisibility(visibility);
     71         if (mView.mGhostView == this) {
     72             int inverseVisibility = (visibility == View.VISIBLE) ? View.INVISIBLE : View.VISIBLE;
     73             mView.setTransitionVisibility(inverseVisibility);
     74         }
     75     }
     76 
     77     @Override
     78     protected void onDetachedFromWindow() {
     79         super.onDetachedFromWindow();
     80         if (!mBeingMoved) {
     81             mView.setTransitionVisibility(View.VISIBLE);
     82             mView.mGhostView = null;
     83             final ViewGroup parent = (ViewGroup) mView.getParent();
     84             if (parent != null) {
     85                 parent.invalidate();
     86             }
     87         }
     88     }
     89 
     90     public static void calculateMatrix(View view, ViewGroup host, Matrix matrix) {
     91         ViewGroup parent = (ViewGroup) view.getParent();
     92         matrix.reset();
     93         parent.transformMatrixToGlobal(matrix);
     94         matrix.preTranslate(-parent.getScrollX(), -parent.getScrollY());
     95         host.transformMatrixToLocal(matrix);
     96     }
     97 
     98     @UnsupportedAppUsage
     99     public static GhostView addGhost(View view, ViewGroup viewGroup, Matrix matrix) {
    100         if (!(view.getParent() instanceof ViewGroup)) {
    101             throw new IllegalArgumentException("Ghosted views must be parented by a ViewGroup");
    102         }
    103         ViewGroupOverlay overlay = viewGroup.getOverlay();
    104         ViewOverlay.OverlayViewGroup overlayViewGroup = overlay.mOverlayViewGroup;
    105         GhostView ghostView = view.mGhostView;
    106         int previousRefCount = 0;
    107         if (ghostView != null) {
    108             View oldParent = (View) ghostView.getParent();
    109             ViewGroup oldGrandParent = (ViewGroup) oldParent.getParent();
    110             if (oldGrandParent != overlayViewGroup) {
    111                 previousRefCount = ghostView.mReferences;
    112                 oldGrandParent.removeView(oldParent);
    113                 ghostView = null;
    114             }
    115         }
    116         if (ghostView == null) {
    117             if (matrix == null) {
    118                 matrix = new Matrix();
    119                 calculateMatrix(view, viewGroup, matrix);
    120             }
    121             ghostView = new GhostView(view);
    122             ghostView.setMatrix(matrix);
    123             FrameLayout parent = new FrameLayout(view.getContext());
    124             parent.setClipChildren(false);
    125             copySize(viewGroup, parent);
    126             copySize(viewGroup, ghostView);
    127             parent.addView(ghostView);
    128             ArrayList<View> tempViews = new ArrayList<View>();
    129             int firstGhost = moveGhostViewsToTop(overlay.mOverlayViewGroup, tempViews);
    130             insertIntoOverlay(overlay.mOverlayViewGroup, parent, ghostView, tempViews, firstGhost);
    131             ghostView.mReferences = previousRefCount;
    132         } else if (matrix != null) {
    133             ghostView.setMatrix(matrix);
    134         }
    135         ghostView.mReferences++;
    136         return ghostView;
    137     }
    138 
    139     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    140     public static GhostView addGhost(View view, ViewGroup viewGroup) {
    141         return addGhost(view, viewGroup, null);
    142     }
    143 
    144     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    145     public static void removeGhost(View view) {
    146         GhostView ghostView = view.mGhostView;
    147         if (ghostView != null) {
    148             ghostView.mReferences--;
    149             if (ghostView.mReferences == 0) {
    150                 ViewGroup parent = (ViewGroup) ghostView.getParent();
    151                 ViewGroup grandParent = (ViewGroup) parent.getParent();
    152                 grandParent.removeView(parent);
    153             }
    154         }
    155     }
    156 
    157     public static GhostView getGhost(View view) {
    158         return view.mGhostView;
    159     }
    160 
    161     private static void copySize(View from, View to) {
    162         to.setLeft(0);
    163         to.setTop(0);
    164         to.setRight(from.getWidth());
    165         to.setBottom(from.getHeight());
    166     }
    167 
    168     /**
    169      * Move the GhostViews to the end so that they are on top of other views and it is easier
    170      * to do binary search for the correct location for the GhostViews in insertIntoOverlay.
    171      *
    172      * @return The index of the first GhostView or -1 if no GhostView is in the ViewGroup
    173      */
    174     private static int moveGhostViewsToTop(ViewGroup viewGroup, ArrayList<View> tempViews) {
    175         final int numChildren = viewGroup.getChildCount();
    176         if (numChildren == 0) {
    177             return -1;
    178         } else if (isGhostWrapper(viewGroup.getChildAt(numChildren - 1))) {
    179             // GhostViews are already at the end
    180             int firstGhost = numChildren - 1;
    181             for (int i = numChildren - 2; i >= 0; i--) {
    182                 if (!isGhostWrapper(viewGroup.getChildAt(i))) {
    183                     break;
    184                 }
    185                 firstGhost = i;
    186             }
    187             return firstGhost;
    188         }
    189 
    190         // Remove all GhostViews from the middle
    191         for (int i = numChildren - 2; i >= 0; i--) {
    192             View child = viewGroup.getChildAt(i);
    193             if (isGhostWrapper(child)) {
    194                 tempViews.add(child);
    195                 GhostView ghostView = (GhostView)((ViewGroup)child).getChildAt(0);
    196                 ghostView.mBeingMoved = true;
    197                 viewGroup.removeViewAt(i);
    198                 ghostView.mBeingMoved = false;
    199             }
    200         }
    201 
    202         final int firstGhost;
    203         if (tempViews.isEmpty()) {
    204             firstGhost = -1;
    205         } else {
    206             firstGhost = viewGroup.getChildCount();
    207             // Add the GhostViews to the end
    208             for (int i = tempViews.size() - 1; i >= 0; i--) {
    209                 viewGroup.addView(tempViews.get(i));
    210             }
    211             tempViews.clear();
    212         }
    213         return firstGhost;
    214     }
    215 
    216     /**
    217      * Inserts a GhostView into the overlay's ViewGroup in the order in which they
    218      * should be displayed by the UI.
    219      */
    220     private static void insertIntoOverlay(ViewGroup viewGroup, ViewGroup wrapper,
    221             GhostView ghostView, ArrayList<View> tempParents, int firstGhost) {
    222         if (firstGhost == -1) {
    223             viewGroup.addView(wrapper);
    224         } else {
    225             ArrayList<View> viewParents = new ArrayList<View>();
    226             getParents(ghostView.mView, viewParents);
    227 
    228             int index = getInsertIndex(viewGroup, viewParents, tempParents, firstGhost);
    229             if (index < 0 || index >= viewGroup.getChildCount()) {
    230                 viewGroup.addView(wrapper);
    231             } else {
    232                 viewGroup.addView(wrapper, index);
    233             }
    234         }
    235     }
    236 
    237     /**
    238      * Find the index into the overlay to insert the GhostView based on the order that the
    239      * views should be drawn. This keeps GhostViews layered in the same order
    240      * that they are ordered in the UI.
    241      */
    242     private static int getInsertIndex(ViewGroup overlayViewGroup, ArrayList<View> viewParents,
    243             ArrayList<View> tempParents, int firstGhost) {
    244         int low = firstGhost;
    245         int high = overlayViewGroup.getChildCount() - 1;
    246 
    247         while (low <= high) {
    248             int mid = (low + high) / 2;
    249             ViewGroup wrapper = (ViewGroup) overlayViewGroup.getChildAt(mid);
    250             GhostView midView = (GhostView) wrapper.getChildAt(0);
    251             getParents(midView.mView, tempParents);
    252             if (isOnTop(viewParents, tempParents)) {
    253                 low = mid + 1;
    254             } else {
    255                 high = mid - 1;
    256             }
    257             tempParents.clear();
    258         }
    259 
    260         return low;
    261     }
    262 
    263     /**
    264      * Returns true if view is a GhostView's FrameLayout wrapper.
    265      */
    266     private static boolean isGhostWrapper(View view) {
    267         if (view instanceof FrameLayout) {
    268             FrameLayout frameLayout = (FrameLayout) view;
    269             if (frameLayout.getChildCount() == 1) {
    270                 View child = frameLayout.getChildAt(0);
    271                 return child instanceof GhostView;
    272             }
    273         }
    274         return false;
    275     }
    276 
    277     /**
    278      * Returns true if viewParents is from a View that is on top of the comparedWith's view.
    279      * The ArrayLists contain the ancestors of views in order from top most grandparent, to
    280      * the view itself, in order. The goal is to find the first matching parent and then
    281      * compare the draw order of the siblings.
    282      */
    283     private static boolean isOnTop(ArrayList<View> viewParents, ArrayList<View> comparedWith) {
    284         if (viewParents.isEmpty() || comparedWith.isEmpty() ||
    285                 viewParents.get(0) != comparedWith.get(0)) {
    286             // Not the same decorView -- arbitrary ordering
    287             return true;
    288         }
    289         int depth = Math.min(viewParents.size(), comparedWith.size());
    290         for (int i = 1; i < depth; i++) {
    291             View viewParent = viewParents.get(i);
    292             View comparedWithParent = comparedWith.get(i);
    293 
    294             if (viewParent != comparedWithParent) {
    295                 // i - 1 is the same parent, but these are different children.
    296                 return isOnTop(viewParent, comparedWithParent);
    297             }
    298         }
    299 
    300         // one of these is the parent of the other
    301         boolean isComparedWithTheParent = (comparedWith.size() == depth);
    302         return isComparedWithTheParent;
    303     }
    304 
    305     /**
    306      * Adds all the parents, grandparents, etc. of view to parents.
    307      */
    308     private static void getParents(View view, ArrayList<View> parents) {
    309         ViewParent parent = view.getParent();
    310         if (parent != null && parent instanceof ViewGroup) {
    311             getParents((View) parent, parents);
    312         }
    313         parents.add(view);
    314     }
    315 
    316     /**
    317      * Returns true if view would be drawn on top of comparedWith or false otherwise.
    318      * view and comparedWith are siblings with the same parent. This uses the logic
    319      * that dispatchDraw uses to determine which View should be drawn first.
    320      */
    321     private static boolean isOnTop(View view, View comparedWith) {
    322         ViewGroup parent = (ViewGroup) view.getParent();
    323 
    324         final int childrenCount = parent.getChildCount();
    325         final ArrayList<View> preorderedList = parent.buildOrderedChildList();
    326         final boolean customOrder = preorderedList == null
    327                 && parent.isChildrenDrawingOrderEnabled();
    328 
    329         // This default value shouldn't be used because both view and comparedWith
    330         // should be in the list. If there is an error, then just return an arbitrary
    331         // view is on top.
    332         boolean isOnTop = true;
    333         for (int i = 0; i < childrenCount; i++) {
    334             int childIndex = customOrder ? parent.getChildDrawingOrder(childrenCount, i) : i;
    335             final View child = (preorderedList == null)
    336                     ? parent.getChildAt(childIndex) : preorderedList.get(childIndex);
    337             if (child == view) {
    338                 isOnTop = false;
    339                 break;
    340             } else if (child == comparedWith) {
    341                 isOnTop = true;
    342                 break;
    343             }
    344         }
    345 
    346         if (preorderedList != null) {
    347             preorderedList.clear();
    348         }
    349         return isOnTop;
    350     }
    351 }
    352