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.annotation.SuppressLint;
8 import android.content.Context;
9 import android.graphics.Canvas;
10 import android.graphics.drawable.Drawable;
11 import android.view.MotionEvent;
12 import android.view.View;
13 import android.view.animation.AnimationUtils;
14 import android.widget.PopupWindow;
16 import org.chromium.base.ApiCompatibilityUtils;
17 import org.chromium.base.CalledByNative;
18 import org.chromium.base.JNINamespace;
19 import org.chromium.content.browser.PositionObserver;
21 import java.lang.ref.WeakReference;
24 * View that displays a selection or insertion handle for text editing.
26 * While a HandleView is logically a child of some other view, it does not exist in that View's
30 @JNINamespace("content")
31 public class PopupTouchHandleDrawable extends View {
32 private Drawable mDrawable;
33 private final PopupWindow mContainer;
34 private final Context mContext;
35 private final PositionObserver.Listener mParentPositionListener;
37 // The weak delegate reference allows the PopupTouchHandleDrawable to be owned by a native
38 // object that might have a different lifetime (or a cyclic lifetime) with respect to the
39 // delegate, allowing garbage collection of any Java references.
40 private final WeakReference<PopupTouchHandleDrawableDelegate> mDelegate;
42 // The observer reference will only be non-null while it is attached to mParentPositionListener.
43 private PositionObserver mParentPositionObserver;
45 // The position of the handle relative to the parent view.
46 private int mPositionX;
47 private int mPositionY;
49 // The position of the parent relative to the application's root view.
50 private int mParentPositionX;
51 private int mParentPositionY;
53 // The offset from this handles position to the "tip" of the handle.
54 private float mHotspotX;
55 private float mHotspotY;
59 private final int[] mTempScreenCoords = new int[2];
61 @SuppressLint("RtlHardcoded")
62 static final int LEFT = 0;
63 static final int CENTER = 1;
64 @SuppressLint("RtlHardcoded")
65 static final int RIGHT = 2;
66 private int mOrientation = -1;
68 // Length of the delay before fading in after the last page movement.
69 private static final int FADE_IN_DELAY_MS = 300;
70 private static final int FADE_IN_DURATION_MS = 200;
71 private Runnable mDeferredHandleFadeInRunnable;
72 private long mFadeStartTime;
73 private boolean mVisible;
74 private boolean mTemporarilyHidden;
76 // Deferred runnable to avoid invalidating outside of frame dispatch,
77 // in turn avoiding issues with sync barrier insertion.
78 private Runnable mInvalidationRunnable;
79 private boolean mHasPendingInvalidate;
82 * Provides additional interaction behaviors necessary for handle
83 * manipulation and interaction.
85 public interface PopupTouchHandleDrawableDelegate {
87 * @return The parent View of the PopupWindow.
92 * @return A position observer for the parent View, used to keep the
93 * absolutely positioned PopupWindow in-sync with the parent.
95 PositionObserver getParentPositionObserver();
98 * Should route MotionEvents to the appropriate logic layer for
99 * performing handle manipulation.
101 boolean onTouchHandleEvent(MotionEvent ev);
104 * @return Whether the associated content is actively scrolling.
106 boolean isScrollInProgress();
109 public PopupTouchHandleDrawable(PopupTouchHandleDrawableDelegate delegate) {
110 super(delegate.getParent().getContext());
111 mContext = delegate.getParent().getContext();
112 mDelegate = new WeakReference<PopupTouchHandleDrawableDelegate>(delegate);
113 mContainer = new PopupWindow(mContext, null, android.R.attr.textSelectHandleWindowStyle);
114 mContainer.setSplitTouchEnabled(true);
115 mContainer.setClippingEnabled(false);
116 mContainer.setAnimationStyle(0);
118 mVisible = getVisibility() == VISIBLE;
119 mParentPositionListener = new PositionObserver.Listener() {
121 public void onPositionChanged(int x, int y) {
122 updateParentPosition(x, y);
127 @SuppressLint("ClickableViewAccessibility")
129 public boolean onTouchEvent(MotionEvent event) {
130 final PopupTouchHandleDrawableDelegate delegate = mDelegate.get();
131 if (delegate == null) {
132 // If the delegate is gone, we should immediately dispose of the popup.
137 // Convert from PopupWindow local coordinates to
138 // parent view local coordinates prior to forwarding.
139 delegate.getParent().getLocationOnScreen(mTempScreenCoords);
140 final float offsetX = event.getRawX() - event.getX() - mTempScreenCoords[0];
141 final float offsetY = event.getRawY() - event.getY() - mTempScreenCoords[1];
142 final MotionEvent offsetEvent = MotionEvent.obtainNoHistory(event);
143 offsetEvent.offsetLocation(offsetX, offsetY);
144 final boolean handled = delegate.onTouchHandleEvent(offsetEvent);
145 offsetEvent.recycle();
149 private void setOrientation(int orientation) {
150 assert orientation >= LEFT && orientation <= RIGHT;
151 if (mOrientation == orientation) return;
153 final boolean hadValidOrientation = mOrientation != -1;
154 mOrientation = orientation;
156 final int oldAdjustedPositionX = getAdjustedPositionX();
157 final int oldAdjustedPositionY = getAdjustedPositionY();
159 switch (orientation) {
161 mDrawable = HandleViewResources.getLeftHandleDrawable(mContext);
162 mHotspotX = (mDrawable.getIntrinsicWidth() * 3) / 4f;
167 mDrawable = HandleViewResources.getRightHandleDrawable(mContext);
168 mHotspotX = mDrawable.getIntrinsicWidth() / 4f;
174 mDrawable = HandleViewResources.getCenterHandleDrawable(mContext);
175 mHotspotX = mDrawable.getIntrinsicWidth() / 2f;
181 // Force handle repositioning to accommodate the new orientation's hotspot.
182 if (hadValidOrientation) setFocus(oldAdjustedPositionX, oldAdjustedPositionY);
183 mDrawable.setAlpha((int) (255 * mAlpha));
184 scheduleInvalidate();
187 private void updateParentPosition(int parentPositionX, int parentPositionY) {
188 if (mParentPositionX == parentPositionX && mParentPositionY == parentPositionY) return;
189 mParentPositionX = parentPositionX;
190 mParentPositionY = parentPositionY;
194 private int getContainerPositionX() {
195 return mParentPositionX + mPositionX;
198 private int getContainerPositionY() {
199 return mParentPositionY + mPositionY;
202 private void updatePosition() {
203 mContainer.update(getContainerPositionX(), getContainerPositionY(),
204 getRight() - getLeft(), getBottom() - getTop());
207 private void updateVisibility() {
208 boolean visible = mVisible && !mTemporarilyHidden;
209 setVisibility(visible ? VISIBLE : INVISIBLE);
212 private void updateAlpha() {
213 if (mAlpha == 1.f) return;
214 long currentTimeMillis = AnimationUtils.currentAnimationTimeMillis();
215 mAlpha = Math.min(1.f, (float) (currentTimeMillis - mFadeStartTime) / FADE_IN_DURATION_MS);
216 mDrawable.setAlpha((int) (255 * mAlpha));
217 scheduleInvalidate();
220 private void temporarilyHide() {
221 mTemporarilyHidden = true;
226 private void doInvalidate() {
227 if (!mContainer.isShowing()) return;
233 private void scheduleInvalidate() {
234 if (mInvalidationRunnable == null) {
235 mInvalidationRunnable = new Runnable() {
238 mHasPendingInvalidate = false;
244 if (mHasPendingInvalidate) return;
245 mHasPendingInvalidate = true;
246 ApiCompatibilityUtils.postOnAnimation(this, mInvalidationRunnable);
249 private void rescheduleFadeIn() {
250 if (mDeferredHandleFadeInRunnable == null) {
251 mDeferredHandleFadeInRunnable = new Runnable() {
254 if (isScrollInProgress()) {
258 mTemporarilyHidden = false;
264 removeCallbacks(mDeferredHandleFadeInRunnable);
265 ApiCompatibilityUtils.postOnAnimationDelayed(
266 this, mDeferredHandleFadeInRunnable, FADE_IN_DELAY_MS);
269 private void beginFadeIn() {
270 if (getVisibility() == VISIBLE) return;
272 mFadeStartTime = AnimationUtils.currentAnimationTimeMillis();
277 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
278 if (mDrawable == null) {
279 setMeasuredDimension(0, 0);
282 setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
286 protected void onDraw(Canvas c) {
287 if (mDrawable == null) return;
289 mDrawable.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop());
293 // Returns the x coordinate of the position that the handle appears to be pointing to relative
294 // to the handles "parent" view.
295 private int getAdjustedPositionX() {
296 return mPositionX + Math.round(mHotspotX);
299 // Returns the y coordinate of the position that the handle appears to be pointing to relative
300 // to the handles "parent" view.
301 private int getAdjustedPositionY() {
302 return mPositionY + Math.round(mHotspotY);
305 private boolean isScrollInProgress() {
306 final PopupTouchHandleDrawableDelegate delegate = mDelegate.get();
307 if (delegate == null) {
312 return delegate.isScrollInProgress();
316 private void show() {
317 if (mContainer.isShowing()) return;
319 final PopupTouchHandleDrawableDelegate delegate = mDelegate.get();
320 if (delegate == null) {
325 mParentPositionObserver = delegate.getParentPositionObserver();
326 assert mParentPositionObserver != null;
328 // While hidden, the parent position may have become stale. It must be updated before
329 // checking isPositionVisible().
330 updateParentPosition(mParentPositionObserver.getPositionX(),
331 mParentPositionObserver.getPositionY());
332 mParentPositionObserver.addListener(mParentPositionListener);
333 mContainer.setContentView(this);
334 mContainer.showAtLocation(delegate.getParent(), 0,
335 getContainerPositionX(), getContainerPositionY());
339 private void hide() {
340 mTemporarilyHidden = false;
341 mContainer.dismiss();
342 if (mParentPositionObserver != null) {
343 mParentPositionObserver.removeListener(mParentPositionListener);
344 // Clear the strong reference to allow garbage collection.
345 mParentPositionObserver = null;
350 private void setRightOrientation() {
351 setOrientation(RIGHT);
355 private void setLeftOrientation() {
356 setOrientation(LEFT);
360 private void setCenterOrientation() {
361 setOrientation(CENTER);
365 private void setOpacity(float alpha) {
366 // Ignore opacity updates from the caller as they are not compatible
367 // with the custom fade animation.
371 private void setFocus(float focusX, float focusY) {
372 int x = (int) focusX - Math.round(mHotspotX);
373 int y = (int) focusY - Math.round(mHotspotY);
374 if (mPositionX == x && mPositionY == y) return;
377 if (isScrollInProgress()) {
380 scheduleInvalidate();
385 private void setVisible(boolean visible) {
387 int visibility = visible ? VISIBLE : INVISIBLE;
388 if (getVisibility() == visibility) return;
389 scheduleInvalidate();
393 private boolean intersectsWith(float x, float y, float width, float height) {
394 if (mDrawable == null) return false;
395 final int drawableWidth = mDrawable.getIntrinsicWidth();
396 final int drawableHeight = mDrawable.getIntrinsicHeight();
397 return !(x >= mPositionX + drawableWidth
398 || y >= mPositionY + drawableHeight
399 || x + width <= mPositionX
400 || y + height <= mPositionY);