Upstream version 11.39.250.0
[platform/framework/web/crosswalk.git] / src / content / public / android / java / src / org / chromium / content / browser / input / PopupTouchHandleDrawable.java
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          */
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          */
92         PositionObserver getParentPositionObserver();
93
94         /**
95          * Should route MotionEvents to the appropriate logic layer for
96          * performing handle manipulation.
97          */
98         boolean onTouchHandleEvent(MotionEvent ev);
99
100         /**
101          * @return Whether the associated content is actively scrolling.
102          */
103         boolean isScrollInProgress();
104     }
105
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
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
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
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
190     private int getContainerPositionX() {
191         return mParentPositionX + mPositionX;
192     }
193
194     private int getContainerPositionY() {
195         return mParentPositionY + mPositionY;
196     }
197
198     private void updatePosition() {
199         mContainer.update(getContainerPositionX(), getContainerPositionY(),
200                 getRight() - getLeft(), getBottom() - getTop());
201     }
202
203     private void updateVisibility() {
204         boolean visible = mVisible && !mTemporarilyHidden;
205         setVisibility(visible ? VISIBLE : INVISIBLE);
206     }
207
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
216     private void temporarilyHide() {
217         mTemporarilyHidden = true;
218         updateVisibility();
219         rescheduleFadeIn();
220     }
221
222     private void doInvalidate() {
223         updatePosition();
224         updateVisibility();
225         invalidate();
226     }
227
228     private void scheduleInvalidate() {
229         if (mInvalidationRunnable == null) {
230             mInvalidationRunnable = new Runnable() {
231                 @Override
232                 public void run() {
233                     mHasPendingInvalidate = false;
234                     doInvalidate();
235                 }
236             };
237         }
238
239         if (mHasPendingInvalidate) return;
240         mHasPendingInvalidate = true;
241         ApiCompatibilityUtils.postOnAnimation(this, mInvalidationRunnable);
242     }
243
244     private void rescheduleFadeIn() {
245         if (mDeferredHandleFadeInRunnable == null) {
246             mDeferredHandleFadeInRunnable = new Runnable() {
247                 @Override
248                 public void run() {
249                     if (isScrollInProgress()) {
250                         rescheduleFadeIn();
251                         return;
252                     }
253                     mTemporarilyHidden = false;
254                     beginFadeIn();
255                 }
256             };
257         }
258
259         removeCallbacks(mDeferredHandleFadeInRunnable);
260         ApiCompatibilityUtils.postOnAnimationDelayed(
261                 this, mDeferredHandleFadeInRunnable, FADE_IN_DELAY_MS);
262     }
263
264     private void beginFadeIn() {
265         if (getVisibility() == VISIBLE) return;
266         mAlpha = 0.f;
267         mFadeStartTime = AnimationUtils.currentAnimationTimeMillis();
268         doInvalidate();
269     }
270
271     @Override
272     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
273         if (mDrawable == null) {
274             setMeasuredDimension(0, 0);
275             return;
276         }
277         setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
278     }
279
280     @Override
281     protected void onDraw(Canvas c) {
282         if (mDrawable == null) return;
283         updateAlpha();
284         mDrawable.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop());
285         mDrawable.draw(c);
286     }
287
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);
292     }
293
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);
298     }
299
300     private boolean isScrollInProgress() {
301         final PopupTouchHandleDrawableDelegate delegate = mDelegate.get();
302         if (delegate == null) {
303             hide();
304             return false;
305         }
306
307         return delegate.isScrollInProgress();
308     }
309
310     @CalledByNative
311     private void show() {
312         if (mContainer.isShowing()) return;
313
314         final PopupTouchHandleDrawableDelegate delegate = mDelegate.get();
315         if (delegate == null) {
316             hide();
317             return;
318         }
319
320         mParentPositionObserver = delegate.getParentPositionObserver();
321         assert mParentPositionObserver != null;
322
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());
331     }
332
333     @CalledByNative
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;
341         }
342     }
343
344     @CalledByNative
345     private void setRightOrientation() {
346         setOrientation(RIGHT);
347     }
348
349     @CalledByNative
350     private void setLeftOrientation() {
351         setOrientation(LEFT);
352     }
353
354     @CalledByNative
355     private void setCenterOrientation() {
356         setOrientation(CENTER);
357     }
358
359     @CalledByNative
360     private void setOpacity(float alpha) {
361         // Ignore opacity updates from the caller as they are not compatible
362         // with the custom fade animation.
363     }
364
365     @CalledByNative
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;
370         mPositionX = x;
371         mPositionY = y;
372         if (isScrollInProgress()) {
373             temporarilyHide();
374         } else {
375             scheduleInvalidate();
376         }
377     }
378
379     @CalledByNative
380     private void setVisible(boolean visible) {
381         mVisible = visible;
382         int visibility = visible ? VISIBLE : INVISIBLE;
383         if (getVisibility() == visibility) return;
384         scheduleInvalidate();
385     }
386
387     @CalledByNative
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);
396     }
397 }