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.cooliris.media; 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 final protected 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, int right, int bottom) { 78 super.onLayout(changed, left, top, right, bottom); 79 mThisWidth = right - left; 80 mThisHeight = bottom - top; 81 Runnable r = mOnLayoutRunnable; 82 if (r != null) { 83 mOnLayoutRunnable = null; 84 r.run(); 85 } 86 if (mBitmapDisplayed.getBitmap() != null) { 87 getProperBaseMatrix(mBitmapDisplayed, mBaseMatrix); 88 setImageMatrix(getImageViewMatrix()); 89 } 90 } 91 92 @Override 93 public boolean onKeyDown(int keyCode, KeyEvent event) { 94 if (keyCode == KeyEvent.KEYCODE_BACK && getScale() > 1.0f) { 95 // If we're zoomed in, pressing Back jumps out to show the entire 96 // image, otherwise Back returns the user to the gallery. 97 zoomTo(1.0f); 98 return true; 99 } 100 return super.onKeyDown(keyCode, event); 101 } 102 103 protected Handler mHandler = new Handler(); 104 105 protected int mLastXTouchPos; 106 protected int mLastYTouchPos; 107 108 @Override 109 public void setImageBitmap(Bitmap bitmap) { 110 setImageBitmap(bitmap, 0); 111 } 112 113 private void setImageBitmap(Bitmap bitmap, int rotation) { 114 super.setImageBitmap(bitmap); 115 Drawable d = getDrawable(); 116 if (d != null) { 117 d.setDither(true); 118 } 119 120 Bitmap old = mBitmapDisplayed.getBitmap(); 121 mBitmapDisplayed.setBitmap(bitmap); 122 mBitmapDisplayed.setRotation(rotation); 123 124 if (old != null && old != bitmap && mRecycler != null) { 125 mRecycler.recycle(old); 126 } 127 } 128 129 public void clear() { 130 setImageBitmapResetBase(null, true); 131 } 132 133 private Runnable mOnLayoutRunnable = null; 134 135 // This function changes bitmap, reset base matrix according to the size 136 // of the bitmap, and optionally reset the supplementary matrix. 137 public void setImageBitmapResetBase(final Bitmap bitmap, final boolean resetSupp) { 138 setImageRotateBitmapResetBase(new RotateBitmap(bitmap), resetSupp); 139 } 140 141 public void setImageRotateBitmapResetBase(final RotateBitmap bitmap, final boolean resetSupp) { 142 final int viewWidth = getWidth(); 143 144 if (viewWidth <= 0) { 145 mOnLayoutRunnable = new Runnable() { 146 public void run() { 147 setImageRotateBitmapResetBase(bitmap, resetSupp); 148 } 149 }; 150 return; 151 } 152 153 if (bitmap.getBitmap() != null) { 154 getProperBaseMatrix(bitmap, mBaseMatrix); 155 setImageBitmap(bitmap.getBitmap(), bitmap.getRotation()); 156 } else { 157 mBaseMatrix.reset(); 158 setImageBitmap(null); 159 } 160 161 if (resetSupp) { 162 mSuppMatrix.reset(); 163 } 164 setImageMatrix(getImageViewMatrix()); 165 mMaxZoom = maxZoom(); 166 } 167 168 // Center as much as possible in one or both axis. Centering is 169 // defined as follows: if the image is scaled down below the 170 // view's dimensions then center it (literally). If the image 171 // is scaled larger than the view and is translated out of view 172 // then translate it back into view (i.e. eliminate black bars). 173 protected void center(boolean horizontal, boolean vertical) { 174 if (mBitmapDisplayed.getBitmap() == null) { 175 return; 176 } 177 178 Matrix m = getImageViewMatrix(); 179 180 RectF rect = new RectF(0, 0, mBitmapDisplayed.getBitmap().getWidth(), mBitmapDisplayed.getBitmap().getHeight()); 181 182 m.mapRect(rect); 183 184 float height = rect.height(); 185 float width = rect.width(); 186 187 float deltaX = 0, deltaY = 0; 188 189 if (vertical) { 190 int viewHeight = getHeight(); 191 if (height < viewHeight) { 192 deltaY = (viewHeight - height) / 2 - rect.top; 193 } else if (rect.top > 0) { 194 deltaY = -rect.top; 195 } else if (rect.bottom < viewHeight) { 196 deltaY = getHeight() - rect.bottom; 197 } 198 } 199 200 if (horizontal) { 201 int viewWidth = getWidth(); 202 if (width < viewWidth) { 203 deltaX = (viewWidth - width) / 2 - rect.left; 204 } else if (rect.left > 0) { 205 deltaX = -rect.left; 206 } else if (rect.right < viewWidth) { 207 deltaX = viewWidth - rect.right; 208 } 209 } 210 211 postTranslate(deltaX, deltaY); 212 setImageMatrix(getImageViewMatrix()); 213 } 214 215 public ImageViewTouchBase(Context context) { 216 super(context); 217 init(); 218 } 219 220 public ImageViewTouchBase(Context context, AttributeSet attrs) { 221 super(context, attrs); 222 init(); 223 } 224 225 private void init() { 226 setScaleType(ImageView.ScaleType.MATRIX); 227 } 228 229 protected float getValue(Matrix matrix, int whichValue) { 230 matrix.getValues(mMatrixValues); 231 return mMatrixValues[whichValue]; 232 } 233 234 // Get the scale factor out of the matrix. 235 protected float getScale(Matrix matrix) { 236 return getValue(matrix, Matrix.MSCALE_X); 237 } 238 239 protected float getScale() { 240 return getScale(mSuppMatrix); 241 } 242 243 // Setup the base matrix so that the image is centered and scaled properly. 244 private void getProperBaseMatrix(RotateBitmap bitmap, Matrix matrix) { 245 float viewWidth = getWidth(); 246 float viewHeight = getHeight(); 247 248 float w = bitmap.getWidth(); 249 float h = bitmap.getHeight(); 250 matrix.reset(); 251 252 // We limit up-scaling to 2x otherwise the result may look bad if it's 253 // a small icon. 254 float widthScale = Math.min(viewWidth / w, 2.0f); 255 float heightScale = Math.min(viewHeight / h, 2.0f); 256 float scale = Math.min(widthScale, heightScale); 257 258 matrix.postConcat(bitmap.getRotateMatrix()); 259 matrix.postScale(scale, scale); 260 261 matrix.postTranslate((viewWidth - w * scale) / 2F, (viewHeight - h * scale) / 2F); 262 } 263 264 // Combine the base matrix and the supp matrix to make the final matrix. 265 protected Matrix getImageViewMatrix() { 266 // The final matrix is computed as the concatentation of the base matrix 267 // and the supplementary matrix. 268 mDisplayMatrix.set(mBaseMatrix); 269 mDisplayMatrix.postConcat(mSuppMatrix); 270 return mDisplayMatrix; 271 } 272 273 static final float SCALE_RATE = 1.25F; 274 275 // Sets the maximum zoom, which is a scale relative to the base matrix. It 276 // is calculated to show the image at 400% zoom regardless of screen or 277 // image orientation. If in the future we decode the full 3 megapixel image, 278 // rather than the current 1024x768, this should be changed down to 200%. 279 protected float maxZoom() { 280 if (mBitmapDisplayed.getBitmap() == null) { 281 return 1F; 282 } 283 284 float fw = (float) mBitmapDisplayed.getWidth() / (float) mThisWidth; 285 float fh = (float) mBitmapDisplayed.getHeight() / (float) mThisHeight; 286 float max = Math.max(fw, fh) * 4; 287 return max; 288 } 289 290 protected void zoomTo(float scale, float centerX, float centerY) { 291 if (scale > mMaxZoom) { 292 scale = mMaxZoom; 293 } 294 295 float oldScale = getScale(); 296 float deltaScale = scale / oldScale; 297 298 mSuppMatrix.postScale(deltaScale, deltaScale, centerX, centerY); 299 setImageMatrix(getImageViewMatrix()); 300 center(true, true); 301 } 302 303 protected void zoomTo(final float scale, final float centerX, final float centerY, final float durationMs) { 304 final float incrementPerMs = (scale - getScale()) / durationMs; 305 final float oldScale = getScale(); 306 final long startTime = System.currentTimeMillis(); 307 308 mHandler.post(new Runnable() { 309 public void run() { 310 long now = System.currentTimeMillis(); 311 float currentMs = Math.min(durationMs, now - startTime); 312 float target = oldScale + (incrementPerMs * currentMs); 313 zoomTo(target, centerX, centerY); 314 315 if (currentMs < durationMs) { 316 mHandler.post(this); 317 } 318 } 319 }); 320 } 321 322 protected void zoomTo(float scale) { 323 float cx = getWidth() / 2F; 324 float cy = getHeight() / 2F; 325 326 zoomTo(scale, cx, cy); 327 } 328 329 protected void zoomIn() { 330 zoomIn(SCALE_RATE); 331 } 332 333 protected void zoomOut() { 334 zoomOut(SCALE_RATE); 335 } 336 337 protected void zoomIn(float rate) { 338 if (getScale() >= mMaxZoom) { 339 return; // Don't let the user zoom into the molecular level. 340 } 341 if (mBitmapDisplayed.getBitmap() == null) { 342 return; 343 } 344 345 float cx = getWidth() / 2F; 346 float cy = getHeight() / 2F; 347 348 mSuppMatrix.postScale(rate, rate, cx, cy); 349 setImageMatrix(getImageViewMatrix()); 350 } 351 352 protected void zoomOut(float rate) { 353 if (mBitmapDisplayed.getBitmap() == null) { 354 return; 355 } 356 357 float cx = getWidth() / 2F; 358 float cy = getHeight() / 2F; 359 360 // Zoom out to at most 1x. 361 Matrix tmp = new Matrix(mSuppMatrix); 362 tmp.postScale(1F / rate, 1F / rate, cx, cy); 363 364 if (getScale(tmp) < 1F) { 365 mSuppMatrix.setScale(1F, 1F, cx, cy); 366 } else { 367 mSuppMatrix.postScale(1F / rate, 1F / rate, cx, cy); 368 } 369 setImageMatrix(getImageViewMatrix()); 370 center(true, true); 371 } 372 373 protected void postTranslate(float dx, float dy) { 374 mSuppMatrix.postTranslate(dx, dy); 375 } 376 377 protected void panBy(float dx, float dy) { 378 postTranslate(dx, dy); 379 setImageMatrix(getImageViewMatrix()); 380 } 381 } 382