Draw SmartSelectSprite in TextView

Instead of the SmartSelectSprite drawing its contents into a View's
ViewOverlay, the SmartSelectSprite can now be drawn on any Canvas. In
order to perform the smart select animation, it is now the TextView that
performs the actual drawing.

Since the TextView can adjust the canvas for its padding and offset,
there is no more need to manually transform the selection rectangles, so
that part can be removed.

Test: manual - verify smart select animation still works
Change-Id: Ibaccf59fd44d5701e6f37d6b4fa97f2b05fd77cc
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 040a1d8..8a374406 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -1689,6 +1689,10 @@
         } else {
             layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
         }
+
+        if (mSelectionActionModeHelper != null) {
+            mSelectionActionModeHelper.onDraw(canvas);
+        }
     }
 
     private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
diff --git a/core/java/android/widget/SelectionActionModeHelper.java b/core/java/android/widget/SelectionActionModeHelper.java
index f1b36274..57a3468 100644
--- a/core/java/android/widget/SelectionActionModeHelper.java
+++ b/core/java/android/widget/SelectionActionModeHelper.java
@@ -21,6 +21,7 @@
 import android.annotation.UiThread;
 import android.annotation.WorkerThread;
 import android.content.Context;
+import android.graphics.Canvas;
 import android.graphics.PointF;
 import android.graphics.RectF;
 import android.os.AsyncTask;
@@ -73,6 +74,9 @@
     private AsyncTask mTextClassificationAsyncTask;
 
     private final SelectionTracker mSelectionTracker;
+
+    // TODO remove nullable marker once the switch gating the feature gets removed
+    @Nullable
     private final SmartSelectSprite mSmartSelectSprite;
 
     SelectionActionModeHelper(@NonNull Editor editor) {
@@ -85,7 +89,8 @@
                 new SelectionTracker(mTextView.getContext(), mTextView.isTextEditable());
 
         if (SMART_SELECT_ANIMATION_ENABLED) {
-            mSmartSelectSprite = new SmartSelectSprite(mTextView);
+            mSmartSelectSprite = new SmartSelectSprite(mTextView.getContext(),
+                    mTextView::invalidate);
         } else {
             mSmartSelectSprite = null;
         }
@@ -167,6 +172,12 @@
         cancelAsyncTask();
     }
 
+    public void onDraw(final Canvas canvas) {
+        if (mSmartSelectSprite != null) {
+            mSmartSelectSprite.draw(canvas);
+        }
+    }
+
     private void cancelAsyncTask() {
         if (mTextClassificationAsyncTask != null) {
             mTextClassificationAsyncTask.cancel(true);
@@ -235,19 +246,6 @@
             return;
         }
 
-        /*
-         * TODO Figure out a more robust approach for this
-         * We have to translate all the generated rectangles by the top-left padding of the
-         * TextView because the padding influences the rendering of the ViewOverlay, but is not
-         * taken into account when generating the selection path rectangles.
-         */
-        for (RectF rectangle : selectionRectangles) {
-            rectangle.left += mTextView.getPaddingLeft();
-            rectangle.right += mTextView.getPaddingLeft();
-            rectangle.top += mTextView.getPaddingTop();
-            rectangle.bottom += mTextView.getPaddingTop();
-        }
-
         final PointF touchPoint = new PointF(
                 mEditor.getLastUpPositionX(),
                 mEditor.getLastUpPositionY());
diff --git a/core/java/android/widget/SmartSelectSprite.java b/core/java/android/widget/SmartSelectSprite.java
index 8d06f5f..27b93bc 100644
--- a/core/java/android/widget/SmartSelectSprite.java
+++ b/core/java/android/widget/SmartSelectSprite.java
@@ -36,11 +36,11 @@
 import android.graphics.drawable.ShapeDrawable;
 import android.graphics.drawable.shapes.Shape;
 import android.util.TypedValue;
-import android.view.View;
-import android.view.ViewOverlay;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 
+import com.android.internal.util.Preconditions;
+
 import java.lang.annotation.Retention;
 import java.util.Collections;
 import java.util.Comparator;
@@ -50,7 +50,6 @@
 /**
  * A utility class for creating and animating the Smart Select animation.
  */
-// TODO Do not rely on ViewOverlays for drawing the Smart Select sprite
 final class SmartSelectSprite {
 
     private static final int EXPAND_DURATION = 300;
@@ -65,8 +64,8 @@
     private final Interpolator mCornerInterpolator;
     private final float mStrokeWidth;
 
-    private final View mView;
     private Animator mActiveAnimator = null;
+    private final Runnable mInvalidator;
     @ColorInt
     private final int mStrokeColor;
 
@@ -331,8 +330,12 @@
 
     }
 
-    SmartSelectSprite(final View view) {
-        final Context context = view.getContext();
+    /**
+     * @param context     The {@link Context} in which the animation will run
+     * @param invalidator A {@link Runnable} which will be called every time the animation updates,
+     *                    indicating that the view drawing the animation should invalidate itself
+     */
+    SmartSelectSprite(final Context context, final Runnable invalidator) {
         mExpandInterpolator = AnimationUtils.loadInterpolator(
                 context,
                 android.R.interpolator.fast_out_slow_in);
@@ -341,7 +344,7 @@
                 android.R.interpolator.fast_out_linear_in);
         mStrokeWidth = dpToPixel(context, STROKE_WIDTH_DP);
         mStrokeColor = getStrokeColor(context);
-        mView = view;
+        mInvalidator = Preconditions.checkNotNull(invalidator);
     }
 
     /**
@@ -366,7 +369,7 @@
         cancelAnimation();
 
         final ValueAnimator.AnimatorUpdateListener updateListener =
-                valueAnimator -> mView.invalidate();
+                valueAnimator -> mInvalidator.run();
 
         final List<RoundedRectangleShape> shapes = new LinkedList<>();
         final List<Animator> cornerAnimators = new LinkedList<>();
@@ -421,7 +424,6 @@
 
         mExistingRectangleList = rectangleList;
         mExistingDrawable = shapeDrawable;
-        mView.getOverlay().add(shapeDrawable);
 
         mActiveAnimator = createAnimator(rectangleList, startingOffsetLeft, startingOffsetRight,
                 cornerAnimators, updateListener,
@@ -480,7 +482,7 @@
             @Override
             public void onAnimationEnd(Animator animator) {
                 mExistingRectangleList.setDisplayType(RectangleList.DisplayType.POLYGON);
-                mExistingDrawable.invalidateSelf();
+                mInvalidator.run();
 
                 onAnimationEnd.run();
             }
@@ -581,11 +583,9 @@
     }
 
     private void removeExistingDrawables() {
-        final ViewOverlay overlay = mView.getOverlay();
-        overlay.remove(mExistingDrawable);
-
         mExistingDrawable = null;
         mExistingRectangleList = null;
+        mInvalidator.run();
     }
 
     /**
@@ -599,4 +599,10 @@
         }
     }
 
+    public void draw(Canvas canvas) {
+        if (mExistingDrawable != null) {
+            mExistingDrawable.draw(canvas);
+        }
+    }
+
 }