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