Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(475)

Side by Side Diff: content/public/android/java/src/org/chromium/content/browser/PopupZoomer.java

Issue 10828427: Add a view to show magnified link preview on Andrdoid. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Nit fixed. Created 8 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
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;
6
7 import android.content.Context;
8 import android.graphics.Bitmap;
9 import android.graphics.Canvas;
10 import android.graphics.Color;
11 import android.graphics.Paint;
12 import android.graphics.Path;
13 import android.graphics.Path.Direction;
14 import android.graphics.PointF;
15 import android.graphics.PorterDuff.Mode;
16 import android.graphics.PorterDuffXfermode;
17 import android.graphics.Rect;
18 import android.graphics.RectF;
19 import android.graphics.Region.Op;
20 import android.graphics.drawable.Drawable;
21 import android.os.SystemClock;
22 import android.view.GestureDetector;
23 import android.view.MotionEvent;
24 import android.view.View;
25 import android.view.animation.Interpolator;
26 import android.view.animation.OvershootInterpolator;
27
28 import org.chromium.content.app.AppResource;
29
30 /**
31 * PopupZoomer is used to show the on-demand link zooming popup. It handles mani pulation of the
32 * canvas and touch events to display the on-demand zoom magnifier.
33 */
34 class PopupZoomer extends View {
35 // The padding between the edges of the view and the popup. Note that there is a mirror
36 // constant in content/renderer/render_view_impl.cc which should be kept in sync if
37 // this is changed.
38 private static final int ZOOM_BOUNDS_MARGIN = 25;
39 // Time it takes for the animation to finish in ms.
40 private static final long ANIMATION_DURATION = 300;
41
42 /**
43 * Interface to be implemented to listen for touch events inside the zoomed area.
44 * The MotionEvent coordinates correspond to original unzoomed view.
45 */
46 public static interface OnTapListener {
47 public boolean onSingleTap(View v, MotionEvent event);
48 public boolean onLongPress(View v, MotionEvent event);
49 }
50
51 private OnTapListener mOnTapListener = null;
52
53 // Cached drawable used to frame the zooming popup.
54 // TODO(tonyg): This should be marked purgeable so that if the system wants to recover this
55 // memory, we can just reload it from the resource ID next time it is needed .
56 // See android.graphics.BitmapFactory.Options#inPurgeable
57 private static Drawable sOverlayDrawable;
58 // The padding used for drawing the overlay around the content, instead of d irectly above it.
59 private static Rect sOverlayPadding;
60 // The radius of the overlay bubble, used for rounding the bitmap to draw un derneath it.
61 private static float sOverlayCornerRadius;
62
63 private Interpolator mShowInterpolator = new OvershootInterpolator();
64 private Interpolator mHideInterpolator = new ReverseInterpolator(mShowInterp olator);
65
66 private boolean mAnimating = false;
67 private boolean mShowing = false;
68 private long mAnimationStartTime = 0;
69
70 // The time that was left for the outwards animation to finish.
71 // This is used in the case that the zoomer is cancelled while it is still a nimating outwards,
72 // to avoid having it jump to full size then animate closed.
73 private long mTimeLeft = 0;
74
75 // Available view area after accounting for ZOOM_BOUNDS_MARGIN.
76 private RectF mViewClipRect;
77
78 // The target rect to be zoomed.
79 private Rect mTargetBounds;
80
81 // The bitmap to hold the zoomed view.
82 private Bitmap mZoomedBitmap;
83
84 // How far to shift the canvas after all zooming is done, to keep it inside the bounds of the
85 // view (including margin).
86 private float mShiftX = 0, mShiftY = 0;
87 // The magnification factor of the popup. It is recomputed once we have mTar getBounds and
88 // mZoomedBitmap.
89 private float mScale = 1.0f;
90 // The bounds representing the actual zoomed popup.
91 private RectF mClipRect;
92 // The extrusion values are how far the zoomed area (mClipRect) extends from the touch point.
93 // These values to used to animate the popup.
94 private float mLeftExtrusion, mTopExtrusion, mRightExtrusion, mBottomExtrusi on;
95 // The last touch point, where the animation will start from.
96 private PointF mTouch = new PointF();
97
98 // Since we sometimes overflow the bounds of the mViewClipRect, we need to a llow scrolling.
99 // Current scroll position.
100 private float mPopupScrollX, mPopupScrollY;
101 // Scroll bounds.
102 private float mMinScrollX, mMaxScrollX;
103 private float mMinScrollY, mMaxScrollY;
104
105 private GestureDetector mGestureDetector;
106
107 /**
108 * Gets the drawable that should be used to frame the zooming popup, loading
109 * it from the resource bundle if not already cached.
110 */
111 protected Drawable getOverlayDrawable() {
112 if (sOverlayDrawable == null) {
113 sOverlayDrawable = loadOverlayDrawable();
114 sOverlayPadding = new Rect();
115 sOverlayDrawable.getPadding(sOverlayPadding);
116 }
117 return sOverlayDrawable;
118 }
119
120 /**
121 * Loads the overlay drawable from the resource bundle.
122 *
123 * @VisibleForTesting
124 */
125 protected Drawable loadOverlayDrawable() {
126 assert AppResource.DRAWABLE_LINK_PREVIEW_POPUP_OVERLAY != 0;
127 return getContext().getResources().getDrawable(
128 AppResource.DRAWABLE_LINK_PREVIEW_POPUP_OVERLAY);
129 }
130
131 private static float constrain(float amount, float low, float high) {
132 return amount < low ? low : (amount > high ? high : amount);
133 }
134
135 private static int constrain(int amount, int low, int high) {
136 return amount < low ? low : (amount > high ? high : amount);
137 }
138
139 /**
140 * Creates Popupzoomer.
141 * @param context Context to be used.
142 * @param overlayRadiusDimensoinResId Resource to be used to get overlay cor ner radius.
143 */
144 public PopupZoomer(Context context, int overlayRadiusDimensoinResId) {
145 super(context);
146
147 if (overlayRadiusDimensoinResId != 0) {
148 sOverlayCornerRadius = context.getResources().getDimension(overlayRa diusDimensoinResId);
149 } else {
150 sOverlayCornerRadius = 0;
151 }
152
153 setVisibility(INVISIBLE);
154 setFocusable(true);
155 setFocusableInTouchMode(true);
156
157 GestureDetector.SimpleOnGestureListener listener =
158 new GestureDetector.SimpleOnGestureListener() {
159 @Override
160 public boolean onScroll(MotionEvent e1, MotionEvent e2,
161 float distanceX, float distanceY) {
162 if (mAnimating) return true;
163
164 if (isTouchOutsideArea(e1.getX(), e1.getY())) {
165 hide(true);
166 } else {
167 scroll(distanceX, distanceY);
168 }
169 return true;
170 }
171
172 @Override
173 public boolean onSingleTapUp(MotionEvent e) {
174 return handleTapOrPress(e, false);
175 }
176
177 @Override
178 public void onLongPress(MotionEvent e) {
179 handleTapOrPress(e, true);
180 }
181
182 private boolean handleTapOrPress(MotionEvent e, boolean isLongPr ess) {
183 if (mAnimating) return true;
184
185 float x = e.getX();
186 float y = e.getY();
187 if (isTouchOutsideArea(x, y)) {
188 // User clicked on area outside the popup.
189 hide(true);
190 } else if (mOnTapListener != null) {
191 PointF converted = convertTouchPoint(x, y);
192 MotionEvent event = MotionEvent.obtainNoHistory(e);
193 event.setLocation(converted.x, converted.y);
194 if (isLongPress) {
195 mOnTapListener.onLongPress(PopupZoomer.this, event);
196 } else {
197 mOnTapListener.onSingleTap(PopupZoomer.this, event);
198 }
199 hide(true);
200 }
201 return true;
202 }
203 };
204 mGestureDetector = new GestureDetector(context, listener);
205 }
206
207 /**
208 * Sets the OnTapListener.
209 */
210 public void setOnTapListener(OnTapListener listener) {
211 mOnTapListener = listener;
212 }
213
214 /**
215 * Sets the bitmap to be used for the zoomed view.
216 */
217 public void setBitmap(Bitmap bitmap) {
218 if (mZoomedBitmap != null) {
219 mZoomedBitmap.recycle();
220 mZoomedBitmap = null;
221 }
222 mZoomedBitmap = bitmap;
223 // Round the corners of the bitmap so it doesn't stick out around the ov erlay.
224 Canvas canvas = new Canvas(mZoomedBitmap);
225 Path path = new Path();
226 RectF canvasRect = new RectF(0, 0, canvas.getWidth(), canvas.getHeight() );
227 path.addRoundRect(canvasRect, sOverlayCornerRadius, sOverlayCornerRadius , Direction.CCW);
228 canvas.clipPath(path, Op.XOR);
229 Paint clearPaint = new Paint();
230 clearPaint.setXfermode(new PorterDuffXfermode(Mode.SRC));
231 clearPaint.setColor(Color.TRANSPARENT);
232 canvas.drawPaint(clearPaint);
233 }
234
235 private void scroll(float x, float y) {
236 mPopupScrollX = constrain(mPopupScrollX - x, mMinScrollX, mMaxScrollX);
237 mPopupScrollY = constrain(mPopupScrollY - y, mMinScrollY, mMaxScrollY);
238 invalidate();
239 }
240
241 private void startAnimation(boolean show) {
242 mAnimating = true;
243 mShowing = show;
244 mTimeLeft = 0;
245 if (show) {
246 setVisibility(VISIBLE);
247 initDimensions();
248 } else {
249 long endTime = mAnimationStartTime + ANIMATION_DURATION;
250 mTimeLeft = endTime - SystemClock.uptimeMillis();
251 if (mTimeLeft < 0) mTimeLeft = 0;
252 }
253 mAnimationStartTime = SystemClock.uptimeMillis();
254 invalidate();
255 }
256
257 private void hideImmediately() {
258 mAnimating = false;
259 mShowing = false;
260 mTimeLeft = 0;
261 setVisibility(INVISIBLE);
262 mZoomedBitmap.recycle();
263 mZoomedBitmap = null;
264 }
265
266 /**
267 * Returns true if the view is currently being shown (or is animating).
268 */
269 public boolean isShowing() {
270 return mShowing || mAnimating;
271 }
272
273 /**
274 * Sets the last touch point (on the unzoomed view).
275 */
276 public void setLastTouch(float x, float y) {
277 mTouch.x = x;
278 mTouch.y = y;
279 }
280
281 private void setTargetBounds(Rect rect) {
282 mViewClipRect = new RectF(ZOOM_BOUNDS_MARGIN,
283 ZOOM_BOUNDS_MARGIN,
284 getWidth() - ZOOM_BOUNDS_MARGIN,
285 getHeight() - ZOOM_BOUNDS_MARGIN);
286 mTargetBounds = rect;
287 }
288
289 private void initDimensions() {
290 if (mTargetBounds == null || mTouch == null) return;
291
292 // Compute the final zoom scale.
293 mScale = (float) mZoomedBitmap.getWidth() / mTargetBounds.width();
294
295 float l = mTouch.x - mScale * (mTouch.x - mTargetBounds.left);
296 float t = mTouch.y - mScale * (mTouch.y - mTargetBounds.top);
297 float r = l + mZoomedBitmap.getWidth();
298 float b = t + mZoomedBitmap.getHeight();
299 mClipRect = new RectF(l, t, r, b);
300 int width = getWidth();
301 int height = getHeight();
302
303 // Ensure it stays inside the bounds of the view. First shift it around to see if it
304 // can fully fit in the view, then clip it to the padding section of the view to
305 // ensure no overflow.
306 mShiftX = 0;
307 mShiftY = 0;
308
309 // Right now this has the happy coincidence of showing the leftmost port ion
310 // of a scaled up bitmap, which usually has the text in it. When we wan t to support
311 // RTL languages, we can conditionally switch the order of this check to push it
312 // to the left instead of right.
313 if (mClipRect.left < ZOOM_BOUNDS_MARGIN) {
314 mShiftX = ZOOM_BOUNDS_MARGIN - mClipRect.left;
315 mClipRect.left += mShiftX;
316 mClipRect.right += mShiftX;
317 } else if (mClipRect.right > width - ZOOM_BOUNDS_MARGIN) {
318 mShiftX = (width - ZOOM_BOUNDS_MARGIN - mClipRect.right);
319 mClipRect.right += mShiftX;
320 mClipRect.left += mShiftX;
321 }
322 if (mClipRect.top < ZOOM_BOUNDS_MARGIN) {
323 mShiftY = ZOOM_BOUNDS_MARGIN - mClipRect.top;
324 mClipRect.top += mShiftY;
325 mClipRect.bottom += mShiftY;
326 } else if (mClipRect.bottom > height - ZOOM_BOUNDS_MARGIN) {
327 mShiftY = height - ZOOM_BOUNDS_MARGIN - mClipRect.bottom;
328 mClipRect.bottom += mShiftY;
329 mClipRect.top += mShiftY;
330 }
331
332 // Allow enough scrolling to get to the entire bitmap that may be clippe d inside the
333 // bounds of the view.
334 mMinScrollX = mMaxScrollX = mMinScrollY = mMaxScrollY = 0;
335 if (mViewClipRect.right + mShiftX < mClipRect.right) {
336 mMinScrollX = mViewClipRect.right - mClipRect.right;
337 }
338 if (mViewClipRect.left + mShiftX > mClipRect.left) {
339 mMaxScrollX = mViewClipRect.left - mClipRect.left;
340 }
341 if (mViewClipRect.top + mShiftY > mClipRect.top) {
342 mMaxScrollY = mViewClipRect.top - mClipRect.top;
343 }
344 if (mViewClipRect.bottom + mShiftY < mClipRect.bottom) {
345 mMinScrollY = mViewClipRect.bottom - mClipRect.bottom;
346 }
347 // Now that we know how much we need to scroll, we can intersect with mV iewClipRect.
348 mClipRect.intersect(mViewClipRect);
349
350 mLeftExtrusion = mTouch.x - mClipRect.left;
351 mRightExtrusion = mClipRect.right - mTouch.x;
352 mTopExtrusion = mTouch.y - mClipRect.top;
353 mBottomExtrusion = mClipRect.bottom - mTouch.y;
354
355 // Set an initial scroll position to take touch point into account.
356 float percentX =
357 (mTouch.x - mTargetBounds.centerX()) / (mTargetBounds.width() / 2.f) + .5f;
358 float percentY =
359 (mTouch.y - mTargetBounds.centerY()) / (mTargetBounds.height() / 2.f) + .5f;
360
361 float scrollWidth = mMaxScrollX - mMinScrollX;
362 float scrollHeight = mMaxScrollY - mMinScrollY;
363 mPopupScrollX = scrollWidth * percentX * -1f;
364 mPopupScrollY = scrollHeight * percentY * -1f;
365 // Constrain initial scroll position within allowed bounds.
366 mPopupScrollX = constrain(mPopupScrollX, mMinScrollX, mMaxScrollX);
367 mPopupScrollY = constrain(mPopupScrollY, mMinScrollY, mMaxScrollY);
368 }
369
370 @Override
371 protected void onDraw(Canvas canvas) {
372 if (!isShowing() || mZoomedBitmap == null) return;
373 canvas.save();
374 // Calculate the elapsed fraction of animation.
375 float time = (SystemClock.uptimeMillis() - mAnimationStartTime + mTimeLe ft) /
376 ((float) ANIMATION_DURATION);
377 time = constrain(time, 0, 1);
378 if (time >= 1) {
379 mAnimating = false;
380 if (!isShowing()) {
381 hideImmediately();
382 return;
383 }
384 } else {
385 invalidate();
386 }
387
388 // Fraction of the animation to actally show.
389 float fractionAnimation;
390 if (mShowing) {
391 fractionAnimation = mShowInterpolator.getInterpolation(time);
392 } else {
393 fractionAnimation = mHideInterpolator.getInterpolation(time);
394 }
395
396 // Draw a faded color over the entire view to fade out the original cont ent, increasing
397 // the alpha value as fractionAnimation increases.
398 // TODO(nileshagrawal): We should use time here instead of fractionAnima tion
399 // as fractionAnimaton is interpolated and can go over 1.
400 canvas.drawARGB((int) (80 * fractionAnimation), 0, 0, 0);
401 canvas.save();
402
403 // Since we want the content to appear directly above its counterpart we need to make
404 // sure that it starts out at exactly the same size as it appears in the page,
405 // i.e. scale grows from 1/mScale to 1. Note that extrusion values are a lready zoomed
406 // with mScale.
407 float scale = fractionAnimation * (mScale - 1.0f) / mScale + 1.0f / mSca le;
408
409 // Since we want the content to appear directly above its counterpart on the
410 // page, we need to remove the mShiftX/Y effect at the beginning of the animation.
411 // The unshifting decreases with the animation.
412 float unshiftX = - mShiftX * (1.0f - fractionAnimation) / mScale;
413 float unshiftY = - mShiftY * (1.0f - fractionAnimation) / mScale;
414
415 // Compute the rect to show.
416 RectF rect = new RectF();
417 rect.left = mTouch.x - mLeftExtrusion * scale + unshiftX;
418 rect.top = mTouch.y - mTopExtrusion * scale + unshiftY;
419 rect.right = mTouch.x + mRightExtrusion * scale + unshiftX;
420 rect.bottom = mTouch.y + mBottomExtrusion * scale + unshiftY;
421 canvas.clipRect(rect);
422
423 // Since the canvas transform APIs all pre-concat the transformations, t his is done in
424 // reverse order. The canvas is first scaled up, then shifted the approp riate amount of
425 // pixels.
426 canvas.scale(scale, scale, rect.left, rect.top);
427 canvas.translate(mPopupScrollX, mPopupScrollY);
428 canvas.drawBitmap(mZoomedBitmap, rect.left, rect.top, null);
429 canvas.restore();
430 Drawable overlayNineTile = getOverlayDrawable();
431 overlayNineTile.setBounds((int) rect.left - sOverlayPadding.left,
432 (int) rect.top - sOverlayPadding.top,
433 (int) rect.right + sOverlayPadding.right,
434 (int) rect.bottom + sOverlayPadding.bottom);
435 // TODO(nileshagrawal): We should use time here instead of fractionAnima tion
436 // as fractionAnimaton is interpolated and can go over 1.
437 int alpha = constrain((int) (fractionAnimation * 255), 0, 255);
438 overlayNineTile.setAlpha(alpha);
439 overlayNineTile.draw(canvas);
440 canvas.restore();
441 }
442
443 /**
444 * Show the PopupZoomer view with given target bounds.
445 */
446 public void show(Rect rect){
447 if (mShowing || mZoomedBitmap == null) return;
448
449 setTargetBounds(rect);
450 startAnimation(true);
451 }
452
453 /**
454 * Hide the PopupZoomer view.
455 * @param animation true if hide with animation.
456 */
457 public void hide(boolean animation){
458 if (!mShowing) return;
459
460 if (animation) {
461 startAnimation(false);
462 } else {
463 hideImmediately();
464 }
465 }
466
467 /**
468 * Converts the coordinates to a point on the original un-zoomed view.
469 */
470 private PointF convertTouchPoint(float x, float y) {
471 x -= mShiftX;
472 y -= mShiftY;
473 x = mTouch.x + (x - mTouch.x - mPopupScrollX) / mScale;
474 y = mTouch.y + (y - mTouch.y - mPopupScrollY) / mScale;
475 return new PointF(x, y);
476 }
477
478 /**
479 * Returns true if the point is inside the final drawable area for this popu p zoomer.
480 */
481 private boolean isTouchOutsideArea(float x, float y) {
482 return !mClipRect.contains(x, y);
483 }
484
485 @Override
486 public boolean onTouchEvent(MotionEvent event) {
487 mGestureDetector.onTouchEvent(event);
488 return true;
489 }
490
491 private static class ReverseInterpolator implements Interpolator {
492 private Interpolator mInterpolator;
493
494 public ReverseInterpolator(Interpolator i) {
495 mInterpolator = i;
496 }
497
498 @Override
499 public float getInterpolation(float input) {
500 input = 1.0f - input;
501 if (mInterpolator == null) return input;
502 return mInterpolator.getInterpolation(input);
503 }
504 }
505 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698