blob: 7cdf55e7b02a05d4fe43de140511e2ba945e284e [file] [log] [blame]
The Android Open Source Project1d4c7502008-10-21 07:00:00 -07001package com.android.camera;
2
3import android.content.Context;
4import android.graphics.Bitmap;
5import android.graphics.Canvas;
6import android.graphics.Matrix;
7import android.graphics.Paint;
8import android.graphics.drawable.Drawable;
9import android.os.Handler;
10import android.os.SystemClock;
11import android.util.AttributeSet;
12import android.util.Config;
13import android.util.Log;
14import android.view.animation.Animation;
15import android.view.animation.TranslateAnimation;
The Android Open Source Project0eaec582008-12-17 18:05:54 -080016import android.view.KeyEvent;
The Android Open Source Project1d4c7502008-10-21 07:00:00 -070017import android.widget.ImageView;
18
19abstract public class ImageViewTouchBase extends ImageView {
20 private static final String TAG = "ImageViewTouchBase";
21
22 // if we're animating these images, it may be faster to cache the image
23 // at its target size first. to do this set this variable to true.
24 // currently we're not animating images, so we don't need to do this
25 // extra work.
26 private final boolean USE_PERFECT_FIT_OPTIMIZATION = false;
27
28 // This is the base transformation which is used to show the image
29 // initially. The current computation for this shows the image in
30 // it's entirety, letterboxing as needed. One could chose to
31 // show the image as cropped instead.
32 //
33 // This matrix is recomputed when we go from the thumbnail image to
34 // the full size image.
35 protected Matrix mBaseMatrix = new Matrix();
36
37 // This is the supplementary transformation which reflects what
38 // the user has done in terms of zooming and panning.
39 //
40 // This matrix remains the same when we go from the thumbnail image
41 // to the full size image.
42 protected Matrix mSuppMatrix = new Matrix();
43
44 // This is the final matrix which is computed as the concatentation
45 // of the base matrix and the supplementary matrix.
46 private Matrix mDisplayMatrix = new Matrix();
47
48 // Temporary buffer used for getting the values out of a matrix.
49 private float[] mMatrixValues = new float[9];
50
51 // The current bitmap being displayed.
52 protected Bitmap mBitmapDisplayed;
53
54 // The thumbnail bitmap.
55 protected Bitmap mThumbBitmap;
56
57 // The full size bitmap which should be used once we start zooming.
58 private Bitmap mFullBitmap;
59
60 // The bitmap which is exactly sized to what we need. The decoded bitmap is
61 // drawn into the mPerfectFitBitmap so that animation is faster.
62 protected Bitmap mPerfectFitBitmap;
63
64 // True if the image is the thumbnail.
65 protected boolean mBitmapIsThumbnail;
66
67 // True if the user is zooming -- use the full size image
68 protected boolean mIsZooming;
69
70 // Paint to use to clear the "mPerfectFitBitmap"
71 protected Paint mPaint = new Paint();
72
73 static boolean sNewZoomControl = false;
74
75 int mThisWidth = -1, mThisHeight = -1;
76
77 float mMaxZoom;
78
79 @Override
80 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
81 super.onLayout(changed, left, top, right, bottom);
82 mThisWidth = right - left;
83 mThisHeight = bottom - top;
84 Runnable r = mOnLayoutRunnable;
85 if (r != null) {
86 mOnLayoutRunnable = null;
87 r.run();
88 }
89 if (mBitmapDisplayed != null) {
90 setBaseMatrix(mBitmapDisplayed, mBaseMatrix);
91 setImageMatrix(getImageViewMatrix());
92 }
93 }
94
The Android Open Source Project0eaec582008-12-17 18:05:54 -080095 @Override
96 public boolean onKeyDown(int keyCode, KeyEvent event) {
97 if (keyCode == KeyEvent.KEYCODE_BACK && getScale() > 1.0f) {
98 // If we're zoomed in, pressing Back jumps out to show the entire image, otherwise Back
99 // returns the user to the gallery.
100 zoomTo(1.0f);
101 return true;
102 }
103 return super.onKeyDown(keyCode, event);
104 }
105
The Android Open Source Project1d4c7502008-10-21 07:00:00 -0700106 protected Handler mHandler = new Handler();
107
108 protected int mLastXTouchPos;
109 protected int mLastYTouchPos;
110
111 protected boolean doesScrolling() {
112 return true;
113 }
114
115 // Translate a given point through a given matrix.
116 static private void translatePoint(Matrix matrix, float [] xy) {
117 matrix.mapPoints(xy);
118 }
119
120 // Return the mapped x coordinate through the matrix.
121 static int mapXPoint(Matrix matrix, int point) {
122 // Matrix's mapPoints takes an array of x/y coordinates.
123 // That's why we have to allocte an array of length two
124 // even though we don't use the y coordinate.
125 float [] xy = new float[2];
126 xy[0] = point;
127 xy[1] = 0F;
128 matrix.mapPoints(xy);
129 return (int) xy[0];
130 }
131
132 @Override
133 public void setImageBitmap(Bitmap bitmap) {
134 throw new NullPointerException();
135 }
136
137 public void setImageBitmap(Bitmap bitmap, boolean isThumbnail) {
138 super.setImageBitmap(bitmap);
139 Drawable d = getDrawable();
140 if (d != null)
141 d.setDither(true);
142 mBitmapDisplayed = bitmap;
143 mBitmapIsThumbnail = isThumbnail;
144 }
145
146 protected boolean usePerfectFitBitmap() {
147 return USE_PERFECT_FIT_OPTIMIZATION && !mIsZooming;
148 }
149
150 public void recycleBitmaps() {
151 if (mFullBitmap != null) {
152 if (Config.LOGV)
153 Log.v(TAG, "recycling mFullBitmap " + mFullBitmap + "; this == " + this.hashCode());
154 mFullBitmap.recycle();
155 mFullBitmap = null;
156 }
157 if (mThumbBitmap != null) {
158 if (Config.LOGV)
159 Log.v(TAG, "recycling mThumbBitmap" + mThumbBitmap + "; this == " + this.hashCode());
160 mThumbBitmap.recycle();
161 mThumbBitmap = null;
162 }
163
164 // mBitmapDisplayed is either mPerfectFitBitmap or mFullBitmap (in the case of zooming)
165 setImageBitmap(null, true);
166 }
167
168 public void clear() {
169 mBitmapDisplayed = null;
170 recycleBitmaps();
171 }
172
173 private Runnable mOnLayoutRunnable = null;
174
175 public void setImageBitmapResetBase(final Bitmap bitmap, final boolean resetSupp, final boolean isThumb) {
176 if ((bitmap != null) && (bitmap == mPerfectFitBitmap)) {
177 // TODO: this should be removed in production
178 throw new IllegalArgumentException("bitmap must not be mPerfectFitBitmap");
179 }
180
181 final int viewWidth = getWidth();
182 final int viewHeight = getHeight();
183
184 if (viewWidth <= 0) {
185 mOnLayoutRunnable = new Runnable() {
186 public void run() {
187 setImageBitmapResetBase(bitmap, resetSupp, isThumb);
188 }
189 };
190 return;
191 }
192
193 if (isThumb && mThumbBitmap != bitmap) {
194 if (mThumbBitmap != null) {
195 mThumbBitmap.recycle();
196 }
197 mThumbBitmap = bitmap;
198 } else if (!isThumb && mFullBitmap != bitmap) {
199 if (mFullBitmap != null) {
200 mFullBitmap.recycle();
201 }
202 mFullBitmap = bitmap;
203 }
204 mBitmapIsThumbnail = isThumb;
205
206 if (bitmap != null) {
207 if (!usePerfectFitBitmap()) {
208 setScaleType(ImageView.ScaleType.MATRIX);
209 setBaseMatrix(bitmap, mBaseMatrix);
210 setImageBitmap(bitmap, isThumb);
211 } else {
212 Matrix matrix = new Matrix();
213 setBaseMatrix(bitmap, matrix);
214 if ((mPerfectFitBitmap == null) ||
215 mPerfectFitBitmap.getWidth() != mThisWidth ||
216 mPerfectFitBitmap.getHeight() != mThisHeight) {
217 if (mPerfectFitBitmap != null) {
218 if (Config.LOGV)
219 Log.v(TAG, "recycling mPerfectFitBitmap " + mPerfectFitBitmap.hashCode());
220 mPerfectFitBitmap.recycle();
221 }
222 mPerfectFitBitmap = Bitmap.createBitmap(mThisWidth, mThisHeight, Bitmap.Config.RGB_565);
223 }
224 Canvas canvas = new Canvas(mPerfectFitBitmap);
225 // clear the bitmap which may be bigger than the image and
226 // contain the the previous image.
227 canvas.drawColor(0xFF000000);
228
229 final int bw = bitmap.getWidth();
230 final int bh = bitmap.getHeight();
231 final float widthScale = Math.min(viewWidth / (float)bw, 1.0f);
232 final float heightScale = Math.min(viewHeight/ (float)bh, 1.0f);
233 int translateX, translateY;
234 if (widthScale > heightScale) {
235 translateX = (int)((viewWidth -(float)bw*heightScale)*0.5f);
236 translateY = (int)((viewHeight-(float)bh*heightScale)*0.5f);
237 } else {
238 translateX = (int)((viewWidth -(float)bw*widthScale)*0.5f);
239 translateY = (int)((viewHeight-(float)bh*widthScale)*0.5f);
240 }
241
242 android.graphics.Rect src = new android.graphics.Rect(0, 0, bw, bh);
243 android.graphics.Rect dst = new android.graphics.Rect(
244 translateX, translateY,
245 mThisWidth - translateX, mThisHeight - translateY);
246 canvas.drawBitmap(bitmap, src, dst, mPaint);
247
248 setImageBitmap(mPerfectFitBitmap, isThumb);
249 setScaleType(ImageView.ScaleType.MATRIX);
250 setImageMatrix(null);
251 }
252 } else {
253 mBaseMatrix.reset();
254 setImageBitmap(null, isThumb);
255 }
256
257 if (resetSupp)
258 mSuppMatrix.reset();
259 setImageMatrix(getImageViewMatrix());
260 mMaxZoom = maxZoom();
261 }
262
263 // Center as much as possible in one or both axis. Centering is
264 // defined as follows: if the image is scaled down below the
265 // view's dimensions then center it (literally). If the image
266 // is scaled larger than the view and is translated out of view
267 // then translate it back into view (i.e. eliminate black bars).
268 protected void center(boolean vertical, boolean horizontal, boolean animate) {
269 if (mBitmapDisplayed == null)
270 return;
271
272 Matrix m = getImageViewMatrix();
273
274 float [] topLeft = new float[] { 0, 0 };
275 float [] botRight = new float[] { mBitmapDisplayed.getWidth(), mBitmapDisplayed.getHeight() };
276
277 translatePoint(m, topLeft);
278 translatePoint(m, botRight);
279
280 float height = botRight[1] - topLeft[1];
281 float width = botRight[0] - topLeft[0];
282
283 float deltaX = 0, deltaY = 0;
284
285 if (vertical) {
286 int viewHeight = getHeight();
287 if (height < viewHeight) {
288 deltaY = (viewHeight - height)/2 - topLeft[1];
289 } else if (topLeft[1] > 0) {
290 deltaY = -topLeft[1];
291 } else if (botRight[1] < viewHeight) {
292 deltaY = getHeight() - botRight[1];
293 }
294 }
295
296 if (horizontal) {
297 int viewWidth = getWidth();
298 if (width < viewWidth) {
299 deltaX = (viewWidth - width)/2 - topLeft[0];
300 } else if (topLeft[0] > 0) {
301 deltaX = -topLeft[0];
302 } else if (botRight[0] < viewWidth) {
303 deltaX = viewWidth - botRight[0];
304 }
305 }
306
307 postTranslate(deltaX, deltaY);
308 if (animate) {
309 Animation a = new TranslateAnimation(-deltaX, 0, -deltaY, 0);
310 a.setStartTime(SystemClock.elapsedRealtime());
311 a.setDuration(250);
312 setAnimation(a);
313 }
314 setImageMatrix(getImageViewMatrix());
315 }
316
317 public void copyFrom(ImageViewTouchBase other) {
318 mSuppMatrix.set(other.mSuppMatrix);
319 mBaseMatrix.set(other.mBaseMatrix);
320
321 if (mThumbBitmap != null)
322 mThumbBitmap.recycle();
323
324 if (mFullBitmap != null)
325 mFullBitmap.recycle();
326
327 // copy the data
328 mThumbBitmap = other.mThumbBitmap;
329 mFullBitmap = null;
330
331 if (other.mFullBitmap != null)
332 other.mFullBitmap.recycle();
333
334 // transfer "ownership"
335 other.mThumbBitmap = null;
336 other.mFullBitmap = null;
337 other.mBitmapIsThumbnail = true;
338
339 setImageMatrix(other.getImageMatrix());
340 setScaleType(other.getScaleType());
341
342 setImageBitmapResetBase(mThumbBitmap, true, true);
343 }
344
345 @Override
346 public void setImageDrawable(android.graphics.drawable.Drawable d) {
347 super.setImageDrawable(d);
348 }
349
350 public ImageViewTouchBase(Context context) {
351 super(context);
352 init();
353 }
354
355 public ImageViewTouchBase(Context context, AttributeSet attrs) {
356 super(context, attrs);
357 init();
358 }
359
360 private void init() {
361 setScaleType(ImageView.ScaleType.MATRIX);
362 mPaint.setDither(true);
363 mPaint.setFilterBitmap(true);
364 }
365
366 protected float getValue(Matrix matrix, int whichValue) {
367 matrix.getValues(mMatrixValues);
368 return mMatrixValues[whichValue];
369 }
370
371 // Get the scale factor out of the matrix.
372 protected float getScale(Matrix matrix) {
373 return getValue(matrix, Matrix.MSCALE_X);
374 }
375
376 protected float getScale() {
377 return getScale(mSuppMatrix);
378 }
379
380 protected float getTranslateX() {
381 return getValue(mSuppMatrix, Matrix.MTRANS_X);
382 }
383
384 protected float getTranslateY() {
385 return getValue(mSuppMatrix, Matrix.MTRANS_Y);
386 }
387
388 // Setup the base matrix so that the image is centered and scaled properly.
389 private void setBaseMatrix(Bitmap bitmap, Matrix matrix) {
390 float viewWidth = getWidth();
391 float viewHeight = getHeight();
392
393 matrix.reset();
394 float widthScale = Math.min(viewWidth / (float)bitmap.getWidth(), 1.0f);
395 float heightScale = Math.min(viewHeight / (float)bitmap.getHeight(), 1.0f);
396 float scale;
397 if (widthScale > heightScale) {
398 scale = heightScale;
399 } else {
400 scale = widthScale;
401 }
402 matrix.setScale(scale, scale);
403 matrix.postTranslate(
404 (viewWidth - ((float)bitmap.getWidth() * scale))/2F,
405 (viewHeight - ((float)bitmap.getHeight() * scale))/2F);
406 }
407
408 // Combine the base matrix and the supp matrix to make the final matrix.
409 protected Matrix getImageViewMatrix() {
410 mDisplayMatrix.set(mBaseMatrix);
411 mDisplayMatrix.postConcat(mSuppMatrix);
412 return mDisplayMatrix;
413 }
414
415 private void onZoom() {
416 mIsZooming = true;
417 if (mFullBitmap != null && mFullBitmap != mBitmapDisplayed) {
418 setImageBitmapResetBase(mFullBitmap, false, mBitmapIsThumbnail);
419 }
420 }
421
422 private String describe(Bitmap b) {
423 StringBuilder sb = new StringBuilder();
424 if (b == null) {
425 sb.append("NULL");
426 } else if (b.isRecycled()) {
427 sb.append(String.format("%08x: RECYCLED", b.hashCode()));
428 } else {
429 sb.append(String.format("%08x: LIVE", b.hashCode()));
430 sb.append(String.format("%d x %d (size == %d)", b.getWidth(), b.getHeight(), b.getWidth()*b.getHeight()*2));
431 }
432 return sb.toString();
433 }
434
435 public void dump() {
436 if (Config.LOGV) {
437 Log.v(TAG, "dump ImageViewTouchBase " + this);
438 Log.v(TAG, "... mBitmapDisplayed = " + describe(mBitmapDisplayed));
439 Log.v(TAG, "... mThumbBitmap = " + describe(mThumbBitmap));
440 Log.v(TAG, "... mFullBitmap = " + describe(mFullBitmap));
441 Log.v(TAG, "... mPerfectFitBitmap = " + describe(mPerfectFitBitmap));
442 Log.v(TAG, "... mIsThumb = " + mBitmapIsThumbnail);
443 }
444 }
445
446 static final float sPanRate = 7;
447 static final float sScaleRate = 1.05F;
448
449 // Sets the maximum zoom, which is a scale relative to the base matrix. It is calculated to show
450 // the image at 400% zoom regardless of screen or image orientation. If in the future we decode
451 // the full 3 megapixel image, rather than the current 1024x768, this should be changed down to
452 // 200%.
453 protected float maxZoom() {
454 if (mBitmapDisplayed == null)
455 return 1F;
456
457 float fw = (float) mBitmapDisplayed.getWidth() / (float)mThisWidth;
458 float fh = (float) mBitmapDisplayed.getHeight() / (float)mThisHeight;
459 float max = Math.max(fw, fh) * 4;
460// Log.v(TAG, "Bitmap " + mBitmapDisplayed.getWidth() + "x" + mBitmapDisplayed.getHeight() +
461// " view " + mThisWidth + "x" + mThisHeight + " max zoom " + max);
462 return max;
463 }
464
465 protected void zoomTo(float scale, float centerX, float centerY) {
466 if (scale > mMaxZoom) {
467 scale = mMaxZoom;
468 }
469 onZoom();
470
471 float oldScale = getScale();
472 float deltaScale = scale / oldScale;
473
474 mSuppMatrix.postScale(deltaScale, deltaScale, centerX, centerY);
475 setImageMatrix(getImageViewMatrix());
476 center(true, true, false);
477 }
478
479 protected void zoomTo(final float scale, final float centerX, final float centerY, final float durationMs) {
480 final float incrementPerMs = (scale - getScale()) / durationMs;
481 final float oldScale = getScale();
482 final long startTime = System.currentTimeMillis();
483
484 mHandler.post(new Runnable() {
485 public void run() {
486 long now = System.currentTimeMillis();
487 float currentMs = Math.min(durationMs, (float)(now - startTime));
488 float target = oldScale + (incrementPerMs * currentMs);
489 zoomTo(target, centerX, centerY);
490
491 if (currentMs < durationMs) {
492 mHandler.post(this);
493 }
494 }
495 });
496 }
497
498 protected void zoomTo(float scale) {
499 float width = getWidth();
500 float height = getHeight();
501
502 zoomTo(scale, width/2F, height/2F);
503 }
504
505 protected void zoomIn() {
506 zoomIn(sScaleRate);
507 }
508
509 protected void zoomOut() {
510 zoomOut(sScaleRate);
511 }
512
513 protected void zoomIn(float rate) {
514 if (getScale() >= mMaxZoom) {
515 return; // Don't let the user zoom into the molecular level.
516 }
517 if (mBitmapDisplayed == null) {
518 return;
519 }
520 float width = getWidth();
521 float height = getHeight();
522
523 mSuppMatrix.postScale(rate, rate, width/2F, height/2F);
524 setImageMatrix(getImageViewMatrix());
525
526 onZoom();
527 }
528
529 protected void zoomOut(float rate) {
530 if (mBitmapDisplayed == null) {
531 return;
532 }
533
534 float width = getWidth();
535 float height = getHeight();
536
537 Matrix tmp = new Matrix(mSuppMatrix);
538 tmp.postScale(1F/sScaleRate, 1F/sScaleRate, width/2F, height/2F);
539 if (getScale(tmp) < 1F) {
540 mSuppMatrix.setScale(1F, 1F, width/2F, height/2F);
541 } else {
542 mSuppMatrix.postScale(1F/rate, 1F/rate, width/2F, height/2F);
543 }
544 setImageMatrix(getImageViewMatrix());
545 center(true, true, false);
546
547 onZoom();
548 }
549
550 protected void postTranslate(float dx, float dy) {
551 mSuppMatrix.postTranslate(dx, dy);
552 }
553
554 protected void panBy(float dx, float dy) {
555 postTranslate(dx, dy);
556 setImageMatrix(getImageViewMatrix());
557 }
558}
559