Home | History | Annotate | Download | only in input
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 package org.chromium.content.browser.input;
      6 
      7 import android.content.Context;
      8 import android.graphics.Canvas;
      9 import android.graphics.drawable.Drawable;
     10 import android.view.MotionEvent;
     11 import android.view.View;
     12 import android.view.animation.AnimationUtils;
     13 import android.widget.PopupWindow;
     14 
     15 import org.chromium.base.ApiCompatibilityUtils;
     16 import org.chromium.base.CalledByNative;
     17 import org.chromium.base.JNINamespace;
     18 import org.chromium.content.browser.PositionObserver;
     19 
     20 import java.lang.ref.WeakReference;
     21 
     22 /**
     23  * View that displays a selection or insertion handle for text editing.
     24  *
     25  * While a HandleView is logically a child of some other view, it does not exist in that View's
     26  * hierarchy.
     27  *
     28  */
     29 @JNINamespace("content")
     30 public class PopupTouchHandleDrawable extends View {
     31     private Drawable mDrawable;
     32     private final PopupWindow mContainer;
     33     private final Context mContext;
     34     private final PositionObserver.Listener mParentPositionListener;
     35 
     36     // The weak delegate reference allows the PopupTouchHandleDrawable to be owned by a native
     37     // object that might have a different lifetime (or a cyclic lifetime) with respect to the
     38     // delegate, allowing garbage collection of any Java references.
     39     private final WeakReference<PopupTouchHandleDrawableDelegate> mDelegate;
     40 
     41     // The observer reference will only be non-null while it is attached to mParentPositionListener.
     42     private PositionObserver mParentPositionObserver;
     43 
     44     // The position of the handle relative to the parent view.
     45     private int mPositionX;
     46     private int mPositionY;
     47 
     48     // The position of the parent relative to the application's root view.
     49     private int mParentPositionX;
     50     private int mParentPositionY;
     51 
     52     // The offset from this handles position to the "tip" of the handle.
     53     private float mHotspotX;
     54     private float mHotspotY;
     55 
     56     private float mAlpha;
     57 
     58     private final int[] mTempScreenCoords = new int[2];
     59 
     60     static final int LEFT = 0;
     61     static final int CENTER = 1;
     62     static final int RIGHT = 2;
     63     private int mOrientation = -1;
     64 
     65     // Length of the delay before fading in after the last page movement.
     66     private static final int FADE_IN_DELAY_MS = 300;
     67     private static final int FADE_IN_DURATION_MS = 200;
     68     private Runnable mDeferredHandleFadeInRunnable;
     69     private long mFadeStartTime;
     70     private boolean mVisible;
     71     private boolean mTemporarilyHidden;
     72 
     73     // Deferred runnable to avoid invalidating outside of frame dispatch,
     74     // in turn avoiding issues with sync barrier insertion.
     75     private Runnable mInvalidationRunnable;
     76     private boolean mHasPendingInvalidate;
     77 
     78     /**
     79      * Provides additional interaction behaviors necessary for handle
     80      * manipulation and interaction.
     81      */
     82     public interface PopupTouchHandleDrawableDelegate {
     83         /**
     84          * @return The parent View of the PopupWindow.
     85          */
     86         View getParent();
     87 
     88         /**
     89          * @return A position observer for the parent View, used to keep the
     90          *         absolutely positioned PopupWindow in-sync with the parent.
     91          */
     92         PositionObserver getParentPositionObserver();
     93 
     94         /**
     95          * Should route MotionEvents to the appropriate logic layer for
     96          * performing handle manipulation.
     97          */
     98         boolean onTouchHandleEvent(MotionEvent ev);
     99 
    100         /**
    101          * @return Whether the associated content is actively scrolling.
    102          */
    103         boolean isScrollInProgress();
    104     }
    105 
    106     public PopupTouchHandleDrawable(PopupTouchHandleDrawableDelegate delegate) {
    107         super(delegate.getParent().getContext());
    108         mContext = delegate.getParent().getContext();
    109         mDelegate = new WeakReference<PopupTouchHandleDrawableDelegate>(delegate);
    110         mContainer = new PopupWindow(mContext, null, android.R.attr.textSelectHandleWindowStyle);
    111         mContainer.setSplitTouchEnabled(true);
    112         mContainer.setClippingEnabled(false);
    113         mContainer.setAnimationStyle(0);
    114         mAlpha = 1.f;
    115         mVisible = getVisibility() == VISIBLE;
    116         mParentPositionListener = new PositionObserver.Listener() {
    117             @Override
    118             public void onPositionChanged(int x, int y) {
    119                 updateParentPosition(x, y);
    120             }
    121         };
    122     }
    123 
    124     @Override
    125     public boolean onTouchEvent(MotionEvent event) {
    126         final PopupTouchHandleDrawableDelegate delegate = mDelegate.get();
    127         if (delegate == null) {
    128             // If the delegate is gone, we should immediately dispose of the popup.
    129             hide();
    130             return false;
    131         }
    132 
    133         // Convert from PopupWindow local coordinates to
    134         // parent view local coordinates prior to forwarding.
    135         delegate.getParent().getLocationOnScreen(mTempScreenCoords);
    136         final float offsetX = event.getRawX() - event.getX() - mTempScreenCoords[0];
    137         final float offsetY = event.getRawY() - event.getY() - mTempScreenCoords[1];
    138         final MotionEvent offsetEvent = MotionEvent.obtainNoHistory(event);
    139         offsetEvent.offsetLocation(offsetX, offsetY);
    140         final boolean handled = delegate.onTouchHandleEvent(offsetEvent);
    141         offsetEvent.recycle();
    142         return handled;
    143     }
    144 
    145     private void setOrientation(int orientation) {
    146         assert orientation >= LEFT && orientation <= RIGHT;
    147         if (mOrientation == orientation) return;
    148 
    149         final boolean hadValidOrientation = mOrientation != -1;
    150         mOrientation = orientation;
    151 
    152         final int oldAdjustedPositionX = getAdjustedPositionX();
    153         final int oldAdjustedPositionY = getAdjustedPositionY();
    154 
    155         switch (orientation) {
    156             case LEFT: {
    157                 mDrawable = HandleViewResources.getLeftHandleDrawable(mContext);
    158                 mHotspotX = (mDrawable.getIntrinsicWidth() * 3) / 4f;
    159                 break;
    160             }
    161 
    162             case RIGHT: {
    163                 mDrawable = HandleViewResources.getRightHandleDrawable(mContext);
    164                 mHotspotX = mDrawable.getIntrinsicWidth() / 4f;
    165                 break;
    166             }
    167 
    168             case CENTER:
    169             default: {
    170                 mDrawable = HandleViewResources.getCenterHandleDrawable(mContext);
    171                 mHotspotX = mDrawable.getIntrinsicWidth() / 2f;
    172                 break;
    173             }
    174         }
    175         mHotspotY = 0;
    176 
    177         // Force handle repositioning to accommodate the new orientation's hotspot.
    178         if (hadValidOrientation) setFocus(oldAdjustedPositionX, oldAdjustedPositionY);
    179         mDrawable.setAlpha((int) (255 * mAlpha));
    180         scheduleInvalidate();
    181     }
    182 
    183     private void updateParentPosition(int parentPositionX, int parentPositionY) {
    184         if (mParentPositionX == parentPositionX && mParentPositionY == parentPositionY) return;
    185         mParentPositionX = parentPositionX;
    186         mParentPositionY = parentPositionY;
    187         temporarilyHide();
    188     }
    189 
    190     private int getContainerPositionX() {
    191         return mParentPositionX + mPositionX;
    192     }
    193 
    194     private int getContainerPositionY() {
    195         return mParentPositionY + mPositionY;
    196     }
    197 
    198     private void updatePosition() {
    199         mContainer.update(getContainerPositionX(), getContainerPositionY(),
    200                 getRight() - getLeft(), getBottom() - getTop());
    201     }
    202 
    203     private void updateVisibility() {
    204         boolean visible = mVisible && !mTemporarilyHidden;
    205         setVisibility(visible ? VISIBLE : INVISIBLE);
    206     }
    207 
    208      private void updateAlpha() {
    209         if (mAlpha == 1.f) return;
    210         long currentTimeMillis = AnimationUtils.currentAnimationTimeMillis();
    211         mAlpha = Math.min(1.f, (float) (currentTimeMillis - mFadeStartTime) / FADE_IN_DURATION_MS);
    212         mDrawable.setAlpha((int) (255 * mAlpha));
    213         scheduleInvalidate();
    214     }
    215 
    216     private void temporarilyHide() {
    217         mTemporarilyHidden = true;
    218         updateVisibility();
    219         rescheduleFadeIn();
    220     }
    221 
    222     private void doInvalidate() {
    223         if (!mContainer.isShowing()) return;
    224         updatePosition();
    225         updateVisibility();
    226         invalidate();
    227     }
    228 
    229     private void scheduleInvalidate() {
    230         if (mInvalidationRunnable == null) {
    231             mInvalidationRunnable = new Runnable() {
    232                 @Override
    233                 public void run() {
    234                     mHasPendingInvalidate = false;
    235                     doInvalidate();
    236                 }
    237             };
    238         }
    239 
    240         if (mHasPendingInvalidate) return;
    241         mHasPendingInvalidate = true;
    242         ApiCompatibilityUtils.postOnAnimation(this, mInvalidationRunnable);
    243     }
    244 
    245     private void rescheduleFadeIn() {
    246         if (mDeferredHandleFadeInRunnable == null) {
    247             mDeferredHandleFadeInRunnable = new Runnable() {
    248                 @Override
    249                 public void run() {
    250                     if (isScrollInProgress()) {
    251                         rescheduleFadeIn();
    252                         return;
    253                     }
    254                     mTemporarilyHidden = false;
    255                     beginFadeIn();
    256                 }
    257             };
    258         }
    259 
    260         removeCallbacks(mDeferredHandleFadeInRunnable);
    261         ApiCompatibilityUtils.postOnAnimationDelayed(
    262                 this, mDeferredHandleFadeInRunnable, FADE_IN_DELAY_MS);
    263     }
    264 
    265     private void beginFadeIn() {
    266         if (getVisibility() == VISIBLE) return;
    267         mAlpha = 0.f;
    268         mFadeStartTime = AnimationUtils.currentAnimationTimeMillis();
    269         doInvalidate();
    270     }
    271 
    272     @Override
    273     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    274         if (mDrawable == null) {
    275             setMeasuredDimension(0, 0);
    276             return;
    277         }
    278         setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
    279     }
    280 
    281     @Override
    282     protected void onDraw(Canvas c) {
    283         if (mDrawable == null) return;
    284         updateAlpha();
    285         mDrawable.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop());
    286         mDrawable.draw(c);
    287     }
    288 
    289     // Returns the x coordinate of the position that the handle appears to be pointing to relative
    290     // to the handles "parent" view.
    291     private int getAdjustedPositionX() {
    292         return mPositionX + Math.round(mHotspotX);
    293     }
    294 
    295     // Returns the y coordinate of the position that the handle appears to be pointing to relative
    296     // to the handles "parent" view.
    297     private int getAdjustedPositionY() {
    298         return mPositionY + Math.round(mHotspotY);
    299     }
    300 
    301     private boolean isScrollInProgress() {
    302         final PopupTouchHandleDrawableDelegate delegate = mDelegate.get();
    303         if (delegate == null) {
    304             hide();
    305             return false;
    306         }
    307 
    308         return delegate.isScrollInProgress();
    309     }
    310 
    311     @CalledByNative
    312     private void show() {
    313         if (mContainer.isShowing()) return;
    314 
    315         final PopupTouchHandleDrawableDelegate delegate = mDelegate.get();
    316         if (delegate == null) {
    317             hide();
    318             return;
    319         }
    320 
    321         mParentPositionObserver = delegate.getParentPositionObserver();
    322         assert mParentPositionObserver != null;
    323 
    324         // While hidden, the parent position may have become stale. It must be updated before
    325         // checking isPositionVisible().
    326         updateParentPosition(mParentPositionObserver.getPositionX(),
    327                 mParentPositionObserver.getPositionY());
    328         mParentPositionObserver.addListener(mParentPositionListener);
    329         mContainer.setContentView(this);
    330         mContainer.showAtLocation(delegate.getParent(), 0,
    331                 getContainerPositionX(), getContainerPositionY());
    332     }
    333 
    334     @CalledByNative
    335     private void hide() {
    336         mTemporarilyHidden = false;
    337         mContainer.dismiss();
    338         if (mParentPositionObserver != null) {
    339             mParentPositionObserver.removeListener(mParentPositionListener);
    340             // Clear the strong reference to allow garbage collection.
    341             mParentPositionObserver = null;
    342         }
    343     }
    344 
    345     @CalledByNative
    346     private void setRightOrientation() {
    347         setOrientation(RIGHT);
    348     }
    349 
    350     @CalledByNative
    351     private void setLeftOrientation() {
    352         setOrientation(LEFT);
    353     }
    354 
    355     @CalledByNative
    356     private void setCenterOrientation() {
    357         setOrientation(CENTER);
    358     }
    359 
    360     @CalledByNative
    361     private void setOpacity(float alpha) {
    362         // Ignore opacity updates from the caller as they are not compatible
    363         // with the custom fade animation.
    364     }
    365 
    366     @CalledByNative
    367     private void setFocus(float focusX, float focusY) {
    368         int x = (int) focusX - Math.round(mHotspotX);
    369         int y = (int) focusY - Math.round(mHotspotY);
    370         if (mPositionX == x && mPositionY == y) return;
    371         mPositionX = x;
    372         mPositionY = y;
    373         if (isScrollInProgress()) {
    374             temporarilyHide();
    375         } else {
    376             scheduleInvalidate();
    377         }
    378     }
    379 
    380     @CalledByNative
    381     private void setVisible(boolean visible) {
    382         mVisible = visible;
    383         int visibility = visible ? VISIBLE : INVISIBLE;
    384         if (getVisibility() == visibility) return;
    385         scheduleInvalidate();
    386     }
    387 
    388     @CalledByNative
    389     private boolean intersectsWith(float x, float y, float width, float height) {
    390         if (mDrawable == null) return false;
    391         final int drawableWidth = mDrawable.getIntrinsicWidth();
    392         final int drawableHeight = mDrawable.getIntrinsicHeight();
    393         return !(x >= mPositionX + drawableWidth
    394                 || y >= mPositionY + drawableHeight
    395                 || x + width <= mPositionX
    396                 || y + height <= mPositionY);
    397     }
    398 }
    399