| 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 | /** |
| 28 | * View that displays a selection or insertion handle for text editing. |
| 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 | private int mPositionX; |
| 36 | private int mPositionY; |
| 37 | private final CursorController mController; |
| 38 | private boolean mIsDragging; |
| 39 | private float mTouchToWindowOffsetX; |
| 40 | private float mTouchToWindowOffsetY; |
| 41 | private float mHotspotX; |
| 42 | private float mHotspotY; |
| 43 | private int mLineOffsetY; |
| 44 | private int mLastParentX; |
| 45 | private int mLastParentY; |
| 46 | private float mDownPositionX, mDownPositionY; |
| 47 | private int mContainerPositionX, mContainerPositionY; |
| 48 | private long mTouchTimer; |
| 49 | private boolean mIsInsertionHandle = false; |
| 50 | private float mAlpha; |
| 51 | private long mFadeStartTime; |
| 52 | |
| 53 | private View mParent; |
| 54 | private InsertionHandleController.PastePopupMenu mPastePopupWindow; |
| 55 | |
| 56 | private final int mTextSelectHandleLeftRes; |
| 57 | private final int mTextSelectHandleRightRes; |
| 58 | private final int mTextSelectHandleRes; |
| 59 | |
| 60 | private Drawable mSelectHandleLeft; |
| 61 | private Drawable mSelectHandleRight; |
| 62 | private Drawable mSelectHandleCenter; |
| 63 | |
| 64 | private final int[] mTempCoords = new int[2]; |
| 65 | private final Rect mTempRect = new Rect(); |
| 66 | |
| 67 | static final int LEFT = 0; |
| 68 | static final int CENTER = 1; |
| 69 | static final int RIGHT = 2; |
| 70 | |
| 71 | // Number of dips to subtract from the handle's y position to give a suitable |
| 72 | // y coordinate for the corresponding text position. This is to compensate for the fact |
| 73 | // that the handle position is at the base of the line of text. |
| 74 | private static final float LINE_OFFSET_Y_DIP = 5.0f; |
| 75 | |
| 76 | private static final int[] TEXT_VIEW_HANDLE_ATTRS = { |
| 77 | android.R.attr.textSelectHandleLeft, |
| 78 | android.R.attr.textSelectHandle, |
| 79 | android.R.attr.textSelectHandleRight, |
| 80 | }; |
| 81 | |
| 82 | HandleView(CursorController controller, int pos, View parent) { |
| 83 | super(parent.getContext()); |
| 84 | Context context = parent.getContext(); |
| 85 | mParent = parent; |
| 86 | mController = controller; |
| 87 | mContainer = new PopupWindow(context, null, android.R.attr.textSelectHandleWindowStyle); |
| 88 | mContainer.setSplitTouchEnabled(true); |
| 89 | mContainer.setClippingEnabled(false); |
| 90 | |
| 91 | TypedArray a = context.obtainStyledAttributes(TEXT_VIEW_HANDLE_ATTRS); |
| 92 | mTextSelectHandleLeftRes = a.getResourceId(a.getIndex(LEFT), 0); |
| 93 | mTextSelectHandleRes = a.getResourceId(a.getIndex(CENTER), 0); |
| 94 | mTextSelectHandleRightRes = a.getResourceId(a.getIndex(RIGHT), 0); |
| 95 | a.recycle(); |
| 96 | |
| 97 | setOrientation(pos); |
| 98 | |
| 99 | // Convert line offset dips to pixels. |
| 100 | mLineOffsetY = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, |
| 101 | LINE_OFFSET_Y_DIP, context.getResources().getDisplayMetrics()); |
| 102 | |
| 103 | mAlpha = 1.f; |
| 104 | } |
| 105 | |
| 106 | void setOrientation(int pos) { |
| 107 | int handleWidth; |
| 108 | switch (pos) { |
| 109 | case LEFT: { |
| 110 | if (mSelectHandleLeft == null) { |
| 111 | mSelectHandleLeft = getContext().getResources().getDrawable( |
| 112 | mTextSelectHandleLeftRes); |
| 113 | } |
| 114 | mDrawable = mSelectHandleLeft; |
| 115 | handleWidth = mDrawable.getIntrinsicWidth(); |
| 116 | mHotspotX = (handleWidth * 3) / 4f; |
| 117 | break; |
| 118 | } |
| 119 | |
| 120 | case RIGHT: { |
| 121 | if (mSelectHandleRight == null) { |
| 122 | mSelectHandleRight = getContext().getResources().getDrawable( |
| 123 | mTextSelectHandleRightRes); |
| 124 | } |
| 125 | mDrawable = mSelectHandleRight; |
| 126 | handleWidth = mDrawable.getIntrinsicWidth(); |
| 127 | mHotspotX = handleWidth / 4f; |
| 128 | break; |
| 129 | } |
| 130 | |
| 131 | case CENTER: |
| 132 | default: { |
| 133 | if (mSelectHandleCenter == null) { |
| 134 | mSelectHandleCenter = getContext().getResources().getDrawable( |
| 135 | mTextSelectHandleRes); |
| 136 | } |
| 137 | mDrawable = mSelectHandleCenter; |
| 138 | handleWidth = mDrawable.getIntrinsicWidth(); |
| 139 | mHotspotX = handleWidth / 2f; |
| 140 | mIsInsertionHandle = true; |
| 141 | break; |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | mHotspotY = 0; |
| 146 | invalidate(); |
| 147 | } |
| 148 | |
| 149 | @Override |
| 150 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| 151 | setMeasuredDimension(mDrawable.getIntrinsicWidth(), |
| 152 | mDrawable.getIntrinsicHeight()); |
| 153 | } |
| 154 | |
| 155 | void show() { |
| 156 | if (!isPositionVisible()) { |
| 157 | hide(); |
| 158 | return; |
| 159 | } |
| 160 | mContainer.setContentView(this); |
| 161 | final int[] coords = mTempCoords; |
| 162 | mParent.getLocationInWindow(coords); |
| 163 | mContainerPositionX = coords[0] + mPositionX; |
| 164 | mContainerPositionY = coords[1] + mPositionY; |
| 165 | mContainer.showAtLocation(mParent, 0, mContainerPositionX, mContainerPositionY); |
| 166 | |
| 167 | // Hide paste view when handle is moved on screen. |
| 168 | if (mPastePopupWindow != null) { |
| 169 | mPastePopupWindow.hide(); |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | void hide() { |
| 174 | mIsDragging = false; |
| 175 | mContainer.dismiss(); |
| 176 | if (mPastePopupWindow != null) { |
| 177 | mPastePopupWindow.hide(); |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | boolean isShowing() { |
| 182 | return mContainer.isShowing(); |
| 183 | } |
| 184 | |
| 185 | private boolean isPositionVisible() { |
| 186 | // Always show a dragging handle. |
| 187 | if (mIsDragging) { |
| 188 | return true; |
| 189 | } |
| 190 | |
| 191 | final Rect clip = mTempRect; |
| 192 | clip.left = 0; |
| 193 | clip.top = 0; |
| 194 | clip.right = mParent.getWidth(); |
| 195 | clip.bottom = mParent.getHeight(); |
| 196 | |
| 197 | final ViewParent parent = mParent.getParent(); |
| 198 | if (parent == null || !parent.getChildVisibleRect(mParent, clip, null)) { |
| 199 | return false; |
| 200 | } |
| 201 | |
| 202 | final int[] coords = mTempCoords; |
| 203 | mParent.getLocationInWindow(coords); |
| 204 | final int posX = coords[0] + mPositionX + (int) mHotspotX; |
| 205 | final int posY = coords[1] + mPositionY + (int) mHotspotY; |
| 206 | |
| 207 | return posX >= clip.left && posX <= clip.right && |
| 208 | posY >= clip.top && posY <= clip.bottom; |
| 209 | } |
| 210 | |
| 211 | // x and y are in physical pixels. |
| 212 | void moveTo(int x, int y) { |
| 213 | mPositionX = x; |
| 214 | mPositionY = y; |
| 215 | if (isPositionVisible()) { |
| 216 | int[] coords = null; |
| 217 | if (mContainer.isShowing()) { |
| 218 | coords = mTempCoords; |
| 219 | mParent.getLocationInWindow(coords); |
| 220 | final int containerPositionX = coords[0] + mPositionX; |
| 221 | final int containerPositionY = coords[1] + mPositionY; |
| 222 | |
| 223 | if (containerPositionX != mContainerPositionX || |
| 224 | containerPositionY != mContainerPositionY) { |
| 225 | mContainerPositionX = containerPositionX; |
| 226 | mContainerPositionY = containerPositionY; |
| 227 | |
| 228 | mContainer.update(mContainerPositionX, mContainerPositionY, |
| 229 | getRight() - getLeft(), getBottom() - getTop()); |
| 230 | |
| 231 | // Hide paste popup window as soon as a scroll occurs. |
| 232 | if (mPastePopupWindow != null) { |
| 233 | mPastePopupWindow.hide(); |
| 234 | } |
| 235 | } |
| 236 | } else { |
| 237 | show(); |
| 238 | } |
| 239 | |
| 240 | if (mIsDragging) { |
| 241 | if (coords == null) { |
| 242 | coords = mTempCoords; |
| 243 | mParent.getLocationInWindow(coords); |
| 244 | } |
| 245 | if (coords[0] != mLastParentX || coords[1] != mLastParentY) { |
| 246 | mTouchToWindowOffsetX += coords[0] - mLastParentX; |
| 247 | mTouchToWindowOffsetY += coords[1] - mLastParentY; |
| 248 | mLastParentX = coords[0]; |
| 249 | mLastParentY = coords[1]; |
| 250 | } |
| 251 | // Hide paste popup window as soon as the handle is dragged. |
| 252 | if (mPastePopupWindow != null) { |
| 253 | mPastePopupWindow.hide(); |
| 254 | } |
| 255 | } |
| 256 | } else { |
| 257 | hide(); |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | @Override |
| 262 | protected void onDraw(Canvas c) { |
| 263 | updateAlpha(); |
| 264 | mDrawable.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop()); |
| 265 | mDrawable.draw(c); |
| 266 | } |
| 267 | |
| 268 | @Override |
| 269 | public boolean onTouchEvent(MotionEvent ev) { |
| 270 | switch (ev.getActionMasked()) { |
| 271 | case MotionEvent.ACTION_DOWN: { |
| 272 | mDownPositionX = ev.getRawX(); |
| 273 | mDownPositionY = ev.getRawY(); |
| 274 | mTouchToWindowOffsetX = mDownPositionX - mPositionX; |
| 275 | mTouchToWindowOffsetY = mDownPositionY - mPositionY; |
| 276 | final int[] coords = mTempCoords; |
| 277 | mParent.getLocationInWindow(coords); |
| 278 | mLastParentX = coords[0]; |
| 279 | mLastParentY = coords[1]; |
| 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((int)(x - mHotspotX), (int)(y - mHotspotY)); |
| 344 | } |
| 345 | |
| 346 | // Returns the x coordinate of the position that the handle appears to be pointing to. |
| 347 | int getAdjustedPositionX() { |
| 348 | return (int) (mPositionX + mHotspotX); |
| 349 | } |
| 350 | |
| 351 | // Returns the y coordinate of the position that the handle appears to be pointing to. |
| 352 | int getAdjustedPositionY() { |
| 353 | return (int) (mPositionY + mHotspotY); |
| 354 | } |
| 355 | |
| 356 | // Returns a suitable y coordinate for the text position corresponding to the handle. |
| 357 | // As the handle points to a position on the base of the line of text, this method |
| 358 | // returns a coordinate a small number of pixels higher (i.e. a slightly smaller number) |
| 359 | // than getAdjustedPositionY. |
| 360 | int getLineAdjustedPositionY() { |
| 361 | return (int) (mPositionY + mHotspotY - mLineOffsetY); |
| 362 | } |
| 363 | |
| 364 | Drawable getDrawable() { |
| 365 | return mDrawable; |
| 366 | } |
| 367 | |
| 368 | private void updateAlpha() { |
| 369 | if (mAlpha == 1.f) return; |
| 370 | mAlpha = Math.min(1.f, (System.currentTimeMillis() - mFadeStartTime) / FADE_DURATION); |
| 371 | mDrawable.setAlpha((int) (255 * mAlpha)); |
| 372 | invalidate(); |
| 373 | } |
| 374 | |
| 375 | /** |
| 376 | * If the handle is not visible, sets its visibility to View.VISIBLE and begins fading it in. |
| 377 | */ |
| 378 | void beginFadeIn() { |
| 379 | if (getVisibility() == VISIBLE) return; |
| 380 | mAlpha = 0.f; |
| 381 | mFadeStartTime = System.currentTimeMillis(); |
| 382 | setVisibility(VISIBLE); |
| 383 | } |
| 384 | |
| 385 | void showPastePopupWindow() { |
| 386 | InsertionHandleController ihc = (InsertionHandleController) mController; |
| 387 | if (mIsInsertionHandle && ihc.canPaste()) { |
| 388 | if (mPastePopupWindow == null) { |
| 389 | // Lazy initialization: create when actually shown only. |
| 390 | mPastePopupWindow = ihc.new PastePopupMenu(); |
| 391 | } |
| 392 | mPastePopupWindow.show(); |
| 393 | } |
| 394 | } |
| 395 | } |