1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.camera; 18 19 import android.content.Context; 20 import android.graphics.Bitmap; 21 import android.graphics.Matrix; 22 import android.graphics.RectF; 23 import android.graphics.drawable.Drawable; 24 import android.os.Handler; 25 import android.util.AttributeSet; 26 import android.view.KeyEvent; 27 import android.widget.ImageView; 28 29 abstract class ImageViewTouchBase extends ImageView { 30 31 @SuppressWarnings("unused") 32 private static final String TAG = "ImageViewTouchBase"; 33 34 // This is the base transformation which is used to show the image 35 // initially. The current computation for this shows the image in 36 // it's entirety, letterboxing as needed. One could choose to 37 // show the image as cropped instead. 38 // 39 // This matrix is recomputed when we go from the thumbnail image to 40 // the full size image. 41 protected Matrix mBaseMatrix = new Matrix(); 42 43 // This is the supplementary transformation which reflects what 44 // the user has done in terms of zooming and panning. 45 // 46 // This matrix remains the same when we go from the thumbnail image 47 // to the full size image. 48 protected Matrix mSuppMatrix = new Matrix(); 49 50 // This is the final matrix which is computed as the concatentation 51 // of the base matrix and the supplementary matrix. 52 private final Matrix mDisplayMatrix = new Matrix(); 53 54 // Temporary buffer used for getting the values out of a matrix. 55 private final float[] mMatrixValues = new float[9]; 56 57 // The current bitmap being displayed. 58 protected final RotateBitmap mBitmapDisplayed = new RotateBitmap(null); 59 60 int mThisWidth = -1, mThisHeight = -1; 61 62 float mMaxZoom; 63 64 // ImageViewTouchBase will pass a Bitmap to the Recycler if it has finished 65 // its use of that Bitmap. 66 public interface Recycler { 67 public void recycle(Bitmap b); 68 } 69 70 public void setRecycler(Recycler r) { 71 mRecycler = r; 72 } 73 74 private Recycler mRecycler; 75 76 @Override 77 protected void onLayout(boolean changed, int left, int top, 78 int right, int bottom) { 79 super.onLayout(changed, left, top, right, bottom); 80 mThisWidth = right - left; 81 mThisHeight = bottom - top; 82 Runnable r = mOnLayoutRunnable; 83 if (r != null) { 84 mOnLayoutRunnable = null; 85 r.run(); 86 } 87 if (mBitmapDisplayed.getBitmap() != null) { 88 getProperBaseMatrix(mBitmapDisplayed, mBaseMatrix); 89 setImageMatrix(getImageViewMatrix()); 90 } 91 } 92 93 @Override 94 public boolean onKeyDown(int keyCode, KeyEvent event) { 95 if (keyCode == KeyEvent.KEYCODE_BACK 96 && event.getRepeatCount() == 0) { 97 event.startTracking(); 98 return true; 99 } 100 return super.onKeyDown(keyCode, event); 101 } 102 103 @Override 104 public boolean onKeyUp(int keyCode, KeyEvent event) { 105 if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() 106 && !event.isCanceled()) { 107 if (getScale() > 1.0f) { 108 // If we're zoomed in, pressing Back jumps out to show the 109 // entire image, otherwise Back returns the user to the gallery. 110 zoomTo(1.0f); 111 return true; 112 } 113 } 114 return super.onKeyUp(keyCode, event); 115 } 116 117 protected Handler mHandler = new Handler(); 118 119 @Override 120 public void setImageBitmap(Bitmap bitmap) { 121 setImageBitmap(bitmap, 0); 122 } 123 124 private void setImageBitmap(Bitmap bitmap, int rotation) { 125 super.setImageBitmap(bitmap); 126 Drawable d = getDrawable(); 127 if (d != null) { 128 d.setDither(true); 129 } 130 131 Bitmap old = mBitmapDisplayed.getBitmap(); 132 mBitmapDisplayed.setBitmap(bitmap); 133 mBitmapDisplayed.setRotation(rotation); 134 135 if (old != null && old != bitmap && mRecycler != null) { 136 mRecycler.recycle(old); 137 } 138 } 139 140 public void clear() { 141 setImageBitmapResetBase(null, true); 142 } 143 144 private Runnable mOnLayoutRunnable = null; 145 146 // This function changes bitmap, reset base matrix according to the size 147 // of the bitmap, and optionally reset the supplementary matrix. 148 public void setImageBitmapResetBase(final Bitmap bitmap, 149 final boolean resetSupp) { 150 setImageRotateBitmapResetBase(new RotateBitmap(bitmap), resetSupp); 151 } 152 153 public void setImageRotateBitmapResetBase(final RotateBitmap bitmap, 154 final boolean resetSupp) { 155 final int viewWidth = getWidth(); 156 157 if (viewWidth <= 0) { 158 mOnLayoutRunnable = new Runnable() { 159 public void run() { 160 setImageRotateBitmapResetBase(bitmap, resetSupp); 161 } 162 }; 163 return; 164 } 165 166 if (bitmap.getBitmap() != null) { 167 getProperBaseMatrix(bitmap, mBaseMatrix); 168 setImageBitmap(bitmap.getBitmap(), bitmap.getRotation()); 169 } else { 170 mBaseMatrix.reset(); 171 setImageBitmap(null); 172 } 173 174 if (resetSupp) { 175 mSuppMatrix.reset(); 176 } 177 setImageMatrix(getImageViewMatrix()); 178 mMaxZoom = maxZoom(); 179 } 180 181 // Center as much as possible in one or both axis. Centering is 182 // defined as follows: if the image is scaled down below the 183 // view's dimensions then center it (literally). If the image 184 // is scaled larger than the view and is translated out of view 185 // then translate it back into view (i.e. eliminate black bars). 186 protected void center(boolean horizontal, boolean vertical) { 187 if (mBitmapDisplayed.getBitmap() == null) { 188 return; 189 } 190 191 Matrix m = getImageViewMatrix(); 192 193 RectF rect = new RectF(0, 0, 194 mBitmapDisplayed.getBitmap().getWidth(), 195 mBitmapDisplayed.getBitmap().getHeight()); 196 197 m.mapRect(rect); 198 199 float height = rect.height(); 200 float width = rect.width(); 201 202 float deltaX = 0, deltaY = 0; 203 204 if (vertical) { 205 int viewHeight = getHeight(); 206 if (height < viewHeight) { 207 deltaY = (viewHeight - height) / 2 - rect.top; 208 } else if (rect.top > 0) { 209 deltaY = -rect.top; 210 } else if (rect.bottom < viewHeight) { 211 deltaY = getHeight() - rect.bottom; 212 } 213 } 214 215 if (horizontal) { 216 int viewWidth = getWidth(); 217 if (width < viewWidth) { 218 deltaX = (viewWidth - width) / 2 - rect.left; 219 } else if (rect.left > 0) { 220 deltaX = -rect.left; 221 } else if (rect.right < viewWidth) { 222 deltaX = viewWidth - rect.right; 223 } 224 } 225 226 postTranslate(deltaX, deltaY); 227 setImageMatrix(getImageViewMatrix()); 228 } 229 230 public ImageViewTouchBase(Context context) { 231 super(context); 232 init(); 233 } 234 235 public ImageViewTouchBase(Context context, AttributeSet attrs) { 236 super(context, attrs); 237 init(); 238 } 239 240 private void init() { 241 setScaleType(ImageView.ScaleType.MATRIX); 242 } 243 244 protected float getValue(Matrix matrix, int whichValue) { 245 matrix.getValues(mMatrixValues); 246 return mMatrixValues[whichValue]; 247 } 248 249 // Get the scale factor out of the matrix. 250 protected float getScale(Matrix matrix) { 251 return getValue(matrix, Matrix.MSCALE_X); 252 } 253 254 protected float getScale() { 255 return getScale(mSuppMatrix); 256 } 257 258 // Setup the base matrix so that the image is centered and scaled properly. 259 private void getProperBaseMatrix(RotateBitmap bitmap, Matrix matrix) { 260 float viewWidth = getWidth(); 261 float viewHeight = getHeight(); 262 263 float w = bitmap.getWidth(); 264 float h = bitmap.getHeight(); 265 matrix.reset(); 266 267 // We limit up-scaling to 3x otherwise the result may look bad if it's 268 // a small icon. 269 float widthScale = Math.min(viewWidth / w, 3.0f); 270 float heightScale = Math.min(viewHeight / h, 3.0f); 271 float scale = Math.min(widthScale, heightScale); 272 273 matrix.postConcat(bitmap.getRotateMatrix()); 274 matrix.postScale(scale, scale); 275 276 matrix.postTranslate( 277 (viewWidth - w * scale) / 2F, 278 (viewHeight - h * scale) / 2F); 279 } 280 281 // Combine the base matrix and the supp matrix to make the final matrix. 282 protected Matrix getImageViewMatrix() { 283 // The final matrix is computed as the concatentation of the base matrix 284 // and the supplementary matrix. 285 mDisplayMatrix.set(mBaseMatrix); 286 mDisplayMatrix.postConcat(mSuppMatrix); 287 return mDisplayMatrix; 288 } 289 290 static final float SCALE_RATE = 1.25F; 291 292 // Sets the maximum zoom, which is a scale relative to the base matrix. It 293 // is calculated to show the image at 400% zoom regardless of screen or 294 // image orientation. If in the future we decode the full 3 megapixel image, 295 // rather than the current 1024x768, this should be changed down to 200%. 296 protected float maxZoom() { 297 if (mBitmapDisplayed.getBitmap() == null) { 298 return 1F; 299 } 300 301 float fw = (float) mBitmapDisplayed.getWidth() / (float) mThisWidth; 302 float fh = (float) mBitmapDisplayed.getHeight() / (float) mThisHeight; 303 float max = Math.max(fw, fh) * 4; 304 return max; 305 } 306 307 protected void zoomTo(float scale, float centerX, float centerY) { 308 if (scale > mMaxZoom) { 309 scale = mMaxZoom; 310 } 311 312 float oldScale = getScale(); 313 float deltaScale = scale / oldScale; 314 315 mSuppMatrix.postScale(deltaScale, deltaScale, centerX, centerY); 316 setImageMatrix(getImageViewMatrix()); 317 center(true, true); 318 } 319 320 protected void zoomTo(final float scale, final float centerX, 321 final float centerY, final float durationMs) { 322 final float incrementPerMs = (scale - getScale()) / durationMs; 323 final float oldScale = getScale(); 324 final long startTime = System.currentTimeMillis(); 325 326 mHandler.post(new Runnable() { 327 public void run() { 328 long now = System.currentTimeMillis(); 329 float currentMs = Math.min(durationMs, now - startTime); 330 float target = oldScale + (incrementPerMs * currentMs); 331 zoomTo(target, centerX, centerY); 332 333 if (currentMs < durationMs) { 334 mHandler.post(this); 335 } 336 } 337 }); 338 } 339 340 protected void zoomTo(float scale) { 341 float cx = getWidth() / 2F; 342 float cy = getHeight() / 2F; 343 344 zoomTo(scale, cx, cy); 345 } 346 347 protected void zoomToPoint(float scale, float pointX, float pointY) { 348 float cx = getWidth() / 2F; 349 float cy = getHeight() / 2F; 350 351 panBy(cx - pointX, cy - pointY); 352 zoomTo(scale, cx, cy); 353 } 354 355 protected void zoomIn() { 356 zoomIn(SCALE_RATE); 357 } 358 359 protected void zoomOut() { 360 zoomOut(SCALE_RATE); 361 } 362 363 protected void zoomIn(float rate) { 364 if (getScale() >= mMaxZoom) { 365 return; // Don't let the user zoom into the molecular level. 366 } 367 if (mBitmapDisplayed.getBitmap() == null) { 368 return; 369 } 370 371 float cx = getWidth() / 2F; 372 float cy = getHeight() / 2F; 373 374 mSuppMatrix.postScale(rate, rate, cx, cy); 375 setImageMatrix(getImageViewMatrix()); 376 } 377 378 protected void zoomOut(float rate) { 379 if (mBitmapDisplayed.getBitmap() == null) { 380 return; 381 } 382 383 float cx = getWidth() / 2F; 384 float cy = getHeight() / 2F; 385 386 // Zoom out to at most 1x. 387 Matrix tmp = new Matrix(mSuppMatrix); 388 tmp.postScale(1F / rate, 1F / rate, cx, cy); 389 390 if (getScale(tmp) < 1F) { 391 mSuppMatrix.setScale(1F, 1F, cx, cy); 392 } else { 393 mSuppMatrix.postScale(1F / rate, 1F / rate, cx, cy); 394 } 395 setImageMatrix(getImageViewMatrix()); 396 center(true, true); 397 } 398 399 protected void postTranslate(float dx, float dy) { 400 mSuppMatrix.postTranslate(dx, dy); 401 } 402 403 protected void panBy(float dx, float dy) { 404 postTranslate(dx, dy); 405 setImageMatrix(getImageViewMatrix()); 406 } 407 } 408