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
5package org.chromium.content.browser.input;
6
7import android.content.Context;
8import android.graphics.Canvas;
9import android.graphics.drawable.Drawable;
10import android.view.MotionEvent;
11import android.view.View;
12import android.view.animation.AnimationUtils;
13import android.widget.PopupWindow;
14
15import org.chromium.base.ApiCompatibilityUtils;
16import org.chromium.base.CalledByNative;
17import org.chromium.base.JNINamespace;
18import org.chromium.content.browser.PositionObserver;
19
20import 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")
30public 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        if (!mContainer.isShowing()) return;
224        updatePosition();
225        updateVisibility();
226        invalidate();
227    }
228
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
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
265    private void beginFadeIn() {
266        if (getVisibility() == VISIBLE) return;
267        mAlpha = 0.f;
268        mFadeStartTime = AnimationUtils.currentAnimationTimeMillis();
269        doInvalidate();
270    }
271
272    @Override
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
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.
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.
297    private int getAdjustedPositionY() {
298        return mPositionY + Math.round(mHotspotY);
299    }
300
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
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
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
346    private void setRightOrientation() {
347        setOrientation(RIGHT);
348    }
349
350    @CalledByNative
351    private void setLeftOrientation() {
352        setOrientation(LEFT);
353    }
354
355    @CalledByNative
356    private void setCenterOrientation() {
357        setOrientation(CENTER);
358    }
359
360    @CalledByNative
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
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
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
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