PopupTouchHandleDrawable.java revision 5f1c94371a64b3196d4be9466099bb892df9b88e
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.widget.PopupWindow;
13
14import org.chromium.base.CalledByNative;
15import org.chromium.base.JNINamespace;
16import org.chromium.content.browser.PositionObserver;
17
18import java.lang.ref.WeakReference;
19
20/**
21 * View that displays a selection or insertion handle for text editing.
22 *
23 * While a HandleView is logically a child of some other view, it does not exist in that View's
24 * hierarchy.
25 *
26 */
27@JNINamespace("content")
28public class PopupTouchHandleDrawable extends View {
29    private Drawable mDrawable;
30    private final PopupWindow mContainer;
31    private final Context mContext;
32    private final PositionObserver.Listener mParentPositionListener;
33
34    // The weak delegate reference allows the PopupTouchHandleDrawable to be owned by a native
35    // object that might have a different lifetime (or a cyclic lifetime) with respect to the
36    // delegate, allowing garbage collection of any Java references.
37    private final WeakReference<PopupTouchHandleDrawableDelegate> mDelegate;
38
39    // The observer reference will only be non-null while it is attached to mParentPositionListener.
40    private PositionObserver mParentPositionObserver;
41
42    // The position of the handle relative to the parent view.
43    private int mPositionX;
44    private int mPositionY;
45
46    // The position of the parent relative to the application's root view.
47    private int mParentPositionX;
48    private int mParentPositionY;
49
50    // The offset from this handles position to the "tip" of the handle.
51    private float mHotspotX;
52    private float mHotspotY;
53
54    private float mAlpha;
55
56    private final int[] mTempScreenCoords = new int[2];
57
58    static final int LEFT = 0;
59    static final int CENTER = 1;
60    static final int RIGHT = 2;
61    private int mOrientation = -1;
62
63    /**
64     * Provides additional interaction behaviors necessary for handle
65     * manipulation and interaction.
66     */
67    public interface PopupTouchHandleDrawableDelegate {
68        /**
69         * @return The parent View of the PopupWindow.
70         */
71        View getParent();
72
73        /**
74         * @return A position observer for the parent View, used to keep the
75         *         absolutely positioned PopupWindow in-sync with the parent.
76         */
77        PositionObserver getParentPositionObserver();
78
79        /**
80         * Should route MotionEvents to the appropriate logic layer for
81         * performing handle manipulation.
82         */
83        boolean onTouchHandleEvent(MotionEvent ev);
84    }
85
86    public PopupTouchHandleDrawable(PopupTouchHandleDrawableDelegate delegate) {
87        super(delegate.getParent().getContext());
88        mContext = delegate.getParent().getContext();
89        mDelegate = new WeakReference<PopupTouchHandleDrawableDelegate>(delegate);
90        mContainer = new PopupWindow(mContext, null, android.R.attr.textSelectHandleWindowStyle);
91        mContainer.setSplitTouchEnabled(true);
92        mContainer.setClippingEnabled(false);
93        mAlpha = 1.f;
94        mParentPositionListener = new PositionObserver.Listener() {
95            @Override
96            public void onPositionChanged(int x, int y) {
97                updateParentPosition(x, y);
98            }
99        };
100    }
101
102    @Override
103    public boolean onTouchEvent(MotionEvent event) {
104        final PopupTouchHandleDrawableDelegate delegate = mDelegate.get();
105        if (delegate == null) {
106            // If the delegate is gone, we should immediately dispose of the popup.
107            hide();
108            return false;
109        }
110
111        // Convert from PopupWindow local coordinates to
112        // parent view local coordinates prior to forwarding.
113        delegate.getParent().getLocationOnScreen(mTempScreenCoords);
114        final float offsetX = event.getRawX() - event.getX() - mTempScreenCoords[0];
115        final float offsetY = event.getRawY() - event.getY() - mTempScreenCoords[1];
116        final MotionEvent offsetEvent = MotionEvent.obtainNoHistory(event);
117        offsetEvent.offsetLocation(offsetX, offsetY);
118        final boolean handled = delegate.onTouchHandleEvent(offsetEvent);
119        offsetEvent.recycle();
120        return handled;
121    }
122
123    private void setOrientation(int orientation) {
124        assert orientation >= LEFT && orientation <= RIGHT;
125        if (mOrientation == orientation) return;
126
127        final boolean hadValidOrientation = mOrientation != -1;
128        mOrientation = orientation;
129
130        final int oldAdjustedPositionX = getAdjustedPositionX();
131        final int oldAdjustedPositionY = getAdjustedPositionY();
132
133        switch (orientation) {
134            case LEFT: {
135                mDrawable = HandleViewResources.getLeftHandleDrawable(mContext);
136                mHotspotX = (mDrawable.getIntrinsicWidth() * 3) / 4f;
137                break;
138            }
139
140            case RIGHT: {
141                mDrawable = HandleViewResources.getRightHandleDrawable(mContext);
142                mHotspotX = mDrawable.getIntrinsicWidth() / 4f;
143                break;
144            }
145
146            case CENTER:
147            default: {
148                mDrawable = HandleViewResources.getCenterHandleDrawable(mContext);
149                mHotspotX = mDrawable.getIntrinsicWidth() / 2f;
150                break;
151            }
152        }
153        mHotspotY = 0;
154
155        // Force handle repositioning to accommodate the new orientation's hotspot.
156        if (hadValidOrientation) positionAt(oldAdjustedPositionX, oldAdjustedPositionY);
157        mDrawable.setAlpha((int) (255 * mAlpha));
158
159        invalidate();
160    }
161
162    private void updateParentPosition(int parentPositionX, int parentPositionY) {
163        mParentPositionX = parentPositionX;
164        mParentPositionY = parentPositionY;
165        onPositionChanged();
166    }
167
168    private int getContainerPositionX() {
169        return mParentPositionX + mPositionX;
170    }
171
172    private int getContainerPositionY() {
173        return mParentPositionY + mPositionY;
174    }
175
176    private void onPositionChanged() {
177        mContainer.update(getContainerPositionX(), getContainerPositionY(),
178                getRight() - getLeft(), getBottom() - getTop());
179    }
180
181    // x and y are in physical pixels.
182    private void moveTo(int x, int y) {
183        mPositionX = x;
184        mPositionY = y;
185        onPositionChanged();
186    }
187
188    @Override
189    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
190        if (mDrawable == null) {
191            setMeasuredDimension(0, 0);
192            return;
193        }
194        setMeasuredDimension(mDrawable.getIntrinsicWidth(),
195                mDrawable.getIntrinsicHeight());
196    }
197
198    @Override
199    protected void onDraw(Canvas c) {
200        if (mDrawable == null) return;
201        mDrawable.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop());
202        mDrawable.draw(c);
203    }
204
205    // x and y are in physical pixels.
206    private void positionAt(int x, int y) {
207        moveTo(x - Math.round(mHotspotX), y - Math.round(mHotspotY));
208    }
209
210    // Returns the x coordinate of the position that the handle appears to be pointing to relative
211    // to the handles "parent" view.
212    private int getAdjustedPositionX() {
213        return mPositionX + Math.round(mHotspotX);
214    }
215
216    // Returns the y coordinate of the position that the handle appears to be pointing to relative
217    // to the handles "parent" view.
218    private int getAdjustedPositionY() {
219        return mPositionY + Math.round(mHotspotY);
220    }
221
222    @CalledByNative
223    private void show() {
224        if (mContainer.isShowing()) return;
225
226        final PopupTouchHandleDrawableDelegate delegate = mDelegate.get();
227        if (delegate == null) {
228            hide();
229            return;
230        }
231
232        mParentPositionObserver = delegate.getParentPositionObserver();
233        assert mParentPositionObserver != null;
234
235        // While hidden, the parent position may have become stale. It must be updated before
236        // checking isPositionVisible().
237        updateParentPosition(mParentPositionObserver.getPositionX(),
238                mParentPositionObserver.getPositionY());
239        mParentPositionObserver.addListener(mParentPositionListener);
240        mContainer.setContentView(this);
241        mContainer.showAtLocation(delegate.getParent(), 0,
242                getContainerPositionX(), getContainerPositionY());
243    }
244
245    @CalledByNative
246    private void hide() {
247        mContainer.dismiss();
248        if (mParentPositionObserver != null) {
249            mParentPositionObserver.removeListener(mParentPositionListener);
250            // Clear the strong reference to allow garbage collection.
251            mParentPositionObserver = null;
252        }
253    }
254
255    @CalledByNative
256    private void setRightOrientation() {
257        setOrientation(RIGHT);
258    }
259
260    @CalledByNative
261    private void setLeftOrientation() {
262        setOrientation(LEFT);
263    }
264
265    @CalledByNative
266    private void setCenterOrientation() {
267        setOrientation(CENTER);
268    }
269
270    @CalledByNative
271    private void setOpacity(float alpha) {
272        if (mAlpha == alpha) return;
273        mAlpha = alpha;
274        if (mDrawable != null) mDrawable.setAlpha((int) (255 * mAlpha));
275    }
276
277    @CalledByNative
278    private void setFocus(float x, float y) {
279        positionAt((int) x, (int) y);
280    }
281
282    @CalledByNative
283    private void setVisible(boolean visible) {
284        setVisibility(visible ? VISIBLE : INVISIBLE);
285    }
286
287    @CalledByNative
288    private boolean intersectsWith(float x, float y, float width, float height) {
289        if (mDrawable == null) return false;
290        final int drawableWidth = mDrawable.getIntrinsicWidth();
291        final int drawableHeight = mDrawable.getIntrinsicHeight();
292        return !(x >= mPositionX + drawableWidth
293                || y >= mPositionY + drawableHeight
294                || x + width <= mPositionX
295                || y + height <= mPositionY);
296    }
297}
298