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