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