1 | // Copyright (c) 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.Gravity; |
15 | import android.view.LayoutInflater; |
16 | import android.view.MotionEvent; |
17 | import android.view.View; |
18 | import android.view.ViewConfiguration; |
19 | import android.view.ViewGroup; |
20 | import android.view.ViewParent; |
21 | import android.view.WindowManager; |
22 | import android.view.View.OnClickListener; |
23 | import android.view.ViewGroup.LayoutParams; |
24 | import android.widget.PopupWindow; |
25 | import android.widget.TextView; |
26 | |
27 | /** |
28 | * View that displays a selection or insertion handle for text editing. |
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 | private int mPositionX; |
36 | private int mPositionY; |
37 | private final CursorController mController; |
38 | private boolean mIsDragging; |
39 | private float mTouchToWindowOffsetX; |
40 | private float mTouchToWindowOffsetY; |
41 | private float mHotspotX; |
42 | private float mHotspotY; |
43 | private int mLineOffsetY; |
44 | private int mLastParentX; |
45 | private int mLastParentY; |
46 | private float mDownPositionX, mDownPositionY; |
47 | private int mContainerPositionX, mContainerPositionY; |
48 | private long mTouchTimer; |
49 | private boolean mIsInsertionHandle = false; |
50 | private float mAlpha; |
51 | private long mFadeStartTime; |
52 | |
53 | private View mParent; |
54 | private InsertionHandleController.PastePopupMenu mPastePopupWindow; |
55 | |
56 | private final int mTextSelectHandleLeftRes; |
57 | private final int mTextSelectHandleRightRes; |
58 | private final int mTextSelectHandleRes; |
59 | |
60 | private Drawable mSelectHandleLeft; |
61 | private Drawable mSelectHandleRight; |
62 | private Drawable mSelectHandleCenter; |
63 | |
64 | private final int[] mTempCoords = new int[2]; |
65 | private final Rect mTempRect = new Rect(); |
66 | |
67 | static final int LEFT = 0; |
68 | static final int CENTER = 1; |
69 | static final int RIGHT = 2; |
70 | |
71 | // Number of dips to subtract from the handle's y position to give a suitable |
72 | // y coordinate for the corresponding text position. This is to compensate for the fact |
73 | // that the handle position is at the base of the line of text. |
74 | private static final float LINE_OFFSET_Y_DIP = 5.0f; |
75 | |
76 | private static final int[] TEXT_VIEW_HANDLE_ATTRS = { |
77 | android.R.attr.textSelectHandleLeft, |
78 | android.R.attr.textSelectHandle, |
79 | android.R.attr.textSelectHandleRight, |
80 | }; |
81 | |
82 | HandleView(CursorController controller, int pos, View parent) { |
83 | super(parent.getContext()); |
84 | Context context = parent.getContext(); |
85 | mParent = parent; |
86 | mController = controller; |
87 | mContainer = new PopupWindow(context, null, android.R.attr.textSelectHandleWindowStyle); |
88 | mContainer.setSplitTouchEnabled(true); |
89 | mContainer.setClippingEnabled(false); |
90 | |
91 | TypedArray a = context.obtainStyledAttributes(TEXT_VIEW_HANDLE_ATTRS); |
92 | mTextSelectHandleLeftRes = a.getResourceId(a.getIndex(LEFT), 0); |
93 | mTextSelectHandleRes = a.getResourceId(a.getIndex(CENTER), 0); |
94 | mTextSelectHandleRightRes = a.getResourceId(a.getIndex(RIGHT), 0); |
95 | a.recycle(); |
96 | |
97 | setOrientation(pos); |
98 | |
99 | // Convert line offset dips to pixels. |
100 | mLineOffsetY = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, |
101 | LINE_OFFSET_Y_DIP, context.getResources().getDisplayMetrics()); |
102 | |
103 | mAlpha = 1.f; |
104 | } |
105 | |
106 | void setOrientation(int pos) { |
107 | int handleWidth; |
108 | switch (pos) { |
109 | case LEFT: { |
110 | if (mSelectHandleLeft == null) { |
111 | mSelectHandleLeft = getContext().getResources().getDrawable( |
112 | mTextSelectHandleLeftRes); |
113 | } |
114 | mDrawable = mSelectHandleLeft; |
115 | handleWidth = mDrawable.getIntrinsicWidth(); |
116 | mHotspotX = (handleWidth * 3) / 4f; |
117 | break; |
118 | } |
119 | |
120 | case RIGHT: { |
121 | if (mSelectHandleRight == null) { |
122 | mSelectHandleRight = getContext().getResources().getDrawable( |
123 | mTextSelectHandleRightRes); |
124 | } |
125 | mDrawable = mSelectHandleRight; |
126 | handleWidth = mDrawable.getIntrinsicWidth(); |
127 | mHotspotX = handleWidth / 4f; |
128 | break; |
129 | } |
130 | |
131 | case CENTER: |
132 | default: { |
133 | if (mSelectHandleCenter == null) { |
134 | mSelectHandleCenter = getContext().getResources().getDrawable( |
135 | mTextSelectHandleRes); |
136 | } |
137 | mDrawable = mSelectHandleCenter; |
138 | handleWidth = mDrawable.getIntrinsicWidth(); |
139 | mHotspotX = handleWidth / 2f; |
140 | mIsInsertionHandle = true; |
141 | break; |
142 | } |
143 | } |
144 | |
145 | mHotspotY = 0; |
146 | invalidate(); |
147 | } |
148 | |
149 | @Override |
150 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
151 | setMeasuredDimension(mDrawable.getIntrinsicWidth(), |
152 | mDrawable.getIntrinsicHeight()); |
153 | } |
154 | |
155 | void show() { |
156 | if (!isPositionVisible()) { |
157 | hide(); |
158 | return; |
159 | } |
160 | mContainer.setContentView(this); |
161 | final int[] coords = mTempCoords; |
162 | mParent.getLocationInWindow(coords); |
163 | mContainerPositionX = coords[0] + mPositionX; |
164 | mContainerPositionY = coords[1] + mPositionY; |
165 | mContainer.showAtLocation(mParent, 0, mContainerPositionX, mContainerPositionY); |
166 | |
167 | // Hide paste view when handle is moved on screen. |
168 | if (mPastePopupWindow != null) { |
169 | mPastePopupWindow.hide(); |
170 | } |
171 | } |
172 | |
173 | void hide() { |
174 | mIsDragging = false; |
175 | mContainer.dismiss(); |
176 | if (mPastePopupWindow != null) { |
177 | mPastePopupWindow.hide(); |
178 | } |
179 | } |
180 | |
181 | boolean isShowing() { |
182 | return mContainer.isShowing(); |
183 | } |
184 | |
185 | private boolean isPositionVisible() { |
186 | // Always show a dragging handle. |
187 | if (mIsDragging) { |
188 | return true; |
189 | } |
190 | |
191 | final Rect clip = mTempRect; |
192 | clip.left = 0; |
193 | clip.top = 0; |
194 | clip.right = mParent.getWidth(); |
195 | clip.bottom = mParent.getHeight(); |
196 | |
197 | final ViewParent parent = mParent.getParent(); |
198 | if (parent == null || !parent.getChildVisibleRect(mParent, clip, null)) { |
199 | return false; |
200 | } |
201 | |
202 | final int[] coords = mTempCoords; |
203 | mParent.getLocationInWindow(coords); |
204 | final int posX = coords[0] + mPositionX + (int) mHotspotX; |
205 | final int posY = coords[1] + mPositionY + (int) mHotspotY; |
206 | |
207 | return posX >= clip.left && posX <= clip.right && |
208 | posY >= clip.top && posY <= clip.bottom; |
209 | } |
210 | |
211 | // x and y are in physical pixels. |
212 | void moveTo(int x, int y) { |
213 | mPositionX = x; |
214 | mPositionY = y; |
215 | if (isPositionVisible()) { |
216 | int[] coords = null; |
217 | if (mContainer.isShowing()) { |
218 | coords = mTempCoords; |
219 | mParent.getLocationInWindow(coords); |
220 | final int containerPositionX = coords[0] + mPositionX; |
221 | final int containerPositionY = coords[1] + mPositionY; |
222 | |
223 | if (containerPositionX != mContainerPositionX || |
224 | containerPositionY != mContainerPositionY) { |
225 | mContainerPositionX = containerPositionX; |
226 | mContainerPositionY = containerPositionY; |
227 | |
228 | mContainer.update(mContainerPositionX, mContainerPositionY, |
229 | getRight() - getLeft(), getBottom() - getTop()); |
230 | |
231 | // Hide paste popup window as soon as a scroll occurs. |
232 | if (mPastePopupWindow != null) { |
233 | mPastePopupWindow.hide(); |
234 | } |
235 | } |
236 | } else { |
237 | show(); |
238 | } |
239 | |
240 | if (mIsDragging) { |
241 | if (coords == null) { |
242 | coords = mTempCoords; |
243 | mParent.getLocationInWindow(coords); |
244 | } |
245 | if (coords[0] != mLastParentX || coords[1] != mLastParentY) { |
246 | mTouchToWindowOffsetX += coords[0] - mLastParentX; |
247 | mTouchToWindowOffsetY += coords[1] - mLastParentY; |
248 | mLastParentX = coords[0]; |
249 | mLastParentY = coords[1]; |
250 | } |
251 | // Hide paste popup window as soon as the handle is dragged. |
252 | if (mPastePopupWindow != null) { |
253 | mPastePopupWindow.hide(); |
254 | } |
255 | } |
256 | } else { |
257 | hide(); |
258 | } |
259 | } |
260 | |
261 | @Override |
262 | protected void onDraw(Canvas c) { |
263 | updateAlpha(); |
264 | mDrawable.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop()); |
265 | mDrawable.draw(c); |
266 | } |
267 | |
268 | @Override |
269 | public boolean onTouchEvent(MotionEvent ev) { |
270 | switch (ev.getActionMasked()) { |
271 | case MotionEvent.ACTION_DOWN: { |
272 | mDownPositionX = ev.getRawX(); |
273 | mDownPositionY = ev.getRawY(); |
274 | mTouchToWindowOffsetX = mDownPositionX - mPositionX; |
275 | mTouchToWindowOffsetY = mDownPositionY - mPositionY; |
276 | final int[] coords = mTempCoords; |
277 | mParent.getLocationInWindow(coords); |
278 | mLastParentX = coords[0]; |
279 | mLastParentY = coords[1]; |
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 | |
316 | boolean isDragging() { |
317 | return mIsDragging; |
318 | } |
319 | |
320 | /** |
321 | * @return Returns the x position of the handle |
322 | */ |
323 | int getPositionX() { |
324 | return mPositionX; |
325 | } |
326 | |
327 | /** |
328 | * @return Returns the y position of the handle |
329 | */ |
330 | int getPositionY() { |
331 | return mPositionY; |
332 | } |
333 | |
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. |
342 | void positionAt(int x, int y) { |
343 | moveTo((int)(x - mHotspotX), (int)(y - mHotspotY)); |
344 | } |
345 | |
346 | // Returns the x coordinate of the position that the handle appears to be pointing to. |
347 | int getAdjustedPositionX() { |
348 | return (int) (mPositionX + mHotspotX); |
349 | } |
350 | |
351 | // Returns the y coordinate of the position that the handle appears to be pointing to. |
352 | int getAdjustedPositionY() { |
353 | return (int) (mPositionY + mHotspotY); |
354 | } |
355 | |
356 | // Returns a suitable y coordinate for the text position corresponding to the handle. |
357 | // As the handle points to a position on the base of the line of text, this method |
358 | // returns a coordinate a small number of pixels higher (i.e. a slightly smaller number) |
359 | // than getAdjustedPositionY. |
360 | int getLineAdjustedPositionY() { |
361 | return (int) (mPositionY + mHotspotY - mLineOffsetY); |
362 | } |
363 | |
364 | Drawable getDrawable() { |
365 | return mDrawable; |
366 | } |
367 | |
368 | private void updateAlpha() { |
369 | if (mAlpha == 1.f) return; |
370 | mAlpha = Math.min(1.f, (System.currentTimeMillis() - mFadeStartTime) / FADE_DURATION); |
371 | mDrawable.setAlpha((int) (255 * mAlpha)); |
372 | invalidate(); |
373 | } |
374 | |
375 | /** |
376 | * If the handle is not visible, sets its visibility to View.VISIBLE and begins fading it in. |
377 | */ |
378 | void beginFadeIn() { |
379 | if (getVisibility() == VISIBLE) return; |
380 | mAlpha = 0.f; |
381 | mFadeStartTime = System.currentTimeMillis(); |
382 | setVisibility(VISIBLE); |
383 | } |
384 | |
385 | void showPastePopupWindow() { |
386 | InsertionHandleController ihc = (InsertionHandleController) mController; |
387 | if (mIsInsertionHandle && ihc.canPaste()) { |
388 | if (mPastePopupWindow == null) { |
389 | // Lazy initialization: create when actually shown only. |
390 | mPastePopupWindow = ihc.new PastePopupMenu(); |
391 | } |
392 | mPastePopupWindow.show(); |
393 | } |
394 | } |
395 | } |