• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2012 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.content.res.TypedArray;
9 import android.graphics.Canvas;
10 import android.graphics.Rect;
11 import android.graphics.drawable.Drawable;
12 import android.os.SystemClock;
13 import android.util.TypedValue;
14 import android.view.MotionEvent;
15 import android.view.View;
16 import android.view.ViewConfiguration;
17 import android.view.ViewParent;
18 import android.view.animation.AnimationUtils;
19 import android.widget.PopupWindow;
20 
21 import org.chromium.content.browser.PositionObserver;
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 public class HandleView extends View {
31     private static final float FADE_DURATION = 200.f;
32 
33     private Drawable mDrawable;
34     private final PopupWindow mContainer;
35 
36     // The position of the handle relative to the parent view.
37     private int mPositionX;
38     private int mPositionY;
39 
40     // The position of the parent relative to the application's root view.
41     private int mParentPositionX;
42     private int mParentPositionY;
43 
44     // The offset from this handles position to the "tip" of the handle.
45     private float mHotspotX;
46     private float mHotspotY;
47 
48     private final CursorController mController;
49     private boolean mIsDragging;
50     private float mTouchToWindowOffsetX;
51     private float mTouchToWindowOffsetY;
52 
53     private final int mLineOffsetY;
54     private float mDownPositionX, mDownPositionY;
55     private long mTouchTimer;
56     private boolean mIsInsertionHandle = false;
57     private float mAlpha;
58     private long mFadeStartTime;
59 
60     private final View mParent;
61     private InsertionHandleController.PastePopupMenu mPastePopupWindow;
62 
63     private final Rect mTempRect = new Rect();
64 
65     static final int LEFT = 0;
66     static final int CENTER = 1;
67     static final int RIGHT = 2;
68 
69     private final PositionObserver mParentPositionObserver;
70     private final PositionObserver.Listener mParentPositionListener;
71 
72     // Number of dips to subtract from the handle's y position to give a suitable
73     // y coordinate for the corresponding text position. This is to compensate for the fact
74     // that the handle position is at the base of the line of text.
75     private static final float LINE_OFFSET_Y_DIP = 5.0f;
76 
77     private static final int[] TEXT_VIEW_HANDLE_ATTRS = {
78         android.R.attr.textSelectHandleLeft,
79         android.R.attr.textSelectHandle,
80         android.R.attr.textSelectHandleRight,
81     };
82 
HandleView(CursorController controller, int pos, View parent, PositionObserver parentPositionObserver)83     HandleView(CursorController controller, int pos, View parent,
84             PositionObserver parentPositionObserver) {
85         super(parent.getContext());
86         mParent = parent;
87         Context context = mParent.getContext();
88         mController = controller;
89         mContainer = new PopupWindow(context, null, android.R.attr.textSelectHandleWindowStyle);
90         mContainer.setSplitTouchEnabled(true);
91         mContainer.setClippingEnabled(false);
92         mContainer.setAnimationStyle(0);
93 
94         setOrientation(pos);
95 
96         // Convert line offset dips to pixels.
97         mLineOffsetY = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
98                 LINE_OFFSET_Y_DIP, context.getResources().getDisplayMetrics());
99 
100         mAlpha = 1.f;
101 
102         mParentPositionListener = new PositionObserver.Listener() {
103             @Override
104             public void onPositionChanged(int x, int y) {
105                 updateParentPosition(x, y);
106             }
107         };
108         mParentPositionObserver = parentPositionObserver;
109     }
110 
setOrientation(int pos)111     void setOrientation(int pos) {
112         Context context = mParent.getContext();
113         TypedArray a = context.getTheme().obtainStyledAttributes(TEXT_VIEW_HANDLE_ATTRS);
114         mDrawable = a.getDrawable(pos);
115         a.recycle();
116 
117         mIsInsertionHandle = (pos == CENTER);
118 
119         int handleWidth = mDrawable.getIntrinsicWidth();
120         switch (pos) {
121             case LEFT: {
122                 mHotspotX = (handleWidth * 3) / 4f;
123                 break;
124             }
125 
126             case RIGHT: {
127                 mHotspotX = handleWidth / 4f;
128                 break;
129             }
130 
131             case CENTER:
132             default: {
133                 mHotspotX = handleWidth / 2f;
134                 break;
135             }
136         }
137         mHotspotY = 0;
138 
139         invalidate();
140     }
141 
142     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)143     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
144         setMeasuredDimension(mDrawable.getIntrinsicWidth(),
145                 mDrawable.getIntrinsicHeight());
146     }
147 
updateParentPosition(int parentPositionX, int parentPositionY)148     private void updateParentPosition(int parentPositionX, int parentPositionY) {
149         // Hide paste popup window as soon as a scroll occurs.
150         if (mPastePopupWindow != null) mPastePopupWindow.hide();
151 
152         mTouchToWindowOffsetX += parentPositionX - mParentPositionX;
153         mTouchToWindowOffsetY += parentPositionY - mParentPositionY;
154         mParentPositionX = parentPositionX;
155         mParentPositionY = parentPositionY;
156         onPositionChanged();
157     }
158 
getContainerPositionX()159     private int getContainerPositionX() {
160         return mParentPositionX + mPositionX;
161     }
162 
getContainerPositionY()163     private int getContainerPositionY() {
164         return mParentPositionY + mPositionY;
165     }
166 
onPositionChanged()167     private void onPositionChanged() {
168         // Deferring View invalidation while the handles are hidden prevents
169         // scheduling conflicts with the compositor.
170         if (getVisibility() != VISIBLE) return;
171         mContainer.update(getContainerPositionX(), getContainerPositionY(),
172                 getRight() - getLeft(), getBottom() - getTop());
173     }
174 
showContainer()175     private void showContainer() {
176         mContainer.showAtLocation(mParent, 0, getContainerPositionX(), getContainerPositionY());
177     }
178 
show()179     void show() {
180         // While hidden, the parent position may have become stale. It must be updated before
181         // checking isPositionVisible().
182         updateParentPosition(mParentPositionObserver.getPositionX(),
183                 mParentPositionObserver.getPositionY());
184         if (!isPositionVisible()) {
185             hide();
186             return;
187         }
188         mParentPositionObserver.addListener(mParentPositionListener);
189         mContainer.setContentView(this);
190         showContainer();
191 
192         // Hide paste view when handle is moved on screen.
193         if (mPastePopupWindow != null) {
194             mPastePopupWindow.hide();
195         }
196     }
197 
hide()198     void hide() {
199         mIsDragging = false;
200         mContainer.dismiss();
201         mParentPositionObserver.removeListener(mParentPositionListener);
202         if (mPastePopupWindow != null) {
203             mPastePopupWindow.hide();
204         }
205     }
206 
isShowing()207     boolean isShowing() {
208         return mContainer.isShowing();
209     }
210 
isPositionVisible()211     private boolean isPositionVisible() {
212         // Always show a dragging handle.
213         if (mIsDragging) {
214             return true;
215         }
216 
217         final Rect clip = mTempRect;
218         clip.left = 0;
219         clip.top = 0;
220         clip.right = mParent.getWidth();
221         clip.bottom = mParent.getHeight();
222 
223         final ViewParent parent = mParent.getParent();
224         if (parent == null || !parent.getChildVisibleRect(mParent, clip, null)) {
225             return false;
226         }
227 
228         final int posX = getContainerPositionX() + (int) mHotspotX;
229         final int posY = getContainerPositionY() + (int) mHotspotY;
230 
231         return posX >= clip.left && posX <= clip.right &&
232                 posY >= clip.top && posY <= clip.bottom;
233     }
234 
235     // x and y are in physical pixels.
moveTo(int x, int y)236     void moveTo(int x, int y) {
237         int previousPositionX = mPositionX;
238         int previousPositionY = mPositionY;
239 
240         mPositionX = x;
241         mPositionY = y;
242         if (isPositionVisible()) {
243             if (mContainer.isShowing()) {
244                 onPositionChanged();
245                 // Hide paste popup window as soon as the handle is dragged.
246                 if (mPastePopupWindow != null &&
247                         (previousPositionX != mPositionX || previousPositionY != mPositionY)) {
248                     mPastePopupWindow.hide();
249                 }
250             } else {
251                 show();
252             }
253 
254             if (mIsDragging) {
255                 // Hide paste popup window as soon as the handle is dragged.
256                 if (mPastePopupWindow != null) {
257                     mPastePopupWindow.hide();
258                 }
259             }
260         } else {
261             hide();
262         }
263     }
264 
265     @Override
onDraw(Canvas c)266     protected void onDraw(Canvas c) {
267         updateAlpha();
268         mDrawable.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop());
269         mDrawable.draw(c);
270     }
271 
272     @Override
onTouchEvent(MotionEvent ev)273     public boolean onTouchEvent(MotionEvent ev) {
274         switch (ev.getActionMasked()) {
275             case MotionEvent.ACTION_DOWN: {
276                 mDownPositionX = ev.getRawX();
277                 mDownPositionY = ev.getRawY();
278                 mTouchToWindowOffsetX = mDownPositionX - mPositionX;
279                 mTouchToWindowOffsetY = mDownPositionY - mPositionY;
280                 mIsDragging = true;
281                 mController.beforeStartUpdatingPosition(this);
282                 mTouchTimer = SystemClock.uptimeMillis();
283                 break;
284             }
285 
286             case MotionEvent.ACTION_MOVE: {
287                 updatePosition(ev.getRawX(), ev.getRawY());
288                 break;
289             }
290 
291             case MotionEvent.ACTION_UP:
292                 if (mIsInsertionHandle) {
293                     long delay = SystemClock.uptimeMillis() - mTouchTimer;
294                     if (delay < ViewConfiguration.getTapTimeout()) {
295                         if (mPastePopupWindow != null && mPastePopupWindow.isShowing()) {
296                             // Tapping on the handle dismisses the displayed paste view,
297                             mPastePopupWindow.hide();
298                         } else {
299                             showPastePopupWindow();
300                         }
301                     }
302                 }
303                 mIsDragging = false;
304                 break;
305 
306             case MotionEvent.ACTION_CANCEL:
307                 mIsDragging = false;
308                 break;
309 
310             default:
311                 return false;
312         }
313         return true;
314     }
315 
isDragging()316     boolean isDragging() {
317         return mIsDragging;
318     }
319 
320     /**
321      * @return Returns the x position of the handle
322      */
getPositionX()323     int getPositionX() {
324         return mPositionX;
325     }
326 
327     /**
328      * @return Returns the y position of the handle
329      */
getPositionY()330     int getPositionY() {
331         return mPositionY;
332     }
333 
updatePosition(float rawX, float rawY)334     private void updatePosition(float rawX, float rawY) {
335         final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
336         final float newPosY = rawY - mTouchToWindowOffsetY + mHotspotY - mLineOffsetY;
337 
338         mController.updatePosition(this, Math.round(newPosX), Math.round(newPosY));
339     }
340 
341     // x and y are in physical pixels.
positionAt(int x, int y)342     void positionAt(int x, int y) {
343         moveTo(x - Math.round(mHotspotX), y - Math.round(mHotspotY));
344     }
345 
346     // Returns the x coordinate of the position that the handle appears to be pointing to relative
347     // to the handles "parent" view.
getAdjustedPositionX()348     int getAdjustedPositionX() {
349         return mPositionX + Math.round(mHotspotX);
350     }
351 
352     // Returns the y coordinate of the position that the handle appears to be pointing to relative
353     // to the handles "parent" view.
getAdjustedPositionY()354     int getAdjustedPositionY() {
355         return mPositionY + Math.round(mHotspotY);
356     }
357 
358     // Returns the x coordinate of the postion that the handle appears to be pointing to relative to
359     // the root view of the application.
getRootViewRelativePositionX()360     int getRootViewRelativePositionX() {
361         return getContainerPositionX() + Math.round(mHotspotX);
362     }
363 
364     // Returns the y coordinate of the postion that the handle appears to be pointing to relative to
365     // the root view of the application.
getRootViewRelativePositionY()366     int getRootViewRelativePositionY() {
367         return getContainerPositionY() + Math.round(mHotspotY);
368     }
369 
370     // Returns a suitable y coordinate for the text position corresponding to the handle.
371     // As the handle points to a position on the base of the line of text, this method
372     // returns a coordinate a small number of pixels higher (i.e. a slightly smaller number)
373     // than getAdjustedPositionY.
getLineAdjustedPositionY()374     int getLineAdjustedPositionY() {
375         return (int) (mPositionY + mHotspotY - mLineOffsetY);
376     }
377 
getDrawable()378     Drawable getDrawable() {
379         return mDrawable;
380     }
381 
updateAlpha()382     private void updateAlpha() {
383         if (mAlpha == 1.f) return;
384         mAlpha = Math.min(1.f,
385                 (AnimationUtils.currentAnimationTimeMillis() - mFadeStartTime) / FADE_DURATION);
386         mDrawable.setAlpha((int) (255 * mAlpha));
387         invalidate();
388     }
389 
390     /**
391      * If the handle is not visible, sets its visibility to View.VISIBLE and begins fading it in.
392      */
beginFadeIn()393     void beginFadeIn() {
394         if (getVisibility() == VISIBLE) return;
395         mAlpha = 0.f;
396         mFadeStartTime = AnimationUtils.currentAnimationTimeMillis();
397         setVisibility(VISIBLE);
398         // Position updates may have been deferred while the handle was hidden.
399         onPositionChanged();
400     }
401 
showPastePopupWindow()402     void showPastePopupWindow() {
403         InsertionHandleController ihc = (InsertionHandleController) mController;
404         if (mIsInsertionHandle && ihc.canPaste()) {
405             if (mPastePopupWindow == null) {
406                 // Lazy initialization: create when actually shown only.
407                 mPastePopupWindow = ihc.new PastePopupMenu();
408             }
409             mPastePopupWindow.show();
410         }
411     }
412 }
413