Add overlays to views

It is useful, particularly in animations, to be able to add a view, or at
least some graphics, on top of a view. For example, to have a child of a layout
fade away, we might want to remove the child from that layout and then fade it out
gradually. Meanwhile, we have to have a place to put that view where it will be
drawn. We could do this in the content container sometimes, but this is not a
reliable workaround in the general case, and may obscure other siblings/parents of
the layout/view in the hierarchy. A better approach would be to place a view/graphic
temporarily in the layout itself.

This feature adds the ability to add one or more Views and Drawables to an "overlay"
layer, after which the view will handle drawing that extra content when it redraws itself.

Issue #8350510 Add APIs needed for future animation capabilities

Change-Id: I70bf78c46ee3db8bd87ea1cdc2ecb5c0747ccbf9
diff --git a/api/current.txt b/api/current.txt
index 1668170..6302ddf 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -24951,6 +24951,14 @@
     field public static final int ORIENTATION_UNKNOWN = -1; // 0xffffffff
   }
 
+  public abstract interface Overlay {
+    method public abstract void add(android.graphics.drawable.Drawable);
+    method public abstract void add(android.view.View);
+    method public abstract void clear();
+    method public abstract void remove(android.graphics.drawable.Drawable);
+    method public abstract void remove(android.view.View);
+  }
+
   public class ScaleGestureDetector {
     ctor public ScaleGestureDetector(android.content.Context, android.view.ScaleGestureDetector.OnScaleGestureListener);
     method public float getCurrentSpan();
@@ -25255,6 +25263,7 @@
     method public int getNextFocusUpId();
     method public android.view.View.OnFocusChangeListener getOnFocusChangeListener();
     method public int getOverScrollMode();
+    method public android.view.Overlay getOverlay();
     method public int getPaddingBottom();
     method public int getPaddingEnd();
     method public int getPaddingLeft();
diff --git a/core/java/android/view/Overlay.java b/core/java/android/view/Overlay.java
new file mode 100644
index 0000000..f15d4d2
--- /dev/null
+++ b/core/java/android/view/Overlay.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2013 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.graphics.drawable.Drawable;
+
+/**
+ * An overlay is an extra layer that sits on top of a View (the "host view") which is drawn after
+ * all other content in that view (including children, if the view is a ViewGroup). Interaction
+ * with the overlay layer is done in terms of adding/removing views and drawables. Invalidation and
+ * redrawing of the overlay layer (and its host view) is handled differently for views versus
+ * drawables in the overlay. Views invalidate themselves as usual, causing appropriate redrawing
+ * to occur automatically. Drawables, on the other hand, do not manage invalidation, so changes to
+ * drawable objects should be accompanied by appropriate calls to invalidate() on the host view.
+ *
+ * @see android.view.View#getOverlay()
+ */
+public interface Overlay {
+
+    /**
+     * Adds a Drawable to the overlay. The bounds of the drawable should be relative to
+     * the host view. Any drawable added to the overlay should be removed when it is no longer
+     * needed or no longer visible. There is no automatic invalidation of the host view; changes to
+     * the drawable should be accompanied by appropriate invalidation calls to the host view
+     * to cause the proper area of the view, and the overlay, to be redrawn.
+     *
+     * @param drawable The Drawable to be added to the overlay. This drawable will be
+     * drawn when the view redraws its overlay.
+     * @see #remove(android.graphics.drawable.Drawable)
+     * @see #add(View)
+     */
+    void add(Drawable drawable);
+
+    /**
+     * Removes the specified Drawable from the overlay.
+     *
+     * @param drawable The Drawable to be removed from the overlay.
+     * @see #add(android.graphics.drawable.Drawable)
+     */
+    void remove(Drawable drawable);
+
+    /**
+     * Adds a View to the overlay. The bounds of the added view should be relative to
+     * the host view. Any view added to the overlay should be removed when it is no longer
+     * needed or no longer visible. The view must not be parented elsewhere when it is added
+     * to the overlay.
+     *
+     * @param view The View to be added to the overlay. The added view will be
+     * drawn when the overlay is drawn.
+     * @see #remove(View)
+     * @see #add(android.graphics.drawable.Drawable)
+     */
+    void add(View view);
+
+    /**
+     * Removes the specified View from the overlay.
+     *
+     * @param view The View to be removed from the overlay.
+     * @see #add(View)
+     */
+    void remove(View view);
+
+    /**
+     * Removes all views and drawables from the overlay.
+     */
+    void clear();
+}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 2e60f51..f9b2064 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -3223,6 +3223,12 @@
     AccessibilityDelegate mAccessibilityDelegate;
 
     /**
+     * The view's overlay layer. Developers get a reference to the overlay via getOverlay()
+     * and add/remove objects to/from the overlay directly through the Overlay methods.
+     */
+    ViewOverlay mOverlay;
+
+    /**
      * Consistency verifier for debugging purposes.
      * @hide
      */
@@ -9589,7 +9595,7 @@
                 mDisplayList.setTop(mTop);
             }
 
-            onSizeChanged(width, mBottom - mTop, width, oldHeight);
+            sizeChange(width, mBottom - mTop, width, oldHeight);
 
             if (!matrixIsIdentity) {
                 if ((mPrivateFlags & PFLAG_PIVOT_EXPLICITLY_SET) == 0) {
@@ -9662,7 +9668,7 @@
                 mDisplayList.setBottom(mBottom);
             }
 
-            onSizeChanged(width, mBottom - mTop, width, oldHeight);
+            sizeChange(width, mBottom - mTop, width, oldHeight);
 
             if (!matrixIsIdentity) {
                 if ((mPrivateFlags & PFLAG_PIVOT_EXPLICITLY_SET) == 0) {
@@ -9729,7 +9735,7 @@
                 mDisplayList.setLeft(left);
             }
 
-            onSizeChanged(mRight - mLeft, height, oldWidth, height);
+            sizeChange(mRight - mLeft, height, oldWidth, height);
 
             if (!matrixIsIdentity) {
                 if ((mPrivateFlags & PFLAG_PIVOT_EXPLICITLY_SET) == 0) {
@@ -9793,7 +9799,7 @@
                 mDisplayList.setRight(mRight);
             }
 
-            onSizeChanged(mRight - mLeft, height, oldWidth, height);
+            sizeChange(mRight - mLeft, height, oldWidth, height);
 
             if (!matrixIsIdentity) {
                 if ((mPrivateFlags & PFLAG_PIVOT_EXPLICITLY_SET) == 0) {
@@ -12088,6 +12094,9 @@
     void dispatchAttachedToWindow(AttachInfo info, int visibility) {
         //System.out.println("Attached! " + this);
         mAttachInfo = info;
+        if (mOverlay != null) {
+            mOverlay.mAttachInfo = info;
+        }
         mWindowAttachCount++;
         // We will need to evaluate the drawable state at least once.
         mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
@@ -12156,6 +12165,9 @@
         }
 
         mAttachInfo = null;
+        if (mOverlay != null) {
+            mOverlay.mAttachInfo = null;
+        }
     }
 
     /**
@@ -12811,6 +12823,9 @@
                     // Fast path for layouts with no backgrounds
                     if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                         dispatchDraw(canvas);
+                        if (mOverlay != null && !mOverlay.isEmpty()) {
+                            mOverlay.draw(canvas);
+                        }
                     } else {
                         draw(canvas);
                     }
@@ -13124,6 +13139,9 @@
             if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                 mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                 dispatchDraw(canvas);
+                if (mOverlay != null && !mOverlay.isEmpty()) {
+                    mOverlay.draw(canvas);
+                }
             } else {
                 draw(canvas);
             }
@@ -13879,6 +13897,10 @@
             // Step 6, draw decorations (scrollbars)
             onDrawScrollBars(canvas);
 
+            if (mOverlay != null && !mOverlay.isEmpty()) {
+                mOverlay.dispatchDraw(canvas);
+            }
+
             // we're done...
             return;
         }
@@ -14018,6 +14040,37 @@
 
         // Step 6, draw decorations (scrollbars)
         onDrawScrollBars(canvas);
+
+        if (mOverlay != null && !mOverlay.isEmpty()) {
+            mOverlay.dispatchDraw(canvas);
+        }
+    }
+
+    /**
+     * Called by the addToOverlay() methods to create, attach, and size the overlay as necessary
+     */
+    private void setupOverlay() {
+        if (mOverlay == null) {
+            mOverlay = new ViewOverlay(mContext, this);
+            mOverlay.mAttachInfo = mAttachInfo;
+            mOverlay.setRight(mRight);
+            mOverlay.setBottom(mBottom);
+        }
+    }
+
+    /**
+     * Returns the overlay for this view, creating it if it does not yet exist. Adding drawables
+     * and/or views to the overlay will cause them to be displayed whenever the view itself is
+     * redrawn. Objects in the overlay should be actively managed: remove them when they should
+     * not be displayed anymore and invalidate this view appropriately when overlay drawables
+     * change. The overlay will always have the same size as its host view.
+     *
+     * @return The Overlay object for this view.
+     * @see Overlay
+     */
+    public Overlay getOverlay() {
+        setupOverlay();
+        return mOverlay;
     }
 
     /**
@@ -14273,7 +14326,7 @@
                         mTransformationInfo.mMatrixDirty = true;
                     }
                 }
-                onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
+                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
             }
 
             if ((mViewFlags & VISIBILITY_MASK) == VISIBLE) {
@@ -14297,6 +14350,14 @@
         return changed;
     }
 
+    private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
+        onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
+        if (mOverlay != null) {
+            mOverlay.setRight(mRight);
+            mOverlay.setBottom(mBottom);
+        }
+    }
+
     /**
      * Finalize inflating a view from XML.  This is called as the last phase
      * of inflation, after all child views have been added.
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index 442dfdb..d63f7bc 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -1869,7 +1869,7 @@
                     removePointersFromTouchTargets(idBitsToAssign);
 
                     final int childrenCount = mChildrenCount;
-                    if (childrenCount != 0) {
+                    if (childrenCount != 0 || mOverlay != null) {
                         // Find a child that can receive the event.
                         // Scan children from front to back.
                         final View[] children = mChildren;
@@ -1906,6 +1906,27 @@
                                 break;
                             }
                         }
+                        if (mOverlay != null && newTouchTarget == null) {
+                            // Check to see whether the overlay can handle the event
+                            final View child = mOverlay;
+                            if (canViewReceivePointerEvents(child) &&
+                                    isTransformedTouchPointInView(x, y, child, null)) {
+                                newTouchTarget = getTouchTarget(child);
+                                if (newTouchTarget != null) {
+                                    newTouchTarget.pointerIdBits |= idBitsToAssign;
+                                } else {
+                                    resetCancelNextUpFlag(child);
+                                    if (dispatchTransformedTouchEvent(ev, false, child,
+                                            idBitsToAssign)) {
+                                        mLastTouchDownTime = ev.getDownTime();
+                                        mLastTouchDownX = ev.getX();
+                                        mLastTouchDownY = ev.getY();
+                                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
+                                        alreadyDispatchedToNewTouchTarget = true;
+                                    }
+                                }
+                            }
+                        }
                     }
 
                     if (newTouchTarget == null && mFirstTouchTarget != null) {
@@ -3022,6 +3043,13 @@
                 child.mRecreateDisplayList = false;
             }
         }
+        if (mOverlay != null) {
+            mOverlay.mRecreateDisplayList = (mOverlay.mPrivateFlags & PFLAG_INVALIDATED)
+                    == PFLAG_INVALIDATED;
+            mOverlay.mPrivateFlags &= ~PFLAG_INVALIDATED;
+            mOverlay.getDisplayList();
+            mOverlay.mRecreateDisplayList = false;
+        }
     }
 
     /**
diff --git a/core/java/android/view/ViewOverlay.java b/core/java/android/view/ViewOverlay.java
new file mode 100644
index 0000000..8c2ab9d
--- /dev/null
+++ b/core/java/android/view/ViewOverlay.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2013 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.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import java.util.ArrayList;
+
+/**
+ * ViewOverlay is a container that View uses to host all objects (views and drawables) that
+ * are added to its "overlay", gotten through {@link View#getOverlay()}. Views and drawables are
+ * added to the overlay via the add/remove methods in this class. These views and drawables are
+ * then drawn whenever the view itself is drawn, after which it will draw its overlay (if it
+ * exists).
+ *
+ * Besides managing and drawing the list of drawables, this class serves two purposes:
+ * (1) it noops layout calls because children are absolutely positioned and
+ * (2) it forwards all invalidation calls to its host view. The invalidation redirect is
+ * necessary because the overlay is not a child of the host view and invalidation cannot
+ * therefore follow the normal path up through the parent hierarchy.
+ *
+ * @hide
+ */
+class ViewOverlay extends ViewGroup implements Overlay {
+
+    /**
+     * The View for which this is an overlay. Invalidations of the overlay are redirected to
+     * this host view.
+     */
+    View mHostView;
+
+    /**
+     * The set of drawables to draw when the overlay is rendered.
+     */
+    ArrayList<Drawable> mDrawables = null;
+
+    ViewOverlay(Context context, View host) {
+        super(context);
+        mHostView = host;
+        mParent = mHostView.getParent();
+    }
+
+    @Override
+    public void add(Drawable drawable) {
+        if (mDrawables == null) {
+            mDrawables = new ArrayList<Drawable>();
+        }
+        if (!mDrawables.contains(drawable)) {
+            // Make each drawable unique in the overlay; can't add it more than once
+            mDrawables.add(drawable);
+            invalidate(drawable.getBounds());
+        }
+    }
+
+    @Override
+    public void remove(Drawable drawable) {
+        if (mDrawables != null) {
+            mDrawables.remove(drawable);
+            invalidate(drawable.getBounds());
+        }
+    }
+
+    @Override
+    public void add(View child) {
+        super.addView(child);
+    }
+
+    @Override
+    public void remove(View view) {
+        super.removeView(view);
+    }
+
+    @Override
+    public void clear() {
+        removeAllViews();
+        mDrawables.clear();
+    }
+
+    boolean isEmpty() {
+        if (getChildCount() == 0 && (mDrawables == null || mDrawables.size() == 0)) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+        final int numDrawables = (mDrawables == null) ? 0 : mDrawables.size();
+        for (int i = 0; i < numDrawables; ++i) {
+            mDrawables.get(i).draw(canvas);
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        // Noop: children are positioned absolutely
+    }
+
+    /*
+     The following invalidation overrides exist for the purpose of redirecting invalidation to
+     the host view. The overlay is not parented to the host view (since a View cannot be a parent),
+     so the invalidation cannot proceed through the normal parent hierarchy.
+     There is a built-in assumption that the overlay exactly covers the host view, therefore
+     the invalidation rectangles received do not need to be adjusted when forwarded to
+     the host view.
+     */
+
+    @Override
+    public void invalidate(Rect dirty) {
+        super.invalidate(dirty);
+        if (mHostView != null) {
+            dirty.offset(getLeft(), getTop());
+            mHostView.invalidate(dirty);
+        }
+    }
+
+    @Override
+    public void invalidate(int l, int t, int r, int b) {
+        super.invalidate(l, t, r, b);
+        if (mHostView != null) {
+            mHostView.invalidate(l, t, r, b);
+        }
+    }
+
+    @Override
+    public void invalidate() {
+        super.invalidate();
+        if (mHostView != null) {
+            mHostView.invalidate();
+        }
+    }
+
+    @Override
+    void invalidate(boolean invalidateCache) {
+        super.invalidate(invalidateCache);
+        if (mHostView != null) {
+            mHostView.invalidate(invalidateCache);
+        }
+    }
+
+    @Override
+    void invalidateViewProperty(boolean invalidateParent, boolean forceRedraw) {
+        super.invalidateViewProperty(invalidateParent, forceRedraw);
+        if (mHostView != null) {
+            mHostView.invalidateViewProperty(invalidateParent, forceRedraw);
+        }
+    }
+
+    @Override
+    protected void invalidateParentCaches() {
+        super.invalidateParentCaches();
+        if (mHostView != null) {
+            mHostView.invalidateParentCaches();
+        }
+    }
+
+    @Override
+    protected void invalidateParentIfNeeded() {
+        super.invalidateParentIfNeeded();
+        if (mHostView != null) {
+            mHostView.invalidateParentIfNeeded();
+        }
+    }
+
+    public void invalidateChildFast(View child, final Rect dirty) {
+        if (mHostView != null) {
+            // Note: This is not a "fast" invalidation. Would be nice to instead invalidate using DL
+            // properties and a dirty rect instead of causing a real invalidation of the host view
+            int left = child.mLeft;
+            int top = child.mTop;
+            if (!child.getMatrix().isIdentity()) {
+                child.transformRect(dirty);
+            }
+            dirty.offset(left, top);
+            mHostView.invalidate(dirty);
+        }
+    }
+
+    @Override
+    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
+        if (mHostView != null) {
+            mHostView.invalidate(dirty);
+        }
+        return null;
+    }
+}