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