• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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