OLD | NEW |
| (Empty) |
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 suitabl
e | |
73 // y coordinate for the corresponding text position. This is to compensate f
or 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 | |
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.textSelectHan
dleWindowStyle); | |
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_D
IP, | |
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 | |
111 void setOrientation(int pos) { | |
112 Context context = mParent.getContext(); | |
113 TypedArray a = context.getTheme().obtainStyledAttributes(TEXT_VIEW_HANDL
E_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 | |
143 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | |
144 setMeasuredDimension(mDrawable.getIntrinsicWidth(), | |
145 mDrawable.getIntrinsicHeight()); | |
146 } | |
147 | |
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 | |
159 private int getContainerPositionX() { | |
160 return mParentPositionX + mPositionX; | |
161 } | |
162 | |
163 private int getContainerPositionY() { | |
164 return mParentPositionY + mPositionY; | |
165 } | |
166 | |
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 | |
175 private void showContainer() { | |
176 mContainer.showAtLocation(mParent, 0, getContainerPositionX(), getContai
nerPositionY()); | |
177 } | |
178 | |
179 void show() { | |
180 // While hidden, the parent position may have become stale. It must be u
pdated 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 | |
198 void hide() { | |
199 mIsDragging = false; | |
200 mContainer.dismiss(); | |
201 mParentPositionObserver.removeListener(mParentPositionListener); | |
202 if (mPastePopupWindow != null) { | |
203 mPastePopupWindow.hide(); | |
204 } | |
205 } | |
206 | |
207 boolean isShowing() { | |
208 return mContainer.isShowing(); | |
209 } | |
210 | |
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. | |
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 | |
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 | |
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.isSho
wing()) { | |
296 // Tapping on the handle dismisses the displayed pas
te 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 - mLineOf
fsetY; | |
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(x - Math.round(mHotspotX), y - Math.round(mHotspotY)); | |
344 } | |
345 | |
346 // Returns the x coordinate of the position that the handle appears to be po
inting to relative | |
347 // to the handles "parent" view. | |
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 po
inting to relative | |
353 // to the handles "parent" view. | |
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 poi
nting to relative to | |
359 // the root view of the application. | |
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 poi
nting to relative to | |
365 // the root view of the application. | |
366 int getRootViewRelativePositionY() { | |
367 return getContainerPositionY() + Math.round(mHotspotY); | |
368 } | |
369 | |
370 // Returns a suitable y coordinate for the text position corresponding to th
e 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 sma
ller number) | |
373 // than getAdjustedPositionY. | |
374 int getLineAdjustedPositionY() { | |
375 return (int) (mPositionY + mHotspotY - mLineOffsetY); | |
376 } | |
377 | |
378 Drawable getDrawable() { | |
379 return mDrawable; | |
380 } | |
381 | |
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 beg
ins fading it in. | |
392 */ | |
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 | |
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 } | |
OLD | NEW |