Update To 11.40.268.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.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;
15
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;
20
21 import java.lang.ref.WeakReference;
22
23 /**
24  * View that displays a selection or insertion handle for text editing.
25  *
26  * While a HandleView is logically a child of some other view, it does not exist in that View's
27  * hierarchy.
28  *
29  */
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;
36
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;
41
42     // The observer reference will only be non-null while it is attached to mParentPositionListener.
43     private PositionObserver mParentPositionObserver;
44
45     // The position of the handle relative to the parent view.
46     private int mPositionX;
47     private int mPositionY;
48
49     // The position of the parent relative to the application's root view.
50     private int mParentPositionX;
51     private int mParentPositionY;
52
53     // The offset from this handles position to the "tip" of the handle.
54     private float mHotspotX;
55     private float mHotspotY;
56
57     private float mAlpha;
58
59     private final int[] mTempScreenCoords = new int[2];
60
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;
67
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;
75
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;
80
81     /**
82      * Provides additional interaction behaviors necessary for handle
83      * manipulation and interaction.
84      */
85     public interface PopupTouchHandleDrawableDelegate {
86         /**
87          * @return The parent View of the PopupWindow.
88          */
89         View getParent();
90
91         /**
92          * @return A position observer for the parent View, used to keep the
93          *         absolutely positioned PopupWindow in-sync with the parent.
94          */
95         PositionObserver getParentPositionObserver();
96
97         /**
98          * Should route MotionEvents to the appropriate logic layer for
99          * performing handle manipulation.
100          */
101         boolean onTouchHandleEvent(MotionEvent ev);
102
103         /**
104          * @return Whether the associated content is actively scrolling.
105          */
106         boolean isScrollInProgress();
107     }
108
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);
117         mAlpha = 1.f;
118         mVisible = getVisibility() == VISIBLE;
119         mParentPositionListener = new PositionObserver.Listener() {
120             @Override
121             public void onPositionChanged(int x, int y) {
122                 updateParentPosition(x, y);
123             }
124         };
125     }
126
127     @SuppressLint("ClickableViewAccessibility")
128     @Override
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.
133             hide();
134             return false;
135         }
136
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();
146         return handled;
147     }
148
149     private void setOrientation(int orientation) {
150         assert orientation >= LEFT && orientation <= RIGHT;
151         if (mOrientation == orientation) return;
152
153         final boolean hadValidOrientation = mOrientation != -1;
154         mOrientation = orientation;
155
156         final int oldAdjustedPositionX = getAdjustedPositionX();
157         final int oldAdjustedPositionY = getAdjustedPositionY();
158
159         switch (orientation) {
160             case LEFT: {
161                 mDrawable = HandleViewResources.getLeftHandleDrawable(mContext);
162                 mHotspotX = (mDrawable.getIntrinsicWidth() * 3) / 4f;
163                 break;
164             }
165
166             case RIGHT: {
167                 mDrawable = HandleViewResources.getRightHandleDrawable(mContext);
168                 mHotspotX = mDrawable.getIntrinsicWidth() / 4f;
169                 break;
170             }
171
172             case CENTER:
173             default: {
174                 mDrawable = HandleViewResources.getCenterHandleDrawable(mContext);
175                 mHotspotX = mDrawable.getIntrinsicWidth() / 2f;
176                 break;
177             }
178         }
179         mHotspotY = 0;
180
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();
185     }
186
187     private void updateParentPosition(int parentPositionX, int parentPositionY) {
188         if (mParentPositionX == parentPositionX && mParentPositionY == parentPositionY) return;
189         mParentPositionX = parentPositionX;
190         mParentPositionY = parentPositionY;
191         temporarilyHide();
192     }
193
194     private int getContainerPositionX() {
195         return mParentPositionX + mPositionX;
196     }
197
198     private int getContainerPositionY() {
199         return mParentPositionY + mPositionY;
200     }
201
202     private void updatePosition() {
203         mContainer.update(getContainerPositionX(), getContainerPositionY(),
204                 getRight() - getLeft(), getBottom() - getTop());
205     }
206
207     private void updateVisibility() {
208         boolean visible = mVisible && !mTemporarilyHidden;
209         setVisibility(visible ? VISIBLE : INVISIBLE);
210     }
211
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();
218     }
219
220     private void temporarilyHide() {
221         mTemporarilyHidden = true;
222         updateVisibility();
223         rescheduleFadeIn();
224     }
225
226     private void doInvalidate() {
227         if (!mContainer.isShowing()) return;
228         updatePosition();
229         updateVisibility();
230         invalidate();
231     }
232
233     private void scheduleInvalidate() {
234         if (mInvalidationRunnable == null) {
235             mInvalidationRunnable = new Runnable() {
236                 @Override
237                 public void run() {
238                     mHasPendingInvalidate = false;
239                     doInvalidate();
240                 }
241             };
242         }
243
244         if (mHasPendingInvalidate) return;
245         mHasPendingInvalidate = true;
246         ApiCompatibilityUtils.postOnAnimation(this, mInvalidationRunnable);
247     }
248
249     private void rescheduleFadeIn() {
250         if (mDeferredHandleFadeInRunnable == null) {
251             mDeferredHandleFadeInRunnable = new Runnable() {
252                 @Override
253                 public void run() {
254                     if (isScrollInProgress()) {
255                         rescheduleFadeIn();
256                         return;
257                     }
258                     mTemporarilyHidden = false;
259                     beginFadeIn();
260                 }
261             };
262         }
263
264         removeCallbacks(mDeferredHandleFadeInRunnable);
265         ApiCompatibilityUtils.postOnAnimationDelayed(
266                 this, mDeferredHandleFadeInRunnable, FADE_IN_DELAY_MS);
267     }
268
269     private void beginFadeIn() {
270         if (getVisibility() == VISIBLE) return;
271         mAlpha = 0.f;
272         mFadeStartTime = AnimationUtils.currentAnimationTimeMillis();
273         doInvalidate();
274     }
275
276     @Override
277     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
278         if (mDrawable == null) {
279             setMeasuredDimension(0, 0);
280             return;
281         }
282         setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
283     }
284
285     @Override
286     protected void onDraw(Canvas c) {
287         if (mDrawable == null) return;
288         updateAlpha();
289         mDrawable.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop());
290         mDrawable.draw(c);
291     }
292
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);
297     }
298
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);
303     }
304
305     private boolean isScrollInProgress() {
306         final PopupTouchHandleDrawableDelegate delegate = mDelegate.get();
307         if (delegate == null) {
308             hide();
309             return false;
310         }
311
312         return delegate.isScrollInProgress();
313     }
314
315     @CalledByNative
316     private void show() {
317         if (mContainer.isShowing()) return;
318
319         final PopupTouchHandleDrawableDelegate delegate = mDelegate.get();
320         if (delegate == null) {
321             hide();
322             return;
323         }
324
325         mParentPositionObserver = delegate.getParentPositionObserver();
326         assert mParentPositionObserver != null;
327
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());
336     }
337
338     @CalledByNative
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;
346         }
347     }
348
349     @CalledByNative
350     private void setRightOrientation() {
351         setOrientation(RIGHT);
352     }
353
354     @CalledByNative
355     private void setLeftOrientation() {
356         setOrientation(LEFT);
357     }
358
359     @CalledByNative
360     private void setCenterOrientation() {
361         setOrientation(CENTER);
362     }
363
364     @CalledByNative
365     private void setOpacity(float alpha) {
366         // Ignore opacity updates from the caller as they are not compatible
367         // with the custom fade animation.
368     }
369
370     @CalledByNative
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;
375         mPositionX = x;
376         mPositionY = y;
377         if (isScrollInProgress()) {
378             temporarilyHide();
379         } else {
380             scheduleInvalidate();
381         }
382     }
383
384     @CalledByNative
385     private void setVisible(boolean visible) {
386         mVisible = visible;
387         int visibility = visible ? VISIBLE : INVISIBLE;
388         if (getVisibility() == visibility) return;
389         scheduleInvalidate();
390     }
391
392     @CalledByNative
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);
401     }
402 }