Home | History | Annotate | Download | only in input
      1 // Copyright 2012 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.content.res.TypedArray;
      9 import android.graphics.Canvas;
     10 import android.graphics.Rect;
     11 import android.graphics.drawable.Drawable;
     12 import android.os.SystemClock;
     13 import android.util.TypedValue;
     14 import android.view.MotionEvent;
     15 import android.view.View;
     16 import android.view.ViewConfiguration;
     17 import android.view.ViewParent;
     18 import android.view.animation.AnimationUtils;
     19 import android.widget.PopupWindow;
     20 
     21 import org.chromium.content.browser.PositionObserver;
     22 
     23 /**
     24  * View that displays a selection or insertion handle for text editing.
     25  *
     26  * While a HandleView is logically a child of some other view, it does not exist in that View's
     27  * hierarchy.
     28  *
     29  */
     30 public class HandleView extends View {
     31     private static final float FADE_DURATION = 200.f;
     32 
     33     private Drawable mDrawable;
     34     private final PopupWindow mContainer;
     35 
     36     // The position of the handle relative to the parent view.
     37     private int mPositionX;
     38     private int mPositionY;
     39 
     40     // The position of the parent relative to the application's root view.
     41     private int mParentPositionX;
     42     private int mParentPositionY;
     43 
     44     // The offset from this handles position to the "tip" of the handle.
     45     private float mHotspotX;
     46     private float mHotspotY;
     47 
     48     private final CursorController mController;
     49     private boolean mIsDragging;
     50     private float mTouchToWindowOffsetX;
     51     private float mTouchToWindowOffsetY;
     52 
     53     private final int mLineOffsetY;
     54     private float mDownPositionX, mDownPositionY;
     55     private long mTouchTimer;
     56     private boolean mIsInsertionHandle = false;
     57     private float mAlpha;
     58     private long mFadeStartTime;
     59 
     60     private final View mParent;
     61     private InsertionHandleController.PastePopupMenu mPastePopupWindow;
     62 
     63     private final Rect mTempRect = new Rect();
     64 
     65     static final int LEFT = 0;
     66     static final int CENTER = 1;
     67     static final int RIGHT = 2;
     68 
     69     private final PositionObserver mParentPositionObserver;
     70     private final PositionObserver.Listener mParentPositionListener;
     71 
     72     // Number of dips to subtract from the handle's y position to give a suitable
     73     // y coordinate for the corresponding text position. This is to compensate for the fact
     74     // that the handle position is at the base of the line of text.
     75     private static final float LINE_OFFSET_Y_DIP = 5.0f;
     76 
     77     private static final int[] TEXT_VIEW_HANDLE_ATTRS = {
     78         android.R.attr.textSelectHandleLeft,
     79         android.R.attr.textSelectHandle,
     80         android.R.attr.textSelectHandleRight,
     81     };
     82 
     83     HandleView(CursorController controller, int pos, View parent,
     84             PositionObserver parentPositionObserver) {
     85         super(parent.getContext());
     86         mParent = parent;
     87         Context context = mParent.getContext();
     88         mController = controller;
     89         mContainer = new PopupWindow(context, null, android.R.attr.textSelectHandleWindowStyle);
     90         mContainer.setSplitTouchEnabled(true);
     91         mContainer.setClippingEnabled(false);
     92         mContainer.setAnimationStyle(0);
     93 
     94         setOrientation(pos);
     95 
     96         // Convert line offset dips to pixels.
     97         mLineOffsetY = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
     98                 LINE_OFFSET_Y_DIP, context.getResources().getDisplayMetrics());
     99 
    100         mAlpha = 1.f;
    101 
    102         mParentPositionListener = new PositionObserver.Listener() {
    103             @Override
    104             public void onPositionChanged(int x, int y) {
    105                 updateParentPosition(x, y);
    106             }
    107         };
    108         mParentPositionObserver = parentPositionObserver;
    109     }
    110 
    111     void setOrientation(int pos) {
    112         Context context = mParent.getContext();
    113         TypedArray a = context.getTheme().obtainStyledAttributes(TEXT_VIEW_HANDLE_ATTRS);
    114         mDrawable = a.getDrawable(pos);
    115         a.recycle();
    116 
    117         mIsInsertionHandle = (pos == CENTER);
    118 
    119         int handleWidth = mDrawable.getIntrinsicWidth();
    120         switch (pos) {
    121             case LEFT: {
    122                 mHotspotX = (handleWidth * 3) / 4f;
    123                 break;
    124             }
    125 
    126             case RIGHT: {
    127                 mHotspotX = handleWidth / 4f;
    128                 break;
    129             }
    130 
    131             case CENTER:
    132             default: {
    133                 mHotspotX = handleWidth / 2f;
    134                 break;
    135             }
    136         }
    137         mHotspotY = 0;
    138 
    139         invalidate();
    140     }
    141 
    142     @Override
    143     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    144         setMeasuredDimension(mDrawable.getIntrinsicWidth(),
    145                 mDrawable.getIntrinsicHeight());
    146     }
    147 
    148     private void updateParentPosition(int parentPositionX, int parentPositionY) {
    149         // Hide paste popup window as soon as a scroll occurs.
    150         if (mPastePopupWindow != null) mPastePopupWindow.hide();
    151 
    152         mTouchToWindowOffsetX += parentPositionX - mParentPositionX;
    153         mTouchToWindowOffsetY += parentPositionY - mParentPositionY;
    154         mParentPositionX = parentPositionX;
    155         mParentPositionY = parentPositionY;
    156         onPositionChanged();
    157     }
    158 
    159     private int getContainerPositionX() {
    160         return mParentPositionX + mPositionX;
    161     }
    162 
    163     private int getContainerPositionY() {
    164         return mParentPositionY + mPositionY;
    165     }
    166 
    167     private void onPositionChanged() {
    168         // Deferring View invalidation while the handles are hidden prevents
    169         // scheduling conflicts with the compositor.
    170         if (getVisibility() != VISIBLE) return;
    171         mContainer.update(getContainerPositionX(), getContainerPositionY(),
    172                 getRight() - getLeft(), getBottom() - getTop());
    173     }
    174 
    175     private void showContainer() {
    176         mContainer.showAtLocation(mParent, 0, getContainerPositionX(), getContainerPositionY());
    177     }
    178 
    179     void show() {
    180         // While hidden, the parent position may have become stale. It must be updated before
    181         // checking isPositionVisible().
    182         updateParentPosition(mParentPositionObserver.getPositionX(),
    183                 mParentPositionObserver.getPositionY());
    184         if (!isPositionVisible()) {
    185             hide();
    186             return;
    187         }
    188         mParentPositionObserver.addListener(mParentPositionListener);
    189         mContainer.setContentView(this);
    190         showContainer();
    191 
    192         // Hide paste view when handle is moved on screen.
    193         if (mPastePopupWindow != null) {
    194             mPastePopupWindow.hide();
    195         }
    196     }
    197 
    198     void hide() {
    199         mIsDragging = false;
    200         mContainer.dismiss();
    201         mParentPositionObserver.removeListener(mParentPositionListener);
    202         if (mPastePopupWindow != null) {
    203             mPastePopupWindow.hide();
    204         }
    205     }
    206 
    207     boolean isShowing() {
    208         return mContainer.isShowing();
    209     }
    210 
    211     private boolean isPositionVisible() {
    212         // Always show a dragging handle.
    213         if (mIsDragging) {
    214             return true;
    215         }
    216 
    217         final Rect clip = mTempRect;
    218         clip.left = 0;
    219         clip.top = 0;
    220         clip.right = mParent.getWidth();
    221         clip.bottom = mParent.getHeight();
    222 
    223         final ViewParent parent = mParent.getParent();
    224         if (parent == null || !parent.getChildVisibleRect(mParent, clip, null)) {
    225             return false;
    226         }
    227 
    228         final int posX = getContainerPositionX() + (int) mHotspotX;
    229         final int posY = getContainerPositionY() + (int) mHotspotY;
    230 
    231         return posX >= clip.left && posX <= clip.right &&
    232                 posY >= clip.top && posY <= clip.bottom;
    233     }
    234 
    235     // x and y are in physical pixels.
    236     void moveTo(int x, int y) {
    237         int previousPositionX = mPositionX;
    238         int previousPositionY = mPositionY;
    239 
    240         mPositionX = x;
    241         mPositionY = y;
    242         if (isPositionVisible()) {
    243             if (mContainer.isShowing()) {
    244                 onPositionChanged();
    245                 // Hide paste popup window as soon as the handle is dragged.
    246                 if (mPastePopupWindow != null &&
    247                         (previousPositionX != mPositionX || previousPositionY != mPositionY)) {
    248                     mPastePopupWindow.hide();
    249                 }
    250             } else {
    251                 show();
    252             }
    253 
    254             if (mIsDragging) {
    255                 // Hide paste popup window as soon as the handle is dragged.
    256                 if (mPastePopupWindow != null) {
    257                     mPastePopupWindow.hide();
    258                 }
    259             }
    260         } else {
    261             hide();
    262         }
    263     }
    264 
    265     @Override
    266     protected void onDraw(Canvas c) {
    267         updateAlpha();
    268         mDrawable.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop());
    269         mDrawable.draw(c);
    270     }
    271 
    272     @Override
    273     public boolean onTouchEvent(MotionEvent ev) {
    274         switch (ev.getActionMasked()) {
    275             case MotionEvent.ACTION_DOWN: {
    276                 mDownPositionX = ev.getRawX();
    277                 mDownPositionY = ev.getRawY();
    278                 mTouchToWindowOffsetX = mDownPositionX - mPositionX;
    279                 mTouchToWindowOffsetY = mDownPositionY - mPositionY;
    280                 mIsDragging = true;
    281                 mController.beforeStartUpdatingPosition(this);
    282                 mTouchTimer = SystemClock.uptimeMillis();
    283                 break;
    284             }
    285 
    286             case MotionEvent.ACTION_MOVE: {
    287                 updatePosition(ev.getRawX(), ev.getRawY());
    288                 break;
    289             }
    290 
    291             case MotionEvent.ACTION_UP:
    292                 if (mIsInsertionHandle) {
    293                     long delay = SystemClock.uptimeMillis() - mTouchTimer;
    294                     if (delay < ViewConfiguration.getTapTimeout()) {
    295                         if (mPastePopupWindow != null && mPastePopupWindow.isShowing()) {
    296                             // Tapping on the handle dismisses the displayed paste view,
    297                             mPastePopupWindow.hide();
    298                         } else {
    299                             showPastePopupWindow();
    300                         }
    301                     }
    302                 }
    303                 mIsDragging = false;
    304                 break;
    305 
    306             case MotionEvent.ACTION_CANCEL:
    307                 mIsDragging = false;
    308                 break;
    309 
    310             default:
    311                 return false;
    312         }
    313         return true;
    314     }
    315 
    316     boolean isDragging() {
    317         return mIsDragging;
    318     }
    319 
    320     /**
    321      * @return Returns the x position of the handle
    322      */
    323     int getPositionX() {
    324         return mPositionX;
    325     }
    326 
    327     /**
    328      * @return Returns the y position of the handle
    329      */
    330     int getPositionY() {
    331         return mPositionY;
    332     }
    333 
    334     private void updatePosition(float rawX, float rawY) {
    335         final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
    336         final float newPosY = rawY - mTouchToWindowOffsetY + mHotspotY - mLineOffsetY;
    337 
    338         mController.updatePosition(this, Math.round(newPosX), Math.round(newPosY));
    339     }
    340 
    341     // x and y are in physical pixels.
    342     void positionAt(int x, int y) {
    343         moveTo(x - Math.round(mHotspotX), y - Math.round(mHotspotY));
    344     }
    345 
    346     // Returns the x coordinate of the position that the handle appears to be pointing to relative
    347     // to the handles "parent" view.
    348     int getAdjustedPositionX() {
    349         return mPositionX + Math.round(mHotspotX);
    350     }
    351 
    352     // Returns the y coordinate of the position that the handle appears to be pointing to relative
    353     // to the handles "parent" view.
    354     int getAdjustedPositionY() {
    355         return mPositionY + Math.round(mHotspotY);
    356     }
    357 
    358     // Returns the x coordinate of the postion that the handle appears to be pointing to relative to
    359     // the root view of the application.
    360     int getRootViewRelativePositionX() {
    361         return getContainerPositionX() + Math.round(mHotspotX);
    362     }
    363 
    364     // Returns the y coordinate of the postion that the handle appears to be pointing to relative to
    365     // the root view of the application.
    366     int getRootViewRelativePositionY() {
    367         return getContainerPositionY() + Math.round(mHotspotY);
    368     }
    369 
    370     // Returns a suitable y coordinate for the text position corresponding to the handle.
    371     // As the handle points to a position on the base of the line of text, this method
    372     // returns a coordinate a small number of pixels higher (i.e. a slightly smaller number)
    373     // than getAdjustedPositionY.
    374     int getLineAdjustedPositionY() {
    375         return (int) (mPositionY + mHotspotY - mLineOffsetY);
    376     }
    377 
    378     Drawable getDrawable() {
    379         return mDrawable;
    380     }
    381 
    382     private void updateAlpha() {
    383         if (mAlpha == 1.f) return;
    384         mAlpha = Math.min(1.f,
    385                 (AnimationUtils.currentAnimationTimeMillis() - mFadeStartTime) / FADE_DURATION);
    386         mDrawable.setAlpha((int) (255 * mAlpha));
    387         invalidate();
    388     }
    389 
    390     /**
    391      * If the handle is not visible, sets its visibility to View.VISIBLE and begins fading it in.
    392      */
    393     void beginFadeIn() {
    394         if (getVisibility() == VISIBLE) return;
    395         mAlpha = 0.f;
    396         mFadeStartTime = AnimationUtils.currentAnimationTimeMillis();
    397         setVisibility(VISIBLE);
    398         // Position updates may have been deferred while the handle was hidden.
    399         onPositionChanged();
    400     }
    401 
    402     void showPastePopupWindow() {
    403         InsertionHandleController ihc = (InsertionHandleController) mController;
    404         if (mIsInsertionHandle && ihc.canPaste()) {
    405             if (mPastePopupWindow == null) {
    406                 // Lazy initialization: create when actually shown only.
    407                 mPastePopupWindow = ihc.new PastePopupMenu();
    408             }
    409             mPastePopupWindow.show();
    410         }
    411     }
    412 }
    413