1 // Copyright 2014 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.graphics.Canvas; 9 import android.graphics.drawable.Drawable; 10 import android.view.MotionEvent; 11 import android.view.View; 12 import android.view.animation.AnimationUtils; 13 import android.widget.PopupWindow; 14 15 import org.chromium.base.ApiCompatibilityUtils; 16 import org.chromium.base.CalledByNative; 17 import org.chromium.base.JNINamespace; 18 import org.chromium.content.browser.PositionObserver; 19 20 import java.lang.ref.WeakReference; 21 22 /** 23 * View that displays a selection or insertion handle for text editing. 24 * 25 * While a HandleView is logically a child of some other view, it does not exist in that View's 26 * hierarchy. 27 * 28 */ 29 @JNINamespace("content") 30 public class PopupTouchHandleDrawable extends View { 31 private Drawable mDrawable; 32 private final PopupWindow mContainer; 33 private final Context mContext; 34 private final PositionObserver.Listener mParentPositionListener; 35 36 // The weak delegate reference allows the PopupTouchHandleDrawable to be owned by a native 37 // object that might have a different lifetime (or a cyclic lifetime) with respect to the 38 // delegate, allowing garbage collection of any Java references. 39 private final WeakReference<PopupTouchHandleDrawableDelegate> mDelegate; 40 41 // The observer reference will only be non-null while it is attached to mParentPositionListener. 42 private PositionObserver mParentPositionObserver; 43 44 // The position of the handle relative to the parent view. 45 private int mPositionX; 46 private int mPositionY; 47 48 // The position of the parent relative to the application's root view. 49 private int mParentPositionX; 50 private int mParentPositionY; 51 52 // The offset from this handles position to the "tip" of the handle. 53 private float mHotspotX; 54 private float mHotspotY; 55 56 private float mAlpha; 57 58 private final int[] mTempScreenCoords = new int[2]; 59 60 static final int LEFT = 0; 61 static final int CENTER = 1; 62 static final int RIGHT = 2; 63 private int mOrientation = -1; 64 65 // Length of the delay before fading in after the last page movement. 66 private static final int FADE_IN_DELAY_MS = 300; 67 private static final int FADE_IN_DURATION_MS = 200; 68 private Runnable mDeferredHandleFadeInRunnable; 69 private long mFadeStartTime; 70 private boolean mVisible; 71 private boolean mTemporarilyHidden; 72 73 // Deferred runnable to avoid invalidating outside of frame dispatch, 74 // in turn avoiding issues with sync barrier insertion. 75 private Runnable mInvalidationRunnable; 76 private boolean mHasPendingInvalidate; 77 78 /** 79 * Provides additional interaction behaviors necessary for handle 80 * manipulation and interaction. 81 */ 82 public interface PopupTouchHandleDrawableDelegate { 83 /** 84 * @return The parent View of the PopupWindow. 85 */ getParent()86 View getParent(); 87 88 /** 89 * @return A position observer for the parent View, used to keep the 90 * absolutely positioned PopupWindow in-sync with the parent. 91 */ getParentPositionObserver()92 PositionObserver getParentPositionObserver(); 93 94 /** 95 * Should route MotionEvents to the appropriate logic layer for 96 * performing handle manipulation. 97 */ onTouchHandleEvent(MotionEvent ev)98 boolean onTouchHandleEvent(MotionEvent ev); 99 100 /** 101 * @return Whether the associated content is actively scrolling. 102 */ isScrollInProgress()103 boolean isScrollInProgress(); 104 } 105 PopupTouchHandleDrawable(PopupTouchHandleDrawableDelegate delegate)106 public PopupTouchHandleDrawable(PopupTouchHandleDrawableDelegate delegate) { 107 super(delegate.getParent().getContext()); 108 mContext = delegate.getParent().getContext(); 109 mDelegate = new WeakReference<PopupTouchHandleDrawableDelegate>(delegate); 110 mContainer = new PopupWindow(mContext, null, android.R.attr.textSelectHandleWindowStyle); 111 mContainer.setSplitTouchEnabled(true); 112 mContainer.setClippingEnabled(false); 113 mContainer.setAnimationStyle(0); 114 mAlpha = 1.f; 115 mVisible = getVisibility() == VISIBLE; 116 mParentPositionListener = new PositionObserver.Listener() { 117 @Override 118 public void onPositionChanged(int x, int y) { 119 updateParentPosition(x, y); 120 } 121 }; 122 } 123 124 @Override onTouchEvent(MotionEvent event)125 public boolean onTouchEvent(MotionEvent event) { 126 final PopupTouchHandleDrawableDelegate delegate = mDelegate.get(); 127 if (delegate == null) { 128 // If the delegate is gone, we should immediately dispose of the popup. 129 hide(); 130 return false; 131 } 132 133 // Convert from PopupWindow local coordinates to 134 // parent view local coordinates prior to forwarding. 135 delegate.getParent().getLocationOnScreen(mTempScreenCoords); 136 final float offsetX = event.getRawX() - event.getX() - mTempScreenCoords[0]; 137 final float offsetY = event.getRawY() - event.getY() - mTempScreenCoords[1]; 138 final MotionEvent offsetEvent = MotionEvent.obtainNoHistory(event); 139 offsetEvent.offsetLocation(offsetX, offsetY); 140 final boolean handled = delegate.onTouchHandleEvent(offsetEvent); 141 offsetEvent.recycle(); 142 return handled; 143 } 144 setOrientation(int orientation)145 private void setOrientation(int orientation) { 146 assert orientation >= LEFT && orientation <= RIGHT; 147 if (mOrientation == orientation) return; 148 149 final boolean hadValidOrientation = mOrientation != -1; 150 mOrientation = orientation; 151 152 final int oldAdjustedPositionX = getAdjustedPositionX(); 153 final int oldAdjustedPositionY = getAdjustedPositionY(); 154 155 switch (orientation) { 156 case LEFT: { 157 mDrawable = HandleViewResources.getLeftHandleDrawable(mContext); 158 mHotspotX = (mDrawable.getIntrinsicWidth() * 3) / 4f; 159 break; 160 } 161 162 case RIGHT: { 163 mDrawable = HandleViewResources.getRightHandleDrawable(mContext); 164 mHotspotX = mDrawable.getIntrinsicWidth() / 4f; 165 break; 166 } 167 168 case CENTER: 169 default: { 170 mDrawable = HandleViewResources.getCenterHandleDrawable(mContext); 171 mHotspotX = mDrawable.getIntrinsicWidth() / 2f; 172 break; 173 } 174 } 175 mHotspotY = 0; 176 177 // Force handle repositioning to accommodate the new orientation's hotspot. 178 if (hadValidOrientation) setFocus(oldAdjustedPositionX, oldAdjustedPositionY); 179 mDrawable.setAlpha((int) (255 * mAlpha)); 180 scheduleInvalidate(); 181 } 182 updateParentPosition(int parentPositionX, int parentPositionY)183 private void updateParentPosition(int parentPositionX, int parentPositionY) { 184 if (mParentPositionX == parentPositionX && mParentPositionY == parentPositionY) return; 185 mParentPositionX = parentPositionX; 186 mParentPositionY = parentPositionY; 187 temporarilyHide(); 188 } 189 getContainerPositionX()190 private int getContainerPositionX() { 191 return mParentPositionX + mPositionX; 192 } 193 getContainerPositionY()194 private int getContainerPositionY() { 195 return mParentPositionY + mPositionY; 196 } 197 updatePosition()198 private void updatePosition() { 199 mContainer.update(getContainerPositionX(), getContainerPositionY(), 200 getRight() - getLeft(), getBottom() - getTop()); 201 } 202 updateVisibility()203 private void updateVisibility() { 204 boolean visible = mVisible && !mTemporarilyHidden; 205 setVisibility(visible ? VISIBLE : INVISIBLE); 206 } 207 updateAlpha()208 private void updateAlpha() { 209 if (mAlpha == 1.f) return; 210 long currentTimeMillis = AnimationUtils.currentAnimationTimeMillis(); 211 mAlpha = Math.min(1.f, (float) (currentTimeMillis - mFadeStartTime) / FADE_IN_DURATION_MS); 212 mDrawable.setAlpha((int) (255 * mAlpha)); 213 scheduleInvalidate(); 214 } 215 temporarilyHide()216 private void temporarilyHide() { 217 mTemporarilyHidden = true; 218 updateVisibility(); 219 rescheduleFadeIn(); 220 } 221 doInvalidate()222 private void doInvalidate() { 223 if (!mContainer.isShowing()) return; 224 updatePosition(); 225 updateVisibility(); 226 invalidate(); 227 } 228 scheduleInvalidate()229 private void scheduleInvalidate() { 230 if (mInvalidationRunnable == null) { 231 mInvalidationRunnable = new Runnable() { 232 @Override 233 public void run() { 234 mHasPendingInvalidate = false; 235 doInvalidate(); 236 } 237 }; 238 } 239 240 if (mHasPendingInvalidate) return; 241 mHasPendingInvalidate = true; 242 ApiCompatibilityUtils.postOnAnimation(this, mInvalidationRunnable); 243 } 244 rescheduleFadeIn()245 private void rescheduleFadeIn() { 246 if (mDeferredHandleFadeInRunnable == null) { 247 mDeferredHandleFadeInRunnable = new Runnable() { 248 @Override 249 public void run() { 250 if (isScrollInProgress()) { 251 rescheduleFadeIn(); 252 return; 253 } 254 mTemporarilyHidden = false; 255 beginFadeIn(); 256 } 257 }; 258 } 259 260 removeCallbacks(mDeferredHandleFadeInRunnable); 261 ApiCompatibilityUtils.postOnAnimationDelayed( 262 this, mDeferredHandleFadeInRunnable, FADE_IN_DELAY_MS); 263 } 264 beginFadeIn()265 private void beginFadeIn() { 266 if (getVisibility() == VISIBLE) return; 267 mAlpha = 0.f; 268 mFadeStartTime = AnimationUtils.currentAnimationTimeMillis(); 269 doInvalidate(); 270 } 271 272 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)273 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 274 if (mDrawable == null) { 275 setMeasuredDimension(0, 0); 276 return; 277 } 278 setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()); 279 } 280 281 @Override onDraw(Canvas c)282 protected void onDraw(Canvas c) { 283 if (mDrawable == null) return; 284 updateAlpha(); 285 mDrawable.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop()); 286 mDrawable.draw(c); 287 } 288 289 // Returns the x coordinate of the position that the handle appears to be pointing to relative 290 // to the handles "parent" view. getAdjustedPositionX()291 private int getAdjustedPositionX() { 292 return mPositionX + Math.round(mHotspotX); 293 } 294 295 // Returns the y coordinate of the position that the handle appears to be pointing to relative 296 // to the handles "parent" view. getAdjustedPositionY()297 private int getAdjustedPositionY() { 298 return mPositionY + Math.round(mHotspotY); 299 } 300 isScrollInProgress()301 private boolean isScrollInProgress() { 302 final PopupTouchHandleDrawableDelegate delegate = mDelegate.get(); 303 if (delegate == null) { 304 hide(); 305 return false; 306 } 307 308 return delegate.isScrollInProgress(); 309 } 310 311 @CalledByNative show()312 private void show() { 313 if (mContainer.isShowing()) return; 314 315 final PopupTouchHandleDrawableDelegate delegate = mDelegate.get(); 316 if (delegate == null) { 317 hide(); 318 return; 319 } 320 321 mParentPositionObserver = delegate.getParentPositionObserver(); 322 assert mParentPositionObserver != null; 323 324 // While hidden, the parent position may have become stale. It must be updated before 325 // checking isPositionVisible(). 326 updateParentPosition(mParentPositionObserver.getPositionX(), 327 mParentPositionObserver.getPositionY()); 328 mParentPositionObserver.addListener(mParentPositionListener); 329 mContainer.setContentView(this); 330 mContainer.showAtLocation(delegate.getParent(), 0, 331 getContainerPositionX(), getContainerPositionY()); 332 } 333 334 @CalledByNative hide()335 private void hide() { 336 mTemporarilyHidden = false; 337 mContainer.dismiss(); 338 if (mParentPositionObserver != null) { 339 mParentPositionObserver.removeListener(mParentPositionListener); 340 // Clear the strong reference to allow garbage collection. 341 mParentPositionObserver = null; 342 } 343 } 344 345 @CalledByNative setRightOrientation()346 private void setRightOrientation() { 347 setOrientation(RIGHT); 348 } 349 350 @CalledByNative setLeftOrientation()351 private void setLeftOrientation() { 352 setOrientation(LEFT); 353 } 354 355 @CalledByNative setCenterOrientation()356 private void setCenterOrientation() { 357 setOrientation(CENTER); 358 } 359 360 @CalledByNative setOpacity(float alpha)361 private void setOpacity(float alpha) { 362 // Ignore opacity updates from the caller as they are not compatible 363 // with the custom fade animation. 364 } 365 366 @CalledByNative setFocus(float focusX, float focusY)367 private void setFocus(float focusX, float focusY) { 368 int x = (int) focusX - Math.round(mHotspotX); 369 int y = (int) focusY - Math.round(mHotspotY); 370 if (mPositionX == x && mPositionY == y) return; 371 mPositionX = x; 372 mPositionY = y; 373 if (isScrollInProgress()) { 374 temporarilyHide(); 375 } else { 376 scheduleInvalidate(); 377 } 378 } 379 380 @CalledByNative setVisible(boolean visible)381 private void setVisible(boolean visible) { 382 mVisible = visible; 383 int visibility = visible ? VISIBLE : INVISIBLE; 384 if (getVisibility() == visibility) return; 385 scheduleInvalidate(); 386 } 387 388 @CalledByNative intersectsWith(float x, float y, float width, float height)389 private boolean intersectsWith(float x, float y, float width, float height) { 390 if (mDrawable == null) return false; 391 final int drawableWidth = mDrawable.getIntrinsicWidth(); 392 final int drawableHeight = mDrawable.getIntrinsicHeight(); 393 return !(x >= mPositionX + drawableWidth 394 || y >= mPositionY + drawableHeight 395 || x + width <= mPositionX 396 || y + height <= mPositionY); 397 } 398 } 399