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.videoeditor.widgets; 18 19 20 import android.content.Context; 21 import android.graphics.Bitmap; 22 import android.graphics.Matrix; 23 import android.graphics.RectF; 24 import android.graphics.drawable.Drawable; 25 import android.util.AttributeSet; 26 import android.view.MotionEvent; 27 import android.widget.ImageView; 28 29 /** 30 * An image view which can be panned and zoomed. 31 */ 32 public class ImageViewTouchBase extends ImageView { 33 private static final float SCALE_RATE = 1.25F; 34 // Zoom scale is applied after the transform that fits the image screen, 35 // so 1.0 is a perfect fit and it doesn't make sense to allow smaller 36 // values. 37 private static final float MIN_ZOOM_SCALE = 1.0f; 38 39 // This is the base transformation which is used to show the image 40 // initially. The current computation for this shows the image in 41 // it's entirety, letterboxing as needed. One could choose to 42 // show the image as cropped instead. 43 // 44 // This matrix is recomputed when we go from the thumbnail image to 45 // the full size image. 46 private Matrix mBaseMatrix = new Matrix(); 47 48 // This is the supplementary transformation which reflects what 49 // the user has done in terms of zooming and panning. 50 // 51 // This matrix remains the same when we go from the thumbnail image 52 // to the full size image. 53 private Matrix mSuppMatrix = new Matrix(); 54 55 // This is the final matrix which is computed as the concatenation 56 // of the base matrix and the supplementary matrix. 57 private final Matrix mDisplayMatrix = new Matrix(); 58 59 // Temporary buffer used for getting the values out of a matrix. 60 private final float[] mMatrixValues = new float[9]; 61 62 // The current bitmap being displayed. 63 private Bitmap mBitmapDisplayed; 64 65 // The width and height of the view 66 private int mThisWidth = -1, mThisHeight = -1; 67 68 private boolean mStretch = true; 69 // The zoom scale 70 private float mMaxZoom; 71 private Runnable mOnLayoutRunnable = null; 72 private ImageTouchEventListener mEventListener; 73 74 /** 75 * Touch interface 76 */ 77 public interface ImageTouchEventListener { 78 public boolean onImageTouchEvent(MotionEvent ev); 79 } 80 /** 81 * Constructor 82 * 83 * @param context The context 84 */ 85 public ImageViewTouchBase(Context context) { 86 super(context); 87 setScaleType(ImageView.ScaleType.MATRIX); 88 } 89 90 /** 91 * Constructor 92 * 93 * @param context The context 94 * @param attrs The attributes 95 */ 96 public ImageViewTouchBase(Context context, AttributeSet attrs) { 97 super(context, attrs); 98 setScaleType(ImageView.ScaleType.MATRIX); 99 } 100 101 /** 102 * Constructor 103 * 104 * @param context The context 105 * @param attrs The attributes 106 * @param defStyle The default style 107 */ 108 public ImageViewTouchBase(Context context, AttributeSet attrs, int defStyle) { 109 super(context, attrs, defStyle); 110 setScaleType(ImageView.ScaleType.MATRIX); 111 } 112 113 /* 114 * {@inheritDoc} 115 */ 116 @Override 117 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 118 super.onLayout(changed, left, top, right, bottom); 119 120 mThisWidth = right - left; 121 mThisHeight = bottom - top; 122 final Runnable r = mOnLayoutRunnable; 123 if (r != null) { 124 mOnLayoutRunnable = null; 125 r.run(); 126 } else { 127 if (mBitmapDisplayed != null) { 128 getProperBaseMatrix(mBitmapDisplayed, mBaseMatrix); 129 setImageMatrix(getImageViewMatrix()); 130 } 131 } 132 } 133 134 /* 135 * {@inheritDoc} 136 */ 137 @Override 138 public boolean dispatchTouchEvent(MotionEvent ev) { 139 if (mEventListener != null) { 140 return mEventListener.onImageTouchEvent(ev); 141 } else { 142 return false; 143 } 144 } 145 146 /* 147 * {@inheritDoc} 148 */ 149 @Override 150 public void setImageBitmap(Bitmap bitmap) { 151 super.setImageBitmap(bitmap); 152 153 final Drawable d = getDrawable(); 154 if (d != null) { 155 d.setDither(true); 156 } 157 158 mBitmapDisplayed = bitmap; 159 } 160 161 /** 162 * @param listener The listener 163 */ 164 public void setEventListener(ImageTouchEventListener listener) { 165 mEventListener = listener; 166 } 167 168 /** 169 * @return The image bitmap 170 */ 171 public Bitmap getImageBitmap() { 172 return mBitmapDisplayed; 173 } 174 175 /** 176 * If the view has not yet been measured delay the method 177 * 178 * @param bitmap The bitmap 179 * @param resetSupp true to reset the transform matrix 180 */ 181 public void setImageBitmapResetBase(final Bitmap bitmap, final boolean resetSupp) { 182 mStretch = true; 183 final int viewWidth = getWidth(); 184 if (viewWidth <= 0) { 185 mOnLayoutRunnable = new Runnable() { 186 @Override 187 public void run() { 188 setImageBitmapResetBase(bitmap, resetSupp); 189 } 190 }; 191 return; 192 } 193 194 if (bitmap != null) { 195 getProperBaseMatrix(bitmap, mBaseMatrix); 196 setImageBitmap(bitmap); 197 } else { 198 mBaseMatrix.reset(); 199 setImageBitmap(null); 200 } 201 202 if (resetSupp) { 203 mSuppMatrix.reset(); 204 } 205 206 setImageMatrix(getImageViewMatrix()); 207 mMaxZoom = maxZoom(); 208 } 209 210 /** 211 * Reset the transform of the current image 212 */ 213 public void reset() { 214 if (mBitmapDisplayed != null) { 215 setImageBitmapResetBase(mBitmapDisplayed, true); 216 } 217 } 218 219 /** 220 * Pan 221 * 222 * @param dx The horizontal offset 223 * @param dy The vertical offset 224 */ 225 public void postTranslateCenter(float dx, float dy) { 226 mSuppMatrix.postTranslate(dx, dy); 227 228 center(true, true); 229 } 230 231 /** 232 * Pan by the specified horizontal and vertical amount 233 * 234 * @param dx Pan by this horizontal amount 235 * @param dy Pan by this vertical amount 236 */ 237 private void panBy(float dx, float dy) { 238 mSuppMatrix.postTranslate(dx, dy); 239 240 setImageMatrix(getImageViewMatrix()); 241 } 242 243 /** 244 * @return The scale 245 */ 246 public float getScale() { 247 return getValue(mSuppMatrix, Matrix.MSCALE_X); 248 } 249 250 /** 251 * @param rect The input/output rectangle 252 */ 253 public void mapRect(RectF rect) { 254 mSuppMatrix.mapRect(rect); 255 } 256 257 /** 258 * Setup the base matrix so that the image is centered and scaled properly. 259 * 260 * @param bitmap The bitmap 261 * @param matrix The matrix 262 */ 263 private void getProperBaseMatrix(Bitmap bitmap, Matrix matrix) { 264 final float viewWidth = getWidth(); 265 final float viewHeight = getHeight(); 266 267 final float w = bitmap.getWidth(); 268 final float h = bitmap.getHeight(); 269 matrix.reset(); 270 271 if (mStretch) { 272 // We limit up-scaling to 10x otherwise the result may look bad if 273 // it's a small icon. 274 float widthScale = Math.min(viewWidth / w, 10.0f); 275 float heightScale = Math.min(viewHeight / h, 10.0f); 276 float scale = Math.min(widthScale, heightScale); 277 matrix.postScale(scale, scale); 278 matrix.postTranslate((viewWidth - w * scale) / 2F, (viewHeight - h * scale) / 2F); 279 } else { 280 matrix.postTranslate((viewWidth - w) / 2F, (viewHeight - h) / 2F); 281 } 282 } 283 284 /** 285 * Combine the base matrix and the supp matrix to make the final matrix. 286 */ 287 private Matrix getImageViewMatrix() { 288 // The final matrix is computed as the concatenation of the base matrix 289 // and the supplementary matrix. 290 mDisplayMatrix.set(mBaseMatrix); 291 mDisplayMatrix.postConcat(mSuppMatrix); 292 return mDisplayMatrix; 293 } 294 295 /** 296 * @return The maximum zoom 297 */ 298 public float getMaxZoom() { 299 return mMaxZoom; 300 } 301 302 /** 303 * Sets the maximum zoom, which is a scale relative to the base matrix. It 304 * is calculated to show the image at 400% zoom regardless of screen or 305 * image orientation. If in the future we decode the full 3 megapixel 306 * image, rather than the current 1024x768, this should be changed down 307 * to 200%. 308 */ 309 private float maxZoom() { 310 if (mBitmapDisplayed == null) { 311 return 1F; 312 } 313 314 final float fw = (float)mBitmapDisplayed.getWidth() / mThisWidth; 315 final float fh = (float)mBitmapDisplayed.getHeight() / mThisHeight; 316 317 return Math.max(fw, fh) * 4; 318 } 319 320 /** 321 * Sets the maximum zoom, which is a scale relative to the base matrix. It 322 * is calculated to show the image at 400% zoom regardless of screen or 323 * image orientation. If in the future we decode the full 3 megapixel 324 * image, rather than the current 1024x768, this should be changed down 325 * to 200%. 326 */ 327 public static float maxZoom(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight) { 328 final float fw = (float)bitmapWidth / viewWidth; 329 final float fh = (float)bitmapHeight / viewHeight; 330 331 return Math.max(fw, fh) * 4; 332 } 333 334 /** 335 * Ensure the scale factor is within limits 336 * 337 * @param scale The scale factor 338 * 339 * @return The corrected scaled factor 340 */ 341 private float correctedZoomScale(float scale) { 342 float result = scale; 343 if (result > mMaxZoom) { 344 result = mMaxZoom; 345 } else if (result < MIN_ZOOM_SCALE) { 346 result = MIN_ZOOM_SCALE; 347 } 348 349 return result; 350 } 351 352 /** 353 * Zoom to the specified scale factor 354 * 355 * @param scale The scale factor 356 * @param centerX The horizontal center 357 * @param centerY The vertical center 358 */ 359 public void zoomTo(float scale, float centerX, float centerY) { 360 float correctedScale = correctedZoomScale(scale); 361 362 float oldScale = getScale(); 363 float deltaScale = correctedScale / oldScale; 364 365 mSuppMatrix.postScale(deltaScale, deltaScale, centerX, centerY); 366 setImageMatrix(getImageViewMatrix()); 367 center(true, true); 368 } 369 370 /** 371 * Zoom to the specified scale factor 372 * 373 * @param scale The scale factor 374 */ 375 public void zoomTo(float scale) { 376 final float cx = getWidth() / 2F; 377 final float cy = getHeight() / 2F; 378 379 zoomTo(scale, cx, cy); 380 } 381 382 /** 383 * Zoom to the specified scale factor and center point 384 * 385 * @param scale The scale factor 386 * @param pointX The horizontal position 387 * @param pointY The vertical position 388 */ 389 public void zoomToPoint(float scale, float pointX, float pointY) { 390 final float cx = getWidth() / 2F; 391 final float cy = getHeight() / 2F; 392 393 panBy(cx - pointX, cy - pointY); 394 zoomTo(scale, cx, cy); 395 } 396 397 /** 398 * Zoom to the specified scale factor and point 399 * 400 * @param scale The scale factor 401 * @param pointX The horizontal position 402 * @param pointY The vertical position 403 */ 404 public void zoomToOffset(float scale, float pointX, float pointY) { 405 406 float correctedScale = correctedZoomScale(scale); 407 408 float oldScale = getScale(); 409 float deltaScale = correctedScale / oldScale; 410 411 mSuppMatrix.postScale(deltaScale, deltaScale); 412 setImageMatrix(getImageViewMatrix()); 413 414 panBy(-pointX, -pointY); 415 } 416 417 /** 418 * Zoom in by a preset scale rate 419 */ 420 public void zoomIn() { 421 zoomIn(SCALE_RATE); 422 } 423 424 /** 425 * Zoom in by the specified scale rate 426 * 427 * @param rate The scale rate 428 */ 429 public void zoomIn(float rate) { 430 if (getScale() < mMaxZoom && mBitmapDisplayed != null) { 431 float cx = getWidth() / 2F; 432 float cy = getHeight() / 2F; 433 434 mSuppMatrix.postScale(rate, rate, cx, cy); 435 setImageMatrix(getImageViewMatrix()); 436 } 437 } 438 439 /** 440 * Zoom out by a preset scale rate 441 */ 442 public void zoomOut() { 443 zoomOut(SCALE_RATE); 444 } 445 446 /** 447 * Zoom out by the specified scale rate 448 * 449 * @param rate The scale rate 450 */ 451 public void zoomOut(float rate) { 452 if (getScale() > MIN_ZOOM_SCALE && mBitmapDisplayed != null) { 453 float cx = getWidth() / 2F; 454 float cy = getHeight() / 2F; 455 456 // Zoom out to at most 1x. 457 Matrix tmp = new Matrix(mSuppMatrix); 458 tmp.postScale(1F / rate, 1F / rate, cx, cy); 459 460 if (getValue(tmp, Matrix.MSCALE_X) < 1F) { 461 mSuppMatrix.setScale(1F, 1F, cx, cy); 462 } else { 463 mSuppMatrix.postScale(1F / rate, 1F / rate, cx, cy); 464 } 465 setImageMatrix(getImageViewMatrix()); 466 center(true, true); 467 } 468 } 469 470 /** 471 * Center as much as possible in one or both axis. Centering is 472 * defined as follows: if the image is scaled down below the 473 * view's dimensions then center it (literally). If the image 474 * is scaled larger than the view and is translated out of view 475 * then translate it back into view (i.e. eliminate black bars). 476 */ 477 private void center(boolean horizontal, boolean vertical) { 478 if (mBitmapDisplayed == null) { 479 return; 480 } 481 482 final Matrix m = getImageViewMatrix(); 483 final RectF rect = new RectF(0, 0, mBitmapDisplayed.getWidth(), 484 mBitmapDisplayed.getHeight()); 485 486 m.mapRect(rect); 487 488 final float height = rect.height(); 489 final float width = rect.width(); 490 float deltaX = 0, deltaY = 0; 491 492 if (vertical) { 493 int viewHeight = getHeight(); 494 if (height < viewHeight) { 495 deltaY = (viewHeight - height) / 2 - rect.top; 496 } else if (rect.top > 0) { 497 deltaY = -rect.top; 498 } else if (rect.bottom < viewHeight) { 499 deltaY = getHeight() - rect.bottom; 500 } 501 } 502 503 if (horizontal) { 504 int viewWidth = getWidth(); 505 if (width < viewWidth) { 506 deltaX = (viewWidth - width) / 2 - rect.left; 507 } else if (rect.left > 0) { 508 deltaX = -rect.left; 509 } else if (rect.right < viewWidth) { 510 deltaX = viewWidth - rect.right; 511 } 512 } 513 514 mSuppMatrix.postTranslate(deltaX, deltaY); 515 516 setImageMatrix(getImageViewMatrix()); 517 } 518 519 /** 520 * Get a matrix transform value 521 * 522 * @param matrix The matrix 523 * @param whichValue Which value 524 * @return The value 525 */ 526 private float getValue(Matrix matrix, int whichValue) { 527 matrix.getValues(mMatrixValues); 528 return mMatrixValues[whichValue]; 529 } 530 } 531