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