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.
5 package org.chromium.content.browser.input;
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;
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;
20 import java.lang.ref.WeakReference;
23 * View that displays a selection or insertion handle for text editing.
25 * While a HandleView is logically a child of some other view, it does not exist in that View's
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;
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;
41 // The observer reference will only be non-null while it is attached to mParentPositionListener.
42 private PositionObserver mParentPositionObserver;
44 // The position of the handle relative to the parent view.
45 private int mPositionX;
46 private int mPositionY;
48 // The position of the parent relative to the application's root view.
49 private int mParentPositionX;
50 private int mParentPositionY;
52 // The offset from this handles position to the "tip" of the handle.
53 private float mHotspotX;
54 private float mHotspotY;
58 private final int[] mTempScreenCoords = new int[2];
60 static final int LEFT = 0;
61 static final int CENTER = 1;
62 static final int RIGHT = 2;
63 private int mOrientation = -1;
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;
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;
79 * Provides additional interaction behaviors necessary for handle
80 * manipulation and interaction.
82 public interface PopupTouchHandleDrawableDelegate {
84 * @return The parent View of the PopupWindow.
89 * @return A position observer for the parent View, used to keep the
90 * absolutely positioned PopupWindow in-sync with the parent.
92 PositionObserver getParentPositionObserver();
95 * Should route MotionEvents to the appropriate logic layer for
96 * performing handle manipulation.
98 boolean onTouchHandleEvent(MotionEvent ev);
101 * @return Whether the associated content is actively scrolling.
103 boolean isScrollInProgress();
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);
115 mVisible = getVisibility() == VISIBLE;
116 mParentPositionListener = new PositionObserver.Listener() {
118 public void onPositionChanged(int x, int y) {
119 updateParentPosition(x, y);
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.
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();
145 private void setOrientation(int orientation) {
146 assert orientation >= LEFT && orientation <= RIGHT;
147 if (mOrientation == orientation) return;
149 final boolean hadValidOrientation = mOrientation != -1;
150 mOrientation = orientation;
152 final int oldAdjustedPositionX = getAdjustedPositionX();
153 final int oldAdjustedPositionY = getAdjustedPositionY();
155 switch (orientation) {
157 mDrawable = HandleViewResources.getLeftHandleDrawable(mContext);
158 mHotspotX = (mDrawable.getIntrinsicWidth() * 3) / 4f;
163 mDrawable = HandleViewResources.getRightHandleDrawable(mContext);
164 mHotspotX = mDrawable.getIntrinsicWidth() / 4f;
170 mDrawable = HandleViewResources.getCenterHandleDrawable(mContext);
171 mHotspotX = mDrawable.getIntrinsicWidth() / 2f;
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();
183 private void updateParentPosition(int parentPositionX, int parentPositionY) {
184 if (mParentPositionX == parentPositionX && mParentPositionY == parentPositionY) return;
185 mParentPositionX = parentPositionX;
186 mParentPositionY = parentPositionY;
190 private int getContainerPositionX() {
191 return mParentPositionX + mPositionX;
194 private int getContainerPositionY() {
195 return mParentPositionY + mPositionY;
198 private void updatePosition() {
199 mContainer.update(getContainerPositionX(), getContainerPositionY(),
200 getRight() - getLeft(), getBottom() - getTop());
203 private void updateVisibility() {
204 boolean visible = mVisible && !mTemporarilyHidden;
205 setVisibility(visible ? VISIBLE : INVISIBLE);
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();
216 private void temporarilyHide() {
217 mTemporarilyHidden = true;
222 private void doInvalidate() {
228 private void scheduleInvalidate() {
229 if (mInvalidationRunnable == null) {
230 mInvalidationRunnable = new Runnable() {
233 mHasPendingInvalidate = false;
239 if (mHasPendingInvalidate) return;
240 mHasPendingInvalidate = true;
241 ApiCompatibilityUtils.postOnAnimation(this, mInvalidationRunnable);
244 private void rescheduleFadeIn() {
245 if (mDeferredHandleFadeInRunnable == null) {
246 mDeferredHandleFadeInRunnable = new Runnable() {
249 if (isScrollInProgress()) {
253 mTemporarilyHidden = false;
259 removeCallbacks(mDeferredHandleFadeInRunnable);
260 ApiCompatibilityUtils.postOnAnimationDelayed(
261 this, mDeferredHandleFadeInRunnable, FADE_IN_DELAY_MS);
264 private void beginFadeIn() {
265 if (getVisibility() == VISIBLE) return;
267 mFadeStartTime = AnimationUtils.currentAnimationTimeMillis();
272 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
273 if (mDrawable == null) {
274 setMeasuredDimension(0, 0);
277 setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
281 protected void onDraw(Canvas c) {
282 if (mDrawable == null) return;
284 mDrawable.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop());
288 // Returns the x coordinate of the position that the handle appears to be pointing to relative
289 // to the handles "parent" view.
290 private int getAdjustedPositionX() {
291 return mPositionX + Math.round(mHotspotX);
294 // Returns the y coordinate of the position that the handle appears to be pointing to relative
295 // to the handles "parent" view.
296 private int getAdjustedPositionY() {
297 return mPositionY + Math.round(mHotspotY);
300 private boolean isScrollInProgress() {
301 final PopupTouchHandleDrawableDelegate delegate = mDelegate.get();
302 if (delegate == null) {
307 return delegate.isScrollInProgress();
311 private void show() {
312 if (mContainer.isShowing()) return;
314 final PopupTouchHandleDrawableDelegate delegate = mDelegate.get();
315 if (delegate == null) {
320 mParentPositionObserver = delegate.getParentPositionObserver();
321 assert mParentPositionObserver != null;
323 // While hidden, the parent position may have become stale. It must be updated before
324 // checking isPositionVisible().
325 updateParentPosition(mParentPositionObserver.getPositionX(),
326 mParentPositionObserver.getPositionY());
327 mParentPositionObserver.addListener(mParentPositionListener);
328 mContainer.setContentView(this);
329 mContainer.showAtLocation(delegate.getParent(), 0,
330 getContainerPositionX(), getContainerPositionY());
334 private void hide() {
335 mTemporarilyHidden = false;
336 mContainer.dismiss();
337 if (mParentPositionObserver != null) {
338 mParentPositionObserver.removeListener(mParentPositionListener);
339 // Clear the strong reference to allow garbage collection.
340 mParentPositionObserver = null;
345 private void setRightOrientation() {
346 setOrientation(RIGHT);
350 private void setLeftOrientation() {
351 setOrientation(LEFT);
355 private void setCenterOrientation() {
356 setOrientation(CENTER);
360 private void setOpacity(float alpha) {
361 // Ignore opacity updates from the caller as they are not compatible
362 // with the custom fade animation.
366 private void setFocus(float focusX, float focusY) {
367 int x = (int) focusX - Math.round(mHotspotX);
368 int y = (int) focusY - Math.round(mHotspotY);
369 if (mPositionX == x && mPositionY == y) return;
372 if (isScrollInProgress()) {
375 scheduleInvalidate();
380 private void setVisible(boolean visible) {
382 int visibility = visible ? VISIBLE : INVISIBLE;
383 if (getVisibility() == visibility) return;
384 scheduleInvalidate();
388 private boolean intersectsWith(float x, float y, float width, float height) {
389 if (mDrawable == null) return false;
390 final int drawableWidth = mDrawable.getIntrinsicWidth();
391 final int drawableHeight = mDrawable.getIntrinsicHeight();
392 return !(x >= mPositionX + drawableWidth
393 || y >= mPositionY + drawableHeight
394 || x + width <= mPositionX
395 || y + height <= mPositionY);