1 package com.davemorrissey.labs.subscaleview; 2 3 import android.content.ContentResolver; 4 import android.content.Context; 5 import android.content.res.TypedArray; 6 import android.database.Cursor; 7 import android.graphics.Bitmap; 8 import android.graphics.Canvas; 9 import android.graphics.Color; 10 import android.graphics.Matrix; 11 import android.graphics.Paint; 12 import android.graphics.Paint.Style; 13 import android.graphics.Point; 14 import android.graphics.PointF; 15 import android.graphics.Rect; 16 import android.graphics.RectF; 17 import android.support.media.ExifInterface; 18 import android.net.Uri; 19 import android.os.AsyncTask; 20 import android.os.Handler; 21 import android.os.Message; 22 import android.provider.MediaStore; 23 import android.support.annotation.AnyThread; 24 import android.support.annotation.NonNull; 25 import android.util.AttributeSet; 26 import android.util.DisplayMetrics; 27 import android.util.Log; 28 import android.util.TypedValue; 29 import android.view.GestureDetector; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.view.ViewParent; 33 34 import com.davemorrissey.labs.subscaleview.R.styleable; 35 import com.davemorrissey.labs.subscaleview.decoder.CompatDecoderFactory; 36 import com.davemorrissey.labs.subscaleview.decoder.DecoderFactory; 37 import com.davemorrissey.labs.subscaleview.decoder.ImageDecoder; 38 import com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder; 39 import com.davemorrissey.labs.subscaleview.decoder.SkiaImageDecoder; 40 import com.davemorrissey.labs.subscaleview.decoder.SkiaImageRegionDecoder; 41 42 import java.lang.ref.WeakReference; 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.LinkedHashMap; 46 import java.util.List; 47 import java.util.Locale; 48 import java.util.Map; 49 import java.util.concurrent.Executor; 50 import java.util.concurrent.locks.ReadWriteLock; 51 import java.util.concurrent.locks.ReentrantReadWriteLock; 52 53 /** 54 * <p> 55 * Displays an image subsampled as necessary to avoid loading too much image data into memory. After zooming in, 56 * a set of image tiles subsampled at higher resolution are loaded and displayed over the base layer. During pan and 57 * zoom, tiles off screen or higher/lower resolution than required are discarded from memory. 58 * </p><p> 59 * Tiles are no larger than the max supported bitmap size, so with large images tiling may be used even when zoomed out. 60 * </p><p> 61 * v prefixes - coordinates, translations and distances measured in screen (view) pixels 62 * <br> 63 * s prefixes - coordinates, translations and distances measured in rotated and cropped source image pixels (scaled) 64 * <br> 65 * f prefixes - coordinates, translations and distances measured in original unrotated, uncropped source file pixels 66 * </p><p> 67 * <a href="https://github.com/davemorrissey/subsampling-scale-image-view">View project on GitHub</a> 68 * </p> 69 */ 70 @SuppressWarnings("unused") 71 public class SubsamplingScaleImageView extends View { 72 73 private static final String TAG = SubsamplingScaleImageView.class.getSimpleName(); 74 75 /** Attempt to use EXIF information on the image to rotate it. Works for external files only. */ 76 public static final int ORIENTATION_USE_EXIF = -1; 77 /** Display the image file in its native orientation. */ 78 public static final int ORIENTATION_0 = 0; 79 /** Rotate the image 90 degrees clockwise. */ 80 public static final int ORIENTATION_90 = 90; 81 /** Rotate the image 180 degrees. */ 82 public static final int ORIENTATION_180 = 180; 83 /** Rotate the image 270 degrees clockwise. */ 84 public static final int ORIENTATION_270 = 270; 85 86 private static final List<Integer> VALID_ORIENTATIONS = Arrays.asList(ORIENTATION_0, ORIENTATION_90, ORIENTATION_180, ORIENTATION_270, ORIENTATION_USE_EXIF); 87 88 /** During zoom animation, keep the point of the image that was tapped in the same place, and scale the image around it. */ 89 public static final int ZOOM_FOCUS_FIXED = 1; 90 /** During zoom animation, move the point of the image that was tapped to the center of the screen. */ 91 public static final int ZOOM_FOCUS_CENTER = 2; 92 /** Zoom in to and center the tapped point immediately without animating. */ 93 public static final int ZOOM_FOCUS_CENTER_IMMEDIATE = 3; 94 95 private static final List<Integer> VALID_ZOOM_STYLES = Arrays.asList(ZOOM_FOCUS_FIXED, ZOOM_FOCUS_CENTER, ZOOM_FOCUS_CENTER_IMMEDIATE); 96 97 /** Quadratic ease out. Not recommended for scale animation, but good for panning. */ 98 public static final int EASE_OUT_QUAD = 1; 99 /** Quadratic ease in and out. */ 100 public static final int EASE_IN_OUT_QUAD = 2; 101 102 private static final List<Integer> VALID_EASING_STYLES = Arrays.asList(EASE_IN_OUT_QUAD, EASE_OUT_QUAD); 103 104 /** Don't allow the image to be panned off screen. As much of the image as possible is always displayed, centered in the view when it is smaller. This is the best option for galleries. */ 105 public static final int PAN_LIMIT_INSIDE = 1; 106 /** Allows the image to be panned until it is just off screen, but no further. The edge of the image will stop when it is flush with the screen edge. */ 107 public static final int PAN_LIMIT_OUTSIDE = 2; 108 /** Allows the image to be panned until a corner reaches the center of the screen but no further. Useful when you want to pan any spot on the image to the exact center of the screen. */ 109 public static final int PAN_LIMIT_CENTER = 3; 110 111 private static final List<Integer> VALID_PAN_LIMITS = Arrays.asList(PAN_LIMIT_INSIDE, PAN_LIMIT_OUTSIDE, PAN_LIMIT_CENTER); 112 113 /** Scale the image so that both dimensions of the image will be equal to or less than the corresponding dimension of the view. The image is then centered in the view. This is the default behaviour and best for galleries. */ 114 public static final int SCALE_TYPE_CENTER_INSIDE = 1; 115 /** Scale the image uniformly so that both dimensions of the image will be equal to or larger than the corresponding dimension of the view. The image is then centered in the view. */ 116 public static final int SCALE_TYPE_CENTER_CROP = 2; 117 /** Scale the image so that both dimensions of the image will be equal to or less than the maxScale and equal to or larger than minScale. The image is then centered in the view. */ 118 public static final int SCALE_TYPE_CUSTOM = 3; 119 /** Scale the image so that both dimensions of the image will be equal to or larger than the corresponding dimension of the view. The top left is shown. */ 120 public static final int SCALE_TYPE_START = 4; 121 122 private static final List<Integer> VALID_SCALE_TYPES = Arrays.asList(SCALE_TYPE_CENTER_CROP, SCALE_TYPE_CENTER_INSIDE, SCALE_TYPE_CUSTOM, SCALE_TYPE_START); 123 124 /** State change originated from animation. */ 125 public static final int ORIGIN_ANIM = 1; 126 /** State change originated from touch gesture. */ 127 public static final int ORIGIN_TOUCH = 2; 128 /** State change originated from a fling momentum anim. */ 129 public static final int ORIGIN_FLING = 3; 130 /** State change originated from a double tap zoom anim. */ 131 public static final int ORIGIN_DOUBLE_TAP_ZOOM = 4; 132 133 // Bitmap (preview or full image) 134 private Bitmap bitmap; 135 136 // Whether the bitmap is a preview image 137 private boolean bitmapIsPreview; 138 139 // Specifies if a cache handler is also referencing the bitmap. Do not recycle if so. 140 private boolean bitmapIsCached; 141 142 // Uri of full size image 143 private Uri uri; 144 145 // Sample size used to display the whole image when fully zoomed out 146 private int fullImageSampleSize; 147 148 // Map of zoom level to tile grid 149 private Map<Integer, List<Tile>> tileMap; 150 151 // Overlay tile boundaries and other info 152 private boolean debug; 153 154 // Image orientation setting 155 private int orientation = ORIENTATION_0; 156 157 // Max scale allowed (prevent infinite zoom) 158 private float maxScale = 2F; 159 160 // Min scale allowed (prevent infinite zoom) 161 private float minScale = minScale(); 162 163 // Density to reach before loading higher resolution tiles 164 private int minimumTileDpi = -1; 165 166 // Pan limiting style 167 private int panLimit = PAN_LIMIT_INSIDE; 168 169 // Minimum scale type 170 private int minimumScaleType = SCALE_TYPE_CENTER_INSIDE; 171 172 // overrides for the dimensions of the generated tiles 173 public static final int TILE_SIZE_AUTO = Integer.MAX_VALUE; 174 private int maxTileWidth = TILE_SIZE_AUTO; 175 private int maxTileHeight = TILE_SIZE_AUTO; 176 177 // An executor service for loading of images 178 private Executor executor = AsyncTask.THREAD_POOL_EXECUTOR; 179 180 // Whether tiles should be loaded while gestures and animations are still in progress 181 private boolean eagerLoadingEnabled = true; 182 183 // Gesture detection settings 184 private boolean panEnabled = true; 185 private boolean zoomEnabled = true; 186 private boolean quickScaleEnabled = true; 187 188 // Double tap zoom behaviour 189 private float doubleTapZoomScale = 1F; 190 private int doubleTapZoomStyle = ZOOM_FOCUS_FIXED; 191 private int doubleTapZoomDuration = 500; 192 193 // Current scale and scale at start of zoom 194 private float scale; 195 private float scaleStart; 196 197 // Screen coordinate of top-left corner of source image 198 private PointF vTranslate; 199 private PointF vTranslateStart; 200 private PointF vTranslateBefore; 201 202 // Source coordinate to center on, used when new position is set externally before view is ready 203 private Float pendingScale; 204 private PointF sPendingCenter; 205 private PointF sRequestedCenter; 206 207 // Source image dimensions and orientation - dimensions relate to the unrotated image 208 private int sWidth; 209 private int sHeight; 210 private int sOrientation; 211 private Rect sRegion; 212 private Rect pRegion; 213 214 // Is two-finger zooming in progress 215 private boolean isZooming; 216 // Is one-finger panning in progress 217 private boolean isPanning; 218 // Is quick-scale gesture in progress 219 private boolean isQuickScaling; 220 // Max touches used in current gesture 221 private int maxTouchCount; 222 223 // Fling detector 224 private GestureDetector detector; 225 private GestureDetector singleDetector; 226 227 // Tile and image decoding 228 private ImageRegionDecoder decoder; 229 private final ReadWriteLock decoderLock = new ReentrantReadWriteLock(true); 230 private DecoderFactory<? extends ImageDecoder> bitmapDecoderFactory = new CompatDecoderFactory<ImageDecoder>(SkiaImageDecoder.class); 231 private DecoderFactory<? extends ImageRegionDecoder> regionDecoderFactory = new CompatDecoderFactory<ImageRegionDecoder>(SkiaImageRegionDecoder.class); 232 233 // Debug values 234 private PointF vCenterStart; 235 private float vDistStart; 236 237 // Current quickscale state 238 private final float quickScaleThreshold; 239 private float quickScaleLastDistance; 240 private boolean quickScaleMoved; 241 private PointF quickScaleVLastPoint; 242 private PointF quickScaleSCenter; 243 private PointF quickScaleVStart; 244 245 // Scale and center animation tracking 246 private Anim anim; 247 248 // Whether a ready notification has been sent to subclasses 249 private boolean readySent; 250 // Whether a base layer loaded notification has been sent to subclasses 251 private boolean imageLoadedSent; 252 253 // Event listener 254 private OnImageEventListener onImageEventListener; 255 256 // Scale and center listener 257 private OnStateChangedListener onStateChangedListener; 258 259 // Long click listener 260 private OnLongClickListener onLongClickListener; 261 262 // Long click handler 263 private final Handler handler; 264 private static final int MESSAGE_LONG_CLICK = 1; 265 266 // Paint objects created once and reused for efficiency 267 private Paint bitmapPaint; 268 private Paint debugTextPaint; 269 private Paint debugLinePaint; 270 private Paint tileBgPaint; 271 272 // Volatile fields used to reduce object creation 273 private ScaleAndTranslate satTemp; 274 private Matrix matrix; 275 private RectF sRect; 276 private final float[] srcArray = new float[8]; 277 private final float[] dstArray = new float[8]; 278 279 //The logical density of the display 280 private final float density; 281 282 // A global preference for bitmap format, available to decoder classes that respect it 283 private static Bitmap.Config preferredBitmapConfig; 284 SubsamplingScaleImageView(Context context, AttributeSet attr)285 public SubsamplingScaleImageView(Context context, AttributeSet attr) { 286 super(context, attr); 287 density = getResources().getDisplayMetrics().density; 288 setMinimumDpi(160); 289 setDoubleTapZoomDpi(160); 290 setMinimumTileDpi(320); 291 setGestureDetector(context); 292 this.handler = new Handler(new Handler.Callback() { 293 public boolean handleMessage(Message message) { 294 if (message.what == MESSAGE_LONG_CLICK && onLongClickListener != null) { 295 maxTouchCount = 0; 296 SubsamplingScaleImageView.super.setOnLongClickListener(onLongClickListener); 297 performLongClick(); 298 SubsamplingScaleImageView.super.setOnLongClickListener(null); 299 } 300 return true; 301 } 302 }); 303 // Handle XML attributes 304 if (attr != null) { 305 TypedArray typedAttr = getContext().obtainStyledAttributes(attr, styleable.SubsamplingScaleImageView); 306 if (typedAttr.hasValue(styleable.SubsamplingScaleImageView_assetName)) { 307 String assetName = typedAttr.getString(styleable.SubsamplingScaleImageView_assetName); 308 if (assetName != null && assetName.length() > 0) { 309 setImage(ImageSource.asset(assetName).tilingEnabled()); 310 } 311 } 312 if (typedAttr.hasValue(styleable.SubsamplingScaleImageView_src)) { 313 int resId = typedAttr.getResourceId(styleable.SubsamplingScaleImageView_src, 0); 314 if (resId > 0) { 315 setImage(ImageSource.resource(resId).tilingEnabled()); 316 } 317 } 318 if (typedAttr.hasValue(styleable.SubsamplingScaleImageView_panEnabled)) { 319 setPanEnabled(typedAttr.getBoolean(styleable.SubsamplingScaleImageView_panEnabled, true)); 320 } 321 if (typedAttr.hasValue(styleable.SubsamplingScaleImageView_zoomEnabled)) { 322 setZoomEnabled(typedAttr.getBoolean(styleable.SubsamplingScaleImageView_zoomEnabled, true)); 323 } 324 if (typedAttr.hasValue(styleable.SubsamplingScaleImageView_quickScaleEnabled)) { 325 setQuickScaleEnabled(typedAttr.getBoolean(styleable.SubsamplingScaleImageView_quickScaleEnabled, true)); 326 } 327 if (typedAttr.hasValue(styleable.SubsamplingScaleImageView_tileBackgroundColor)) { 328 setTileBackgroundColor(typedAttr.getColor(styleable.SubsamplingScaleImageView_tileBackgroundColor, Color.argb(0, 0, 0, 0))); 329 } 330 typedAttr.recycle(); 331 } 332 333 quickScaleThreshold = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, context.getResources().getDisplayMetrics()); 334 } 335 SubsamplingScaleImageView(Context context)336 public SubsamplingScaleImageView(Context context) { 337 this(context, null); 338 } 339 340 /** 341 * Get the current preferred configuration for decoding bitmaps. {@link ImageDecoder} and {@link ImageRegionDecoder} 342 * instances can read this and use it when decoding images. 343 * @return the preferred bitmap configuration, or null if none has been set. 344 */ getPreferredBitmapConfig()345 public static Bitmap.Config getPreferredBitmapConfig() { 346 return preferredBitmapConfig; 347 } 348 349 /** 350 * Set a global preferred bitmap config shared by all view instances and applied to new instances 351 * initialised after the call is made. This is a hint only; the bundled {@link ImageDecoder} and 352 * {@link ImageRegionDecoder} classes all respect this (except when they were constructed with 353 * an instance-specific config) but custom decoder classes will not. 354 * @param preferredBitmapConfig the bitmap configuration to be used by future instances of the view. Pass null to restore the default. 355 */ setPreferredBitmapConfig(Bitmap.Config preferredBitmapConfig)356 public static void setPreferredBitmapConfig(Bitmap.Config preferredBitmapConfig) { 357 SubsamplingScaleImageView.preferredBitmapConfig = preferredBitmapConfig; 358 } 359 360 /** 361 * Sets the image orientation. It's best to call this before setting the image file or asset, because it may waste 362 * loading of tiles. However, this can be freely called at any time. 363 * @param orientation orientation to be set. See ORIENTATION_ static fields for valid values. 364 */ setOrientation(int orientation)365 public final void setOrientation(int orientation) { 366 if (!VALID_ORIENTATIONS.contains(orientation)) { 367 throw new IllegalArgumentException("Invalid orientation: " + orientation); 368 } 369 this.orientation = orientation; 370 reset(false); 371 invalidate(); 372 requestLayout(); 373 } 374 375 /** 376 * Set the image source from a bitmap, resource, asset, file or other URI. 377 * @param imageSource Image source. 378 */ setImage(ImageSource imageSource)379 public final void setImage(ImageSource imageSource) { 380 setImage(imageSource, null, null); 381 } 382 383 /** 384 * Set the image source from a bitmap, resource, asset, file or other URI, starting with a given orientation 385 * setting, scale and center. This is the best method to use when you want scale and center to be restored 386 * after screen orientation change; it avoids any redundant loading of tiles in the wrong orientation. 387 * @param imageSource Image source. 388 * @param state State to be restored. Nullable. 389 */ setImage(ImageSource imageSource, ImageViewState state)390 public final void setImage(ImageSource imageSource, ImageViewState state) { 391 setImage(imageSource, null, state); 392 } 393 394 /** 395 * Set the image source from a bitmap, resource, asset, file or other URI, providing a preview image to be 396 * displayed until the full size image is loaded. 397 * 398 * You must declare the dimensions of the full size image by calling {@link ImageSource#dimensions(int, int)} 399 * on the imageSource object. The preview source will be ignored if you don't provide dimensions, 400 * and if you provide a bitmap for the full size image. 401 * @param imageSource Image source. Dimensions must be declared. 402 * @param previewSource Optional source for a preview image to be displayed and allow interaction while the full size image loads. 403 */ setImage(ImageSource imageSource, ImageSource previewSource)404 public final void setImage(ImageSource imageSource, ImageSource previewSource) { 405 setImage(imageSource, previewSource, null); 406 } 407 408 /** 409 * Set the image source from a bitmap, resource, asset, file or other URI, providing a preview image to be 410 * displayed until the full size image is loaded, starting with a given orientation setting, scale and center. 411 * This is the best method to use when you want scale and center to be restored after screen orientation change; 412 * it avoids any redundant loading of tiles in the wrong orientation. 413 * 414 * You must declare the dimensions of the full size image by calling {@link ImageSource#dimensions(int, int)} 415 * on the imageSource object. The preview source will be ignored if you don't provide dimensions, 416 * and if you provide a bitmap for the full size image. 417 * @param imageSource Image source. Dimensions must be declared. 418 * @param previewSource Optional source for a preview image to be displayed and allow interaction while the full size image loads. 419 * @param state State to be restored. Nullable. 420 */ setImage(ImageSource imageSource, ImageSource previewSource, ImageViewState state)421 public final void setImage(ImageSource imageSource, ImageSource previewSource, ImageViewState state) { 422 if (imageSource == null) { 423 throw new NullPointerException("imageSource must not be null"); 424 } 425 426 reset(true); 427 if (state != null) { restoreState(state); } 428 429 if (previewSource != null) { 430 if (imageSource.getBitmap() != null) { 431 throw new IllegalArgumentException("Preview image cannot be used when a bitmap is provided for the main image"); 432 } 433 if (imageSource.getSWidth() <= 0 || imageSource.getSHeight() <= 0) { 434 throw new IllegalArgumentException("Preview image cannot be used unless dimensions are provided for the main image"); 435 } 436 this.sWidth = imageSource.getSWidth(); 437 this.sHeight = imageSource.getSHeight(); 438 this.pRegion = previewSource.getSRegion(); 439 if (previewSource.getBitmap() != null) { 440 this.bitmapIsCached = previewSource.isCached(); 441 onPreviewLoaded(previewSource.getBitmap()); 442 } else { 443 Uri uri = previewSource.getUri(); 444 if (uri == null && previewSource.getResource() != null) { 445 uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + getContext().getPackageName() + "/" + previewSource.getResource()); 446 } 447 BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, true); 448 execute(task); 449 } 450 } 451 452 if (imageSource.getBitmap() != null && imageSource.getSRegion() != null) { 453 onImageLoaded(Bitmap.createBitmap(imageSource.getBitmap(), imageSource.getSRegion().left, imageSource.getSRegion().top, imageSource.getSRegion().width(), imageSource.getSRegion().height()), ORIENTATION_0, false); 454 } else if (imageSource.getBitmap() != null) { 455 onImageLoaded(imageSource.getBitmap(), ORIENTATION_0, imageSource.isCached()); 456 } else { 457 sRegion = imageSource.getSRegion(); 458 uri = imageSource.getUri(); 459 if (uri == null && imageSource.getResource() != null) { 460 uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + getContext().getPackageName() + "/" + imageSource.getResource()); 461 } 462 if (imageSource.getTile() || sRegion != null) { 463 // Load the bitmap using tile decoding. 464 TilesInitTask task = new TilesInitTask(this, getContext(), regionDecoderFactory, uri); 465 execute(task); 466 } else { 467 // Load the bitmap as a single image. 468 BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false); 469 execute(task); 470 } 471 } 472 } 473 474 /** 475 * Reset all state before setting/changing image or setting new rotation. 476 */ reset(boolean newImage)477 private void reset(boolean newImage) { 478 debug("reset newImage=" + newImage); 479 scale = 0f; 480 scaleStart = 0f; 481 vTranslate = null; 482 vTranslateStart = null; 483 vTranslateBefore = null; 484 pendingScale = 0f; 485 sPendingCenter = null; 486 sRequestedCenter = null; 487 isZooming = false; 488 isPanning = false; 489 isQuickScaling = false; 490 maxTouchCount = 0; 491 fullImageSampleSize = 0; 492 vCenterStart = null; 493 vDistStart = 0; 494 quickScaleLastDistance = 0f; 495 quickScaleMoved = false; 496 quickScaleSCenter = null; 497 quickScaleVLastPoint = null; 498 quickScaleVStart = null; 499 anim = null; 500 satTemp = null; 501 matrix = null; 502 sRect = null; 503 if (newImage) { 504 uri = null; 505 decoderLock.writeLock().lock(); 506 try { 507 if (decoder != null) { 508 decoder.recycle(); 509 decoder = null; 510 } 511 } finally { 512 decoderLock.writeLock().unlock(); 513 } 514 if (bitmap != null && !bitmapIsCached) { 515 bitmap.recycle(); 516 } 517 if (bitmap != null && bitmapIsCached && onImageEventListener != null) { 518 onImageEventListener.onPreviewReleased(); 519 } 520 sWidth = 0; 521 sHeight = 0; 522 sOrientation = 0; 523 sRegion = null; 524 pRegion = null; 525 readySent = false; 526 imageLoadedSent = false; 527 bitmap = null; 528 bitmapIsPreview = false; 529 bitmapIsCached = false; 530 } 531 if (tileMap != null) { 532 for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) { 533 for (Tile tile : tileMapEntry.getValue()) { 534 tile.visible = false; 535 if (tile.bitmap != null) { 536 tile.bitmap.recycle(); 537 tile.bitmap = null; 538 } 539 } 540 } 541 tileMap = null; 542 } 543 setGestureDetector(getContext()); 544 } 545 setGestureDetector(final Context context)546 private void setGestureDetector(final Context context) { 547 this.detector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { 548 549 @Override 550 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 551 if (panEnabled && readySent && vTranslate != null && e1 != null && e2 != null && (Math.abs(e1.getX() - e2.getX()) > 50 || Math.abs(e1.getY() - e2.getY()) > 50) && (Math.abs(velocityX) > 500 || Math.abs(velocityY) > 500) && !isZooming) { 552 PointF vTranslateEnd = new PointF(vTranslate.x + (velocityX * 0.25f), vTranslate.y + (velocityY * 0.25f)); 553 float sCenterXEnd = ((getWidth()/2) - vTranslateEnd.x)/scale; 554 float sCenterYEnd = ((getHeight()/2) - vTranslateEnd.y)/scale; 555 new AnimationBuilder(new PointF(sCenterXEnd, sCenterYEnd)).withEasing(EASE_OUT_QUAD).withPanLimited(false).withOrigin(ORIGIN_FLING).start(); 556 return true; 557 } 558 return super.onFling(e1, e2, velocityX, velocityY); 559 } 560 561 @Override 562 public boolean onSingleTapConfirmed(MotionEvent e) { 563 performClick(); 564 return true; 565 } 566 567 @Override 568 public boolean onDoubleTap(MotionEvent e) { 569 if (zoomEnabled && readySent && vTranslate != null) { 570 // Hacky solution for #15 - after a double tap the GestureDetector gets in a state 571 // where the next fling is ignored, so here we replace it with a new one. 572 setGestureDetector(context); 573 if (quickScaleEnabled) { 574 // Store quick scale params. This will become either a double tap zoom or a 575 // quick scale depending on whether the user swipes. 576 vCenterStart = new PointF(e.getX(), e.getY()); 577 vTranslateStart = new PointF(vTranslate.x, vTranslate.y); 578 scaleStart = scale; 579 isQuickScaling = true; 580 isZooming = true; 581 quickScaleLastDistance = -1F; 582 quickScaleSCenter = viewToSourceCoord(vCenterStart); 583 quickScaleVStart = new PointF(e.getX(), e.getY()); 584 quickScaleVLastPoint = new PointF(quickScaleSCenter.x, quickScaleSCenter.y); 585 quickScaleMoved = false; 586 // We need to get events in onTouchEvent after this. 587 return false; 588 } else { 589 // Start double tap zoom animation. 590 doubleTapZoom(viewToSourceCoord(new PointF(e.getX(), e.getY())), new PointF(e.getX(), e.getY())); 591 return true; 592 } 593 } 594 return super.onDoubleTapEvent(e); 595 } 596 }); 597 598 singleDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { 599 @Override 600 public boolean onSingleTapConfirmed(MotionEvent e) { 601 performClick(); 602 return true; 603 } 604 }); 605 } 606 607 /** 608 * On resize, preserve center and scale. Various behaviours are possible, override this method to use another. 609 */ 610 @Override onSizeChanged(int w, int h, int oldw, int oldh)611 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 612 debug("onSizeChanged %dx%d -> %dx%d", oldw, oldh, w, h); 613 PointF sCenter = getCenter(); 614 if (readySent && sCenter != null) { 615 this.anim = null; 616 this.pendingScale = scale; 617 this.sPendingCenter = sCenter; 618 } 619 } 620 621 /** 622 * Measures the width and height of the view, preserving the aspect ratio of the image displayed if wrap_content is 623 * used. The image will scale within this box, not resizing the view as it is zoomed. 624 */ 625 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)626 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 627 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 628 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 629 int parentWidth = MeasureSpec.getSize(widthMeasureSpec); 630 int parentHeight = MeasureSpec.getSize(heightMeasureSpec); 631 boolean resizeWidth = widthSpecMode != MeasureSpec.EXACTLY; 632 boolean resizeHeight = heightSpecMode != MeasureSpec.EXACTLY; 633 int width = parentWidth; 634 int height = parentHeight; 635 if (sWidth > 0 && sHeight > 0) { 636 if (resizeWidth && resizeHeight) { 637 width = sWidth(); 638 height = sHeight(); 639 } else if (resizeHeight) { 640 height = (int)((((double)sHeight()/(double)sWidth()) * width)); 641 } else if (resizeWidth) { 642 width = (int)((((double)sWidth()/(double)sHeight()) * height)); 643 } 644 } 645 width = Math.max(width, getSuggestedMinimumWidth()); 646 height = Math.max(height, getSuggestedMinimumHeight()); 647 setMeasuredDimension(width, height); 648 } 649 650 /** 651 * Handle touch events. One finger pans, and two finger pinch and zoom plus panning. 652 */ 653 @Override onTouchEvent(@onNull MotionEvent event)654 public boolean onTouchEvent(@NonNull MotionEvent event) { 655 // During non-interruptible anims, ignore all touch events 656 if (anim != null && !anim.interruptible) { 657 requestDisallowInterceptTouchEvent(true); 658 return true; 659 } else { 660 if (anim != null && anim.listener != null) { 661 try { 662 anim.listener.onInterruptedByUser(); 663 } catch (Exception e) { 664 Log.w(TAG, "Error thrown by animation listener", e); 665 } 666 } 667 anim = null; 668 } 669 670 // Abort if not ready 671 if (vTranslate == null) { 672 if (singleDetector != null) { 673 singleDetector.onTouchEvent(event); 674 } 675 return true; 676 } 677 // Detect flings, taps and double taps 678 if (!isQuickScaling && (detector == null || detector.onTouchEvent(event))) { 679 isZooming = false; 680 isPanning = false; 681 maxTouchCount = 0; 682 return true; 683 } 684 685 if (vTranslateStart == null) { vTranslateStart = new PointF(0, 0); } 686 if (vTranslateBefore == null) { vTranslateBefore = new PointF(0, 0); } 687 if (vCenterStart == null) { vCenterStart = new PointF(0, 0); } 688 689 // Store current values so we can send an event if they change 690 float scaleBefore = scale; 691 vTranslateBefore.set(vTranslate); 692 693 boolean handled = onTouchEventInternal(event); 694 sendStateChanged(scaleBefore, vTranslateBefore, ORIGIN_TOUCH); 695 return handled || super.onTouchEvent(event); 696 } 697 698 @SuppressWarnings("deprecation") onTouchEventInternal(@onNull MotionEvent event)699 private boolean onTouchEventInternal(@NonNull MotionEvent event) { 700 int touchCount = event.getPointerCount(); 701 switch (event.getAction()) { 702 case MotionEvent.ACTION_DOWN: 703 case MotionEvent.ACTION_POINTER_1_DOWN: 704 case MotionEvent.ACTION_POINTER_2_DOWN: 705 anim = null; 706 requestDisallowInterceptTouchEvent(true); 707 maxTouchCount = Math.max(maxTouchCount, touchCount); 708 if (touchCount >= 2) { 709 if (zoomEnabled) { 710 // Start pinch to zoom. Calculate distance between touch points and center point of the pinch. 711 float distance = distance(event.getX(0), event.getX(1), event.getY(0), event.getY(1)); 712 scaleStart = scale; 713 vDistStart = distance; 714 vTranslateStart.set(vTranslate.x, vTranslate.y); 715 vCenterStart.set((event.getX(0) + event.getX(1))/2, (event.getY(0) + event.getY(1))/2); 716 } else { 717 // Abort all gestures on second touch 718 maxTouchCount = 0; 719 } 720 // Cancel long click timer 721 handler.removeMessages(MESSAGE_LONG_CLICK); 722 } else if (!isQuickScaling) { 723 // Start one-finger pan 724 vTranslateStart.set(vTranslate.x, vTranslate.y); 725 vCenterStart.set(event.getX(), event.getY()); 726 727 // Start long click timer 728 handler.sendEmptyMessageDelayed(MESSAGE_LONG_CLICK, 600); 729 } 730 return true; 731 case MotionEvent.ACTION_MOVE: 732 boolean consumed = false; 733 if (maxTouchCount > 0) { 734 if (touchCount >= 2) { 735 // Calculate new distance between touch points, to scale and pan relative to start values. 736 float vDistEnd = distance(event.getX(0), event.getX(1), event.getY(0), event.getY(1)); 737 float vCenterEndX = (event.getX(0) + event.getX(1))/2; 738 float vCenterEndY = (event.getY(0) + event.getY(1))/2; 739 740 if (zoomEnabled && (distance(vCenterStart.x, vCenterEndX, vCenterStart.y, vCenterEndY) > 5 || Math.abs(vDistEnd - vDistStart) > 5 || isPanning)) { 741 isZooming = true; 742 isPanning = true; 743 consumed = true; 744 745 double previousScale = scale; 746 scale = Math.min(maxScale, (vDistEnd / vDistStart) * scaleStart); 747 748 if (scale <= minScale()) { 749 // Minimum scale reached so don't pan. Adjust start settings so any expand will zoom in. 750 vDistStart = vDistEnd; 751 scaleStart = minScale(); 752 vCenterStart.set(vCenterEndX, vCenterEndY); 753 vTranslateStart.set(vTranslate); 754 } else if (panEnabled) { 755 // Translate to place the source image coordinate that was at the center of the pinch at the start 756 // at the center of the pinch now, to give simultaneous pan + zoom. 757 float vLeftStart = vCenterStart.x - vTranslateStart.x; 758 float vTopStart = vCenterStart.y - vTranslateStart.y; 759 float vLeftNow = vLeftStart * (scale/scaleStart); 760 float vTopNow = vTopStart * (scale/scaleStart); 761 vTranslate.x = vCenterEndX - vLeftNow; 762 vTranslate.y = vCenterEndY - vTopNow; 763 if ((previousScale * sHeight() < getHeight() && scale * sHeight() >= getHeight()) || (previousScale * sWidth() < getWidth() && scale * sWidth() >= getWidth())) { 764 fitToBounds(true); 765 vCenterStart.set(vCenterEndX, vCenterEndY); 766 vTranslateStart.set(vTranslate); 767 scaleStart = scale; 768 vDistStart = vDistEnd; 769 } 770 } else if (sRequestedCenter != null) { 771 // With a center specified from code, zoom around that point. 772 vTranslate.x = (getWidth()/2) - (scale * sRequestedCenter.x); 773 vTranslate.y = (getHeight()/2) - (scale * sRequestedCenter.y); 774 } else { 775 // With no requested center, scale around the image center. 776 vTranslate.x = (getWidth()/2) - (scale * (sWidth()/2)); 777 vTranslate.y = (getHeight()/2) - (scale * (sHeight()/2)); 778 } 779 780 fitToBounds(true); 781 refreshRequiredTiles(eagerLoadingEnabled); 782 } 783 } else if (isQuickScaling) { 784 // One finger zoom 785 // Stole Google's Magical Formula™ to make sure it feels the exact same 786 float dist = Math.abs(quickScaleVStart.y - event.getY()) * 2 + quickScaleThreshold; 787 788 if (quickScaleLastDistance == -1f) { 789 quickScaleLastDistance = dist; 790 } 791 boolean isUpwards = event.getY() > quickScaleVLastPoint.y; 792 quickScaleVLastPoint.set(0, event.getY()); 793 794 float spanDiff = Math.abs(1 - (dist / quickScaleLastDistance)) * 0.5f; 795 796 if (spanDiff > 0.03f || quickScaleMoved) { 797 quickScaleMoved = true; 798 799 float multiplier = 1; 800 if (quickScaleLastDistance > 0) { 801 multiplier = isUpwards ? (1 + spanDiff) : (1 - spanDiff); 802 } 803 804 double previousScale = scale; 805 scale = Math.max(minScale(), Math.min(maxScale, scale * multiplier)); 806 807 if (panEnabled) { 808 float vLeftStart = vCenterStart.x - vTranslateStart.x; 809 float vTopStart = vCenterStart.y - vTranslateStart.y; 810 float vLeftNow = vLeftStart * (scale/scaleStart); 811 float vTopNow = vTopStart * (scale/scaleStart); 812 vTranslate.x = vCenterStart.x - vLeftNow; 813 vTranslate.y = vCenterStart.y - vTopNow; 814 if ((previousScale * sHeight() < getHeight() && scale * sHeight() >= getHeight()) || (previousScale * sWidth() < getWidth() && scale * sWidth() >= getWidth())) { 815 fitToBounds(true); 816 vCenterStart.set(sourceToViewCoord(quickScaleSCenter)); 817 vTranslateStart.set(vTranslate); 818 scaleStart = scale; 819 dist = 0; 820 } 821 } else if (sRequestedCenter != null) { 822 // With a center specified from code, zoom around that point. 823 vTranslate.x = (getWidth()/2) - (scale * sRequestedCenter.x); 824 vTranslate.y = (getHeight()/2) - (scale * sRequestedCenter.y); 825 } else { 826 // With no requested center, scale around the image center. 827 vTranslate.x = (getWidth()/2) - (scale * (sWidth()/2)); 828 vTranslate.y = (getHeight()/2) - (scale * (sHeight()/2)); 829 } 830 } 831 832 quickScaleLastDistance = dist; 833 834 fitToBounds(true); 835 refreshRequiredTiles(eagerLoadingEnabled); 836 837 consumed = true; 838 } else if (!isZooming) { 839 // One finger pan - translate the image. We do this calculation even with pan disabled so click 840 // and long click behaviour is preserved. 841 float dx = Math.abs(event.getX() - vCenterStart.x); 842 float dy = Math.abs(event.getY() - vCenterStart.y); 843 844 //On the Samsung S6 long click event does not work, because the dx > 5 usually true 845 float offset = density * 5; 846 if (dx > offset || dy > offset || isPanning) { 847 consumed = true; 848 vTranslate.x = vTranslateStart.x + (event.getX() - vCenterStart.x); 849 vTranslate.y = vTranslateStart.y + (event.getY() - vCenterStart.y); 850 851 float lastX = vTranslate.x; 852 float lastY = vTranslate.y; 853 fitToBounds(true); 854 boolean atXEdge = lastX != vTranslate.x; 855 boolean atYEdge = lastY != vTranslate.y; 856 boolean edgeXSwipe = atXEdge && dx > dy && !isPanning; 857 boolean edgeYSwipe = atYEdge && dy > dx && !isPanning; 858 boolean yPan = lastY == vTranslate.y && dy > offset * 3; 859 if (!edgeXSwipe && !edgeYSwipe && (!atXEdge || !atYEdge || yPan || isPanning)) { 860 isPanning = true; 861 } else if (dx > offset || dy > offset) { 862 // Haven't panned the image, and we're at the left or right edge. Switch to page swipe. 863 maxTouchCount = 0; 864 handler.removeMessages(MESSAGE_LONG_CLICK); 865 requestDisallowInterceptTouchEvent(false); 866 } 867 if (!panEnabled) { 868 vTranslate.x = vTranslateStart.x; 869 vTranslate.y = vTranslateStart.y; 870 requestDisallowInterceptTouchEvent(false); 871 } 872 873 refreshRequiredTiles(eagerLoadingEnabled); 874 } 875 } 876 } 877 if (consumed) { 878 handler.removeMessages(MESSAGE_LONG_CLICK); 879 invalidate(); 880 return true; 881 } 882 break; 883 case MotionEvent.ACTION_UP: 884 case MotionEvent.ACTION_POINTER_UP: 885 case MotionEvent.ACTION_POINTER_2_UP: 886 handler.removeMessages(MESSAGE_LONG_CLICK); 887 if (isQuickScaling) { 888 isQuickScaling = false; 889 if (!quickScaleMoved) { 890 doubleTapZoom(quickScaleSCenter, vCenterStart); 891 } 892 } 893 if (maxTouchCount > 0 && (isZooming || isPanning)) { 894 if (isZooming && touchCount == 2) { 895 // Convert from zoom to pan with remaining touch 896 isPanning = true; 897 vTranslateStart.set(vTranslate.x, vTranslate.y); 898 if (event.getActionIndex() == 1) { 899 vCenterStart.set(event.getX(0), event.getY(0)); 900 } else { 901 vCenterStart.set(event.getX(1), event.getY(1)); 902 } 903 } 904 if (touchCount < 3) { 905 // End zooming when only one touch point 906 isZooming = false; 907 } 908 if (touchCount < 2) { 909 // End panning when no touch points 910 isPanning = false; 911 maxTouchCount = 0; 912 } 913 // Trigger load of tiles now required 914 refreshRequiredTiles(true); 915 return true; 916 } 917 if (touchCount == 1) { 918 isZooming = false; 919 isPanning = false; 920 maxTouchCount = 0; 921 } 922 return true; 923 } 924 return false; 925 } 926 requestDisallowInterceptTouchEvent(boolean disallowIntercept)927 private void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 928 ViewParent parent = getParent(); 929 if (parent != null) { 930 parent.requestDisallowInterceptTouchEvent(disallowIntercept); 931 } 932 } 933 934 /** 935 * Double tap zoom handler triggered from gesture detector or on touch, depending on whether 936 * quick scale is enabled. 937 */ doubleTapZoom(PointF sCenter, PointF vFocus)938 private void doubleTapZoom(PointF sCenter, PointF vFocus) { 939 if (!panEnabled) { 940 if (sRequestedCenter != null) { 941 // With a center specified from code, zoom around that point. 942 sCenter.x = sRequestedCenter.x; 943 sCenter.y = sRequestedCenter.y; 944 } else { 945 // With no requested center, scale around the image center. 946 sCenter.x = sWidth()/2; 947 sCenter.y = sHeight()/2; 948 } 949 } 950 float doubleTapZoomScale = Math.min(maxScale, SubsamplingScaleImageView.this.doubleTapZoomScale); 951 boolean zoomIn = (scale <= doubleTapZoomScale * 0.9) || scale == minScale; 952 float targetScale = zoomIn ? doubleTapZoomScale : minScale(); 953 if (doubleTapZoomStyle == ZOOM_FOCUS_CENTER_IMMEDIATE) { 954 setScaleAndCenter(targetScale, sCenter); 955 } else if (doubleTapZoomStyle == ZOOM_FOCUS_CENTER || !zoomIn || !panEnabled) { 956 new AnimationBuilder(targetScale, sCenter).withInterruptible(false).withDuration(doubleTapZoomDuration).withOrigin(ORIGIN_DOUBLE_TAP_ZOOM).start(); 957 } else if (doubleTapZoomStyle == ZOOM_FOCUS_FIXED) { 958 new AnimationBuilder(targetScale, sCenter, vFocus).withInterruptible(false).withDuration(doubleTapZoomDuration).withOrigin(ORIGIN_DOUBLE_TAP_ZOOM).start(); 959 } 960 invalidate(); 961 } 962 963 /** 964 * Draw method should not be called until the view has dimensions so the first calls are used as triggers to calculate 965 * the scaling and tiling required. Once the view is setup, tiles are displayed as they are loaded. 966 */ 967 @Override onDraw(Canvas canvas)968 protected void onDraw(Canvas canvas) { 969 super.onDraw(canvas); 970 createPaints(); 971 972 // If image or view dimensions are not known yet, abort. 973 if (sWidth == 0 || sHeight == 0 || getWidth() == 0 || getHeight() == 0) { 974 return; 975 } 976 977 // When using tiles, on first render with no tile map ready, initialise it and kick off async base image loading. 978 if (tileMap == null && decoder != null) { 979 initialiseBaseLayer(getMaxBitmapDimensions(canvas)); 980 } 981 982 // If image has been loaded or supplied as a bitmap, onDraw may be the first time the view has 983 // dimensions and therefore the first opportunity to set scale and translate. If this call returns 984 // false there is nothing to be drawn so return immediately. 985 if (!checkReady()) { 986 return; 987 } 988 989 // Set scale and translate before draw. 990 preDraw(); 991 992 // If animating scale, calculate current scale and center with easing equations 993 if (anim != null && anim.vFocusStart != null) { 994 // Store current values so we can send an event if they change 995 float scaleBefore = scale; 996 if (vTranslateBefore == null) { vTranslateBefore = new PointF(0, 0); } 997 vTranslateBefore.set(vTranslate); 998 999 long scaleElapsed = System.currentTimeMillis() - anim.time; 1000 boolean finished = scaleElapsed > anim.duration; 1001 scaleElapsed = Math.min(scaleElapsed, anim.duration); 1002 scale = ease(anim.easing, scaleElapsed, anim.scaleStart, anim.scaleEnd - anim.scaleStart, anim.duration); 1003 1004 // Apply required animation to the focal point 1005 float vFocusNowX = ease(anim.easing, scaleElapsed, anim.vFocusStart.x, anim.vFocusEnd.x - anim.vFocusStart.x, anim.duration); 1006 float vFocusNowY = ease(anim.easing, scaleElapsed, anim.vFocusStart.y, anim.vFocusEnd.y - anim.vFocusStart.y, anim.duration); 1007 // Find out where the focal point is at this scale and adjust its position to follow the animation path 1008 vTranslate.x -= sourceToViewX(anim.sCenterEnd.x) - vFocusNowX; 1009 vTranslate.y -= sourceToViewY(anim.sCenterEnd.y) - vFocusNowY; 1010 1011 // For translate anims, showing the image non-centered is never allowed, for scaling anims it is during the animation. 1012 fitToBounds(finished || (anim.scaleStart == anim.scaleEnd)); 1013 sendStateChanged(scaleBefore, vTranslateBefore, anim.origin); 1014 refreshRequiredTiles(finished); 1015 if (finished) { 1016 if (anim.listener != null) { 1017 try { 1018 anim.listener.onComplete(); 1019 } catch (Exception e) { 1020 Log.w(TAG, "Error thrown by animation listener", e); 1021 } 1022 } 1023 anim = null; 1024 } 1025 invalidate(); 1026 } 1027 1028 if (tileMap != null && isBaseLayerReady()) { 1029 1030 // Optimum sample size for current scale 1031 int sampleSize = Math.min(fullImageSampleSize, calculateInSampleSize(scale)); 1032 1033 // First check for missing tiles - if there are any we need the base layer underneath to avoid gaps 1034 boolean hasMissingTiles = false; 1035 for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) { 1036 if (tileMapEntry.getKey() == sampleSize) { 1037 for (Tile tile : tileMapEntry.getValue()) { 1038 if (tile.visible && (tile.loading || tile.bitmap == null)) { 1039 hasMissingTiles = true; 1040 } 1041 } 1042 } 1043 } 1044 1045 // Render all loaded tiles. LinkedHashMap used for bottom up rendering - lower res tiles underneath. 1046 for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) { 1047 if (tileMapEntry.getKey() == sampleSize || hasMissingTiles) { 1048 for (Tile tile : tileMapEntry.getValue()) { 1049 sourceToViewRect(tile.sRect, tile.vRect); 1050 if (!tile.loading && tile.bitmap != null) { 1051 if (tileBgPaint != null) { 1052 canvas.drawRect(tile.vRect, tileBgPaint); 1053 } 1054 if (matrix == null) { matrix = new Matrix(); } 1055 matrix.reset(); 1056 setMatrixArray(srcArray, 0, 0, tile.bitmap.getWidth(), 0, tile.bitmap.getWidth(), tile.bitmap.getHeight(), 0, tile.bitmap.getHeight()); 1057 if (getRequiredRotation() == ORIENTATION_0) { 1058 setMatrixArray(dstArray, tile.vRect.left, tile.vRect.top, tile.vRect.right, tile.vRect.top, tile.vRect.right, tile.vRect.bottom, tile.vRect.left, tile.vRect.bottom); 1059 } else if (getRequiredRotation() == ORIENTATION_90) { 1060 setMatrixArray(dstArray, tile.vRect.right, tile.vRect.top, tile.vRect.right, tile.vRect.bottom, tile.vRect.left, tile.vRect.bottom, tile.vRect.left, tile.vRect.top); 1061 } else if (getRequiredRotation() == ORIENTATION_180) { 1062 setMatrixArray(dstArray, tile.vRect.right, tile.vRect.bottom, tile.vRect.left, tile.vRect.bottom, tile.vRect.left, tile.vRect.top, tile.vRect.right, tile.vRect.top); 1063 } else if (getRequiredRotation() == ORIENTATION_270) { 1064 setMatrixArray(dstArray, tile.vRect.left, tile.vRect.bottom, tile.vRect.left, tile.vRect.top, tile.vRect.right, tile.vRect.top, tile.vRect.right, tile.vRect.bottom); 1065 } 1066 matrix.setPolyToPoly(srcArray, 0, dstArray, 0, 4); 1067 canvas.drawBitmap(tile.bitmap, matrix, bitmapPaint); 1068 if (debug) { 1069 canvas.drawRect(tile.vRect, debugLinePaint); 1070 } 1071 } else if (tile.loading && debug) { 1072 canvas.drawText("LOADING", tile.vRect.left + px(5), tile.vRect.top + px(35), debugTextPaint); 1073 } 1074 if (tile.visible && debug) { 1075 canvas.drawText("ISS " + tile.sampleSize + " RECT " + tile.sRect.top + "," + tile.sRect.left + "," + tile.sRect.bottom + "," + tile.sRect.right, tile.vRect.left + px(5), tile.vRect.top + px(15), debugTextPaint); 1076 } 1077 } 1078 } 1079 } 1080 1081 } else if (bitmap != null) { 1082 1083 float xScale = scale, yScale = scale; 1084 if (bitmapIsPreview) { 1085 xScale = scale * ((float)sWidth/bitmap.getWidth()); 1086 yScale = scale * ((float)sHeight/bitmap.getHeight()); 1087 } 1088 1089 if (matrix == null) { matrix = new Matrix(); } 1090 matrix.reset(); 1091 matrix.postScale(xScale, yScale); 1092 matrix.postRotate(getRequiredRotation()); 1093 matrix.postTranslate(vTranslate.x, vTranslate.y); 1094 1095 if (getRequiredRotation() == ORIENTATION_180) { 1096 matrix.postTranslate(scale * sWidth, scale * sHeight); 1097 } else if (getRequiredRotation() == ORIENTATION_90) { 1098 matrix.postTranslate(scale * sHeight, 0); 1099 } else if (getRequiredRotation() == ORIENTATION_270) { 1100 matrix.postTranslate(0, scale * sWidth); 1101 } 1102 1103 if (tileBgPaint != null) { 1104 if (sRect == null) { sRect = new RectF(); } 1105 sRect.set(0f, 0f, bitmapIsPreview ? bitmap.getWidth() : sWidth, bitmapIsPreview ? bitmap.getHeight() : sHeight); 1106 matrix.mapRect(sRect); 1107 canvas.drawRect(sRect, tileBgPaint); 1108 } 1109 canvas.drawBitmap(bitmap, matrix, bitmapPaint); 1110 1111 } 1112 1113 if (debug) { 1114 canvas.drawText("Scale: " + String.format(Locale.ENGLISH, "%.2f", scale) + " (" + String.format(Locale.ENGLISH, "%.2f", minScale()) + " - " + String.format(Locale.ENGLISH, "%.2f", maxScale) + ")", px(5), px(15), debugTextPaint); 1115 canvas.drawText("Translate: " + String.format(Locale.ENGLISH, "%.2f", vTranslate.x) + ":" + String.format(Locale.ENGLISH, "%.2f", vTranslate.y), px(5), px(30), debugTextPaint); 1116 PointF center = getCenter(); 1117 canvas.drawText("Source center: " + String.format(Locale.ENGLISH, "%.2f", center.x) + ":" + String.format(Locale.ENGLISH, "%.2f", center.y), px(5), px(45), debugTextPaint); 1118 if (anim != null) { 1119 PointF vCenterStart = sourceToViewCoord(anim.sCenterStart); 1120 PointF vCenterEndRequested = sourceToViewCoord(anim.sCenterEndRequested); 1121 PointF vCenterEnd = sourceToViewCoord(anim.sCenterEnd); 1122 canvas.drawCircle(vCenterStart.x, vCenterStart.y, px(10), debugLinePaint); 1123 debugLinePaint.setColor(Color.RED); 1124 canvas.drawCircle(vCenterEndRequested.x, vCenterEndRequested.y, px(20), debugLinePaint); 1125 debugLinePaint.setColor(Color.BLUE); 1126 canvas.drawCircle(vCenterEnd.x, vCenterEnd.y, px(25), debugLinePaint); 1127 debugLinePaint.setColor(Color.CYAN); 1128 canvas.drawCircle(getWidth() / 2, getHeight() / 2, px(30), debugLinePaint); 1129 } 1130 if (vCenterStart != null) { 1131 debugLinePaint.setColor(Color.RED); 1132 canvas.drawCircle(vCenterStart.x, vCenterStart.y, px(20), debugLinePaint); 1133 } 1134 if (quickScaleSCenter != null) { 1135 debugLinePaint.setColor(Color.BLUE); 1136 canvas.drawCircle(sourceToViewX(quickScaleSCenter.x), sourceToViewY(quickScaleSCenter.y), px(35), debugLinePaint); 1137 } 1138 if (quickScaleVStart != null && isQuickScaling) { 1139 debugLinePaint.setColor(Color.CYAN); 1140 canvas.drawCircle(quickScaleVStart.x, quickScaleVStart.y, px(30), debugLinePaint); 1141 } 1142 debugLinePaint.setColor(Color.MAGENTA); 1143 } 1144 } 1145 1146 /** 1147 * Helper method for setting the values of a tile matrix array. 1148 */ setMatrixArray(float[] array, float f0, float f1, float f2, float f3, float f4, float f5, float f6, float f7)1149 private void setMatrixArray(float[] array, float f0, float f1, float f2, float f3, float f4, float f5, float f6, float f7) { 1150 array[0] = f0; 1151 array[1] = f1; 1152 array[2] = f2; 1153 array[3] = f3; 1154 array[4] = f4; 1155 array[5] = f5; 1156 array[6] = f6; 1157 array[7] = f7; 1158 } 1159 1160 /** 1161 * Checks whether the base layer of tiles or full size bitmap is ready. 1162 */ isBaseLayerReady()1163 private boolean isBaseLayerReady() { 1164 if (bitmap != null && !bitmapIsPreview) { 1165 return true; 1166 } else if (tileMap != null) { 1167 boolean baseLayerReady = true; 1168 for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) { 1169 if (tileMapEntry.getKey() == fullImageSampleSize) { 1170 for (Tile tile : tileMapEntry.getValue()) { 1171 if (tile.loading || tile.bitmap == null) { 1172 baseLayerReady = false; 1173 } 1174 } 1175 } 1176 } 1177 return baseLayerReady; 1178 } 1179 return false; 1180 } 1181 1182 /** 1183 * Check whether view and image dimensions are known and either a preview, full size image or 1184 * base layer tiles are loaded. First time, send ready event to listener. The next draw will 1185 * display an image. 1186 */ checkReady()1187 private boolean checkReady() { 1188 boolean ready = getWidth() > 0 && getHeight() > 0 && sWidth > 0 && sHeight > 0 && (bitmap != null || isBaseLayerReady()); 1189 if (!readySent && ready) { 1190 preDraw(); 1191 readySent = true; 1192 onReady(); 1193 if (onImageEventListener != null) { 1194 onImageEventListener.onReady(); 1195 } 1196 } 1197 return ready; 1198 } 1199 1200 /** 1201 * Check whether either the full size bitmap or base layer tiles are loaded. First time, send image 1202 * loaded event to listener. 1203 */ checkImageLoaded()1204 private boolean checkImageLoaded() { 1205 boolean imageLoaded = isBaseLayerReady(); 1206 if (!imageLoadedSent && imageLoaded) { 1207 preDraw(); 1208 imageLoadedSent = true; 1209 onImageLoaded(); 1210 if (onImageEventListener != null) { 1211 onImageEventListener.onImageLoaded(); 1212 } 1213 } 1214 return imageLoaded; 1215 } 1216 1217 /** 1218 * Creates Paint objects once when first needed. 1219 */ createPaints()1220 private void createPaints() { 1221 if (bitmapPaint == null) { 1222 bitmapPaint = new Paint(); 1223 bitmapPaint.setAntiAlias(true); 1224 bitmapPaint.setFilterBitmap(true); 1225 bitmapPaint.setDither(true); 1226 } 1227 if ((debugTextPaint == null || debugLinePaint == null) && debug) { 1228 debugTextPaint = new Paint(); 1229 debugTextPaint.setTextSize(px(12)); 1230 debugTextPaint.setColor(Color.MAGENTA); 1231 debugTextPaint.setStyle(Style.FILL); 1232 debugLinePaint = new Paint(); 1233 debugLinePaint.setColor(Color.MAGENTA); 1234 debugLinePaint.setStyle(Style.STROKE); 1235 debugLinePaint.setStrokeWidth(px(1)); 1236 } 1237 } 1238 1239 /** 1240 * Called on first draw when the view has dimensions. Calculates the initial sample size and starts async loading of 1241 * the base layer image - the whole source subsampled as necessary. 1242 */ initialiseBaseLayer(Point maxTileDimensions)1243 private synchronized void initialiseBaseLayer(Point maxTileDimensions) { 1244 debug("initialiseBaseLayer maxTileDimensions=%dx%d", maxTileDimensions.x, maxTileDimensions.y); 1245 1246 satTemp = new ScaleAndTranslate(0f, new PointF(0, 0)); 1247 fitToBounds(true, satTemp); 1248 1249 // Load double resolution - next level will be split into four tiles and at the center all four are required, 1250 // so don't bother with tiling until the next level 16 tiles are needed. 1251 fullImageSampleSize = calculateInSampleSize(satTemp.scale); 1252 if (fullImageSampleSize > 1) { 1253 fullImageSampleSize /= 2; 1254 } 1255 1256 if (fullImageSampleSize == 1 && sRegion == null && sWidth() < maxTileDimensions.x && sHeight() < maxTileDimensions.y) { 1257 1258 // Whole image is required at native resolution, and is smaller than the canvas max bitmap size. 1259 // Use BitmapDecoder for better image support. 1260 decoder.recycle(); 1261 decoder = null; 1262 BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false); 1263 execute(task); 1264 1265 } else { 1266 1267 initialiseTileMap(maxTileDimensions); 1268 1269 List<Tile> baseGrid = tileMap.get(fullImageSampleSize); 1270 for (Tile baseTile : baseGrid) { 1271 TileLoadTask task = new TileLoadTask(this, decoder, baseTile); 1272 execute(task); 1273 } 1274 refreshRequiredTiles(true); 1275 1276 } 1277 1278 } 1279 1280 /** 1281 * Loads the optimum tiles for display at the current scale and translate, so the screen can be filled with tiles 1282 * that are at least as high resolution as the screen. Frees up bitmaps that are now off the screen. 1283 * @param load Whether to load the new tiles needed. Use false while scrolling/panning for performance. 1284 */ refreshRequiredTiles(boolean load)1285 private void refreshRequiredTiles(boolean load) { 1286 if (decoder == null || tileMap == null) { return; } 1287 1288 int sampleSize = Math.min(fullImageSampleSize, calculateInSampleSize(scale)); 1289 1290 // Load tiles of the correct sample size that are on screen. Discard tiles off screen, and those that are higher 1291 // resolution than required, or lower res than required but not the base layer, so the base layer is always present. 1292 for (Map.Entry<Integer, List<Tile>> tileMapEntry : tileMap.entrySet()) { 1293 for (Tile tile : tileMapEntry.getValue()) { 1294 if (tile.sampleSize < sampleSize || (tile.sampleSize > sampleSize && tile.sampleSize != fullImageSampleSize)) { 1295 tile.visible = false; 1296 if (tile.bitmap != null) { 1297 tile.bitmap.recycle(); 1298 tile.bitmap = null; 1299 } 1300 } 1301 if (tile.sampleSize == sampleSize) { 1302 if (tileVisible(tile)) { 1303 tile.visible = true; 1304 if (!tile.loading && tile.bitmap == null && load) { 1305 TileLoadTask task = new TileLoadTask(this, decoder, tile); 1306 execute(task); 1307 } 1308 } else if (tile.sampleSize != fullImageSampleSize) { 1309 tile.visible = false; 1310 if (tile.bitmap != null) { 1311 tile.bitmap.recycle(); 1312 tile.bitmap = null; 1313 } 1314 } 1315 } else if (tile.sampleSize == fullImageSampleSize) { 1316 tile.visible = true; 1317 } 1318 } 1319 } 1320 1321 } 1322 1323 /** 1324 * Determine whether tile is visible. 1325 */ tileVisible(Tile tile)1326 private boolean tileVisible(Tile tile) { 1327 float sVisLeft = viewToSourceX(0), 1328 sVisRight = viewToSourceX(getWidth()), 1329 sVisTop = viewToSourceY(0), 1330 sVisBottom = viewToSourceY(getHeight()); 1331 return !(sVisLeft > tile.sRect.right || tile.sRect.left > sVisRight || sVisTop > tile.sRect.bottom || tile.sRect.top > sVisBottom); 1332 } 1333 1334 /** 1335 * Sets scale and translate ready for the next draw. 1336 */ preDraw()1337 private void preDraw() { 1338 if (getWidth() == 0 || getHeight() == 0 || sWidth <= 0 || sHeight <= 0) { 1339 return; 1340 } 1341 1342 // If waiting to translate to new center position, set translate now 1343 if (sPendingCenter != null && pendingScale != null) { 1344 scale = pendingScale; 1345 if (vTranslate == null) { 1346 vTranslate = new PointF(); 1347 } 1348 vTranslate.x = (getWidth()/2) - (scale * sPendingCenter.x); 1349 vTranslate.y = (getHeight()/2) - (scale * sPendingCenter.y); 1350 sPendingCenter = null; 1351 pendingScale = null; 1352 fitToBounds(true); 1353 refreshRequiredTiles(true); 1354 } 1355 1356 // On first display of base image set up position, and in other cases make sure scale is correct. 1357 fitToBounds(false); 1358 } 1359 1360 /** 1361 * Calculates sample size to fit the source image in given bounds. 1362 */ calculateInSampleSize(float scale)1363 private int calculateInSampleSize(float scale) { 1364 if (minimumTileDpi > 0) { 1365 DisplayMetrics metrics = getResources().getDisplayMetrics(); 1366 float averageDpi = (metrics.xdpi + metrics.ydpi)/2; 1367 scale = (minimumTileDpi/averageDpi) * scale; 1368 } 1369 1370 int reqWidth = (int)(sWidth() * scale); 1371 int reqHeight = (int)(sHeight() * scale); 1372 1373 // Raw height and width of image 1374 int inSampleSize = 1; 1375 if (reqWidth == 0 || reqHeight == 0) { 1376 return 32; 1377 } 1378 1379 if (sHeight() > reqHeight || sWidth() > reqWidth) { 1380 1381 // Calculate ratios of height and width to requested height and width 1382 final int heightRatio = Math.round((float) sHeight() / (float) reqHeight); 1383 final int widthRatio = Math.round((float) sWidth() / (float) reqWidth); 1384 1385 // Choose the smallest ratio as inSampleSize value, this will guarantee 1386 // a final image with both dimensions larger than or equal to the 1387 // requested height and width. 1388 inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; 1389 } 1390 1391 // We want the actual sample size that will be used, so round down to nearest power of 2. 1392 int power = 1; 1393 while (power * 2 < inSampleSize) { 1394 power = power * 2; 1395 } 1396 1397 return power; 1398 } 1399 1400 /** 1401 * Adjusts hypothetical future scale and translate values to keep scale within the allowed range and the image on screen. Minimum scale 1402 * is set so one dimension fills the view and the image is centered on the other dimension. Used to calculate what the target of an 1403 * animation should be. 1404 * @param center Whether the image should be centered in the dimension it's too small to fill. While animating this can be false to avoid changes in direction as bounds are reached. 1405 * @param sat The scale we want and the translation we're aiming for. The values are adjusted to be valid. 1406 */ 1407 private void fitToBounds(boolean center, ScaleAndTranslate sat) { 1408 if (panLimit == PAN_LIMIT_OUTSIDE && isReady()) { 1409 center = false; 1410 } 1411 1412 PointF vTranslate = sat.vTranslate; 1413 float scale = limitedScale(sat.scale); 1414 float scaleWidth = scale * sWidth(); 1415 float scaleHeight = scale * sHeight(); 1416 1417 if (panLimit == PAN_LIMIT_CENTER && isReady()) { 1418 vTranslate.x = Math.max(vTranslate.x, getWidth()/2 - scaleWidth); 1419 vTranslate.y = Math.max(vTranslate.y, getHeight()/2 - scaleHeight); 1420 } else if (center) { 1421 vTranslate.x = Math.max(vTranslate.x, getWidth() - scaleWidth); 1422 vTranslate.y = Math.max(vTranslate.y, getHeight() - scaleHeight); 1423 } else { 1424 vTranslate.x = Math.max(vTranslate.x, -scaleWidth); 1425 vTranslate.y = Math.max(vTranslate.y, -scaleHeight); 1426 } 1427 1428 // Asymmetric padding adjustments 1429 float xPaddingRatio = getPaddingLeft() > 0 || getPaddingRight() > 0 ? getPaddingLeft()/(float)(getPaddingLeft() + getPaddingRight()) : 0.5f; 1430 float yPaddingRatio = getPaddingTop() > 0 || getPaddingBottom() > 0 ? getPaddingTop()/(float)(getPaddingTop() + getPaddingBottom()) : 0.5f; 1431 1432 float maxTx; 1433 float maxTy; 1434 if (panLimit == PAN_LIMIT_CENTER && isReady()) { 1435 maxTx = Math.max(0, getWidth()/2); 1436 maxTy = Math.max(0, getHeight()/2); 1437 } else if (center) { 1438 maxTx = Math.max(0, (getWidth() - scaleWidth) * xPaddingRatio); 1439 maxTy = Math.max(0, (getHeight() - scaleHeight) * yPaddingRatio); 1440 } else { 1441 maxTx = Math.max(0, getWidth()); 1442 maxTy = Math.max(0, getHeight()); 1443 } 1444 1445 vTranslate.x = Math.min(vTranslate.x, maxTx); 1446 vTranslate.y = Math.min(vTranslate.y, maxTy); 1447 1448 sat.scale = scale; 1449 } 1450 1451 /** 1452 * Adjusts current scale and translate values to keep scale within the allowed range and the image on screen. Minimum scale 1453 * is set so one dimension fills the view and the image is centered on the other dimension. 1454 * @param center Whether the image should be centered in the dimension it's too small to fill. While animating this can be false to avoid changes in direction as bounds are reached. 1455 */ fitToBounds(boolean center)1456 private void fitToBounds(boolean center) { 1457 boolean init = false; 1458 if (vTranslate == null) { 1459 init = true; 1460 vTranslate = new PointF(0, 0); 1461 } 1462 if (satTemp == null) { 1463 satTemp = new ScaleAndTranslate(0, new PointF(0, 0)); 1464 } 1465 satTemp.scale = scale; 1466 satTemp.vTranslate.set(vTranslate); 1467 fitToBounds(center, satTemp); 1468 scale = satTemp.scale; 1469 vTranslate.set(satTemp.vTranslate); 1470 if (init && minimumScaleType != SCALE_TYPE_START) { 1471 vTranslate.set(vTranslateForSCenter(sWidth()/2, sHeight()/2, scale)); 1472 } 1473 } 1474 1475 /** 1476 * Once source image and view dimensions are known, creates a map of sample size to tile grid. 1477 */ initialiseTileMap(Point maxTileDimensions)1478 private void initialiseTileMap(Point maxTileDimensions) { 1479 debug("initialiseTileMap maxTileDimensions=%dx%d", maxTileDimensions.x, maxTileDimensions.y); 1480 this.tileMap = new LinkedHashMap<>(); 1481 int sampleSize = fullImageSampleSize; 1482 int xTiles = 1; 1483 int yTiles = 1; 1484 while (true) { 1485 int sTileWidth = sWidth()/xTiles; 1486 int sTileHeight = sHeight()/yTiles; 1487 int subTileWidth = sTileWidth/sampleSize; 1488 int subTileHeight = sTileHeight/sampleSize; 1489 while (subTileWidth + xTiles + 1 > maxTileDimensions.x || (subTileWidth > getWidth() * 1.25 && sampleSize < fullImageSampleSize)) { 1490 xTiles += 1; 1491 sTileWidth = sWidth()/xTiles; 1492 subTileWidth = sTileWidth/sampleSize; 1493 } 1494 while (subTileHeight + yTiles + 1 > maxTileDimensions.y || (subTileHeight > getHeight() * 1.25 && sampleSize < fullImageSampleSize)) { 1495 yTiles += 1; 1496 sTileHeight = sHeight()/yTiles; 1497 subTileHeight = sTileHeight/sampleSize; 1498 } 1499 List<Tile> tileGrid = new ArrayList<>(xTiles * yTiles); 1500 for (int x = 0; x < xTiles; x++) { 1501 for (int y = 0; y < yTiles; y++) { 1502 Tile tile = new Tile(); 1503 tile.sampleSize = sampleSize; 1504 tile.visible = sampleSize == fullImageSampleSize; 1505 tile.sRect = new Rect( 1506 x * sTileWidth, 1507 y * sTileHeight, 1508 x == xTiles - 1 ? sWidth() : (x + 1) * sTileWidth, 1509 y == yTiles - 1 ? sHeight() : (y + 1) * sTileHeight 1510 ); 1511 tile.vRect = new Rect(0, 0, 0, 0); 1512 tile.fileSRect = new Rect(tile.sRect); 1513 tileGrid.add(tile); 1514 } 1515 } 1516 tileMap.put(sampleSize, tileGrid); 1517 if (sampleSize == 1) { 1518 break; 1519 } else { 1520 sampleSize /= 2; 1521 } 1522 } 1523 } 1524 1525 /** 1526 * Async task used to get image details without blocking the UI thread. 1527 */ 1528 private static class TilesInitTask extends AsyncTask<Void, Void, int[]> { 1529 private final WeakReference<SubsamplingScaleImageView> viewRef; 1530 private final WeakReference<Context> contextRef; 1531 private final WeakReference<DecoderFactory<? extends ImageRegionDecoder>> decoderFactoryRef; 1532 private final Uri source; 1533 private ImageRegionDecoder decoder; 1534 private Exception exception; 1535 TilesInitTask(SubsamplingScaleImageView view, Context context, DecoderFactory<? extends ImageRegionDecoder> decoderFactory, Uri source)1536 TilesInitTask(SubsamplingScaleImageView view, Context context, DecoderFactory<? extends ImageRegionDecoder> decoderFactory, Uri source) { 1537 this.viewRef = new WeakReference<>(view); 1538 this.contextRef = new WeakReference<>(context); 1539 this.decoderFactoryRef = new WeakReference<DecoderFactory<? extends ImageRegionDecoder>>(decoderFactory); 1540 this.source = source; 1541 } 1542 1543 @Override doInBackground(Void... params)1544 protected int[] doInBackground(Void... params) { 1545 try { 1546 String sourceUri = source.toString(); 1547 Context context = contextRef.get(); 1548 DecoderFactory<? extends ImageRegionDecoder> decoderFactory = decoderFactoryRef.get(); 1549 SubsamplingScaleImageView view = viewRef.get(); 1550 if (context != null && decoderFactory != null && view != null) { 1551 view.debug("TilesInitTask.doInBackground"); 1552 decoder = decoderFactory.make(); 1553 Point dimensions = decoder.init(context, source); 1554 int sWidth = dimensions.x; 1555 int sHeight = dimensions.y; 1556 int exifOrientation = view.getExifOrientation(context, sourceUri); 1557 if (view.sRegion != null) { 1558 view.sRegion.left = Math.max(0, view.sRegion.left); 1559 view.sRegion.top = Math.max(0, view.sRegion.top); 1560 view.sRegion.right = Math.min(sWidth, view.sRegion.right); 1561 view.sRegion.bottom = Math.min(sHeight, view.sRegion.bottom); 1562 sWidth = view.sRegion.width(); 1563 sHeight = view.sRegion.height(); 1564 } 1565 return new int[] { sWidth, sHeight, exifOrientation }; 1566 } 1567 } catch (Exception e) { 1568 Log.e(TAG, "Failed to initialise bitmap decoder", e); 1569 this.exception = e; 1570 } 1571 return null; 1572 } 1573 1574 @Override onPostExecute(int[] xyo)1575 protected void onPostExecute(int[] xyo) { 1576 final SubsamplingScaleImageView view = viewRef.get(); 1577 if (view != null) { 1578 if (decoder != null && xyo != null && xyo.length == 3) { 1579 view.onTilesInited(decoder, xyo[0], xyo[1], xyo[2]); 1580 } else if (exception != null && view.onImageEventListener != null) { 1581 view.onImageEventListener.onImageLoadError(exception); 1582 } 1583 } 1584 } 1585 } 1586 1587 /** 1588 * Called by worker task when decoder is ready and image size and EXIF orientation is known. 1589 */ onTilesInited(ImageRegionDecoder decoder, int sWidth, int sHeight, int sOrientation)1590 private synchronized void onTilesInited(ImageRegionDecoder decoder, int sWidth, int sHeight, int sOrientation) { 1591 debug("onTilesInited sWidth=%d, sHeight=%d, sOrientation=%d", sWidth, sHeight, orientation); 1592 // If actual dimensions don't match the declared size, reset everything. 1593 if (this.sWidth > 0 && this.sHeight > 0 && (this.sWidth != sWidth || this.sHeight != sHeight)) { 1594 reset(false); 1595 if (bitmap != null) { 1596 if (!bitmapIsCached) { 1597 bitmap.recycle(); 1598 } 1599 bitmap = null; 1600 if (onImageEventListener != null && bitmapIsCached) { 1601 onImageEventListener.onPreviewReleased(); 1602 } 1603 bitmapIsPreview = false; 1604 bitmapIsCached = false; 1605 } 1606 } 1607 this.decoder = decoder; 1608 this.sWidth = sWidth; 1609 this.sHeight = sHeight; 1610 this.sOrientation = sOrientation; 1611 checkReady(); 1612 if (!checkImageLoaded() && maxTileWidth > 0 && maxTileWidth != TILE_SIZE_AUTO && maxTileHeight > 0 && maxTileHeight != TILE_SIZE_AUTO && getWidth() > 0 && getHeight() > 0) { 1613 initialiseBaseLayer(new Point(maxTileWidth, maxTileHeight)); 1614 } 1615 invalidate(); 1616 requestLayout(); 1617 } 1618 1619 /** 1620 * Async task used to load images without blocking the UI thread. 1621 */ 1622 private static class TileLoadTask extends AsyncTask<Void, Void, Bitmap> { 1623 private final WeakReference<SubsamplingScaleImageView> viewRef; 1624 private final WeakReference<ImageRegionDecoder> decoderRef; 1625 private final WeakReference<Tile> tileRef; 1626 private Exception exception; 1627 TileLoadTask(SubsamplingScaleImageView view, ImageRegionDecoder decoder, Tile tile)1628 TileLoadTask(SubsamplingScaleImageView view, ImageRegionDecoder decoder, Tile tile) { 1629 this.viewRef = new WeakReference<>(view); 1630 this.decoderRef = new WeakReference<>(decoder); 1631 this.tileRef = new WeakReference<>(tile); 1632 tile.loading = true; 1633 } 1634 1635 @Override doInBackground(Void... params)1636 protected Bitmap doInBackground(Void... params) { 1637 try { 1638 SubsamplingScaleImageView view = viewRef.get(); 1639 ImageRegionDecoder decoder = decoderRef.get(); 1640 Tile tile = tileRef.get(); 1641 if (decoder != null && tile != null && view != null && decoder.isReady() && tile.visible) { 1642 view.debug("TileLoadTask.doInBackground, tile.sRect=%s, tile.sampleSize=%d", tile.sRect, tile.sampleSize); 1643 view.decoderLock.readLock().lock(); 1644 try { 1645 if (decoder.isReady()) { 1646 // Update tile's file sRect according to rotation 1647 view.fileSRect(tile.sRect, tile.fileSRect); 1648 if (view.sRegion != null) { 1649 tile.fileSRect.offset(view.sRegion.left, view.sRegion.top); 1650 } 1651 return decoder.decodeRegion(tile.fileSRect, tile.sampleSize); 1652 } else { 1653 tile.loading = false; 1654 } 1655 } finally { 1656 view.decoderLock.readLock().unlock(); 1657 } 1658 } else if (tile != null) { 1659 tile.loading = false; 1660 } 1661 } catch (Exception e) { 1662 Log.e(TAG, "Failed to decode tile", e); 1663 this.exception = e; 1664 } catch (OutOfMemoryError e) { 1665 Log.e(TAG, "Failed to decode tile - OutOfMemoryError", e); 1666 this.exception = new RuntimeException(e); 1667 } 1668 return null; 1669 } 1670 1671 @Override onPostExecute(Bitmap bitmap)1672 protected void onPostExecute(Bitmap bitmap) { 1673 final SubsamplingScaleImageView subsamplingScaleImageView = viewRef.get(); 1674 final Tile tile = tileRef.get(); 1675 if (subsamplingScaleImageView != null && tile != null) { 1676 if (bitmap != null) { 1677 tile.bitmap = bitmap; 1678 tile.loading = false; 1679 subsamplingScaleImageView.onTileLoaded(); 1680 } else if (exception != null && subsamplingScaleImageView.onImageEventListener != null) { 1681 subsamplingScaleImageView.onImageEventListener.onTileLoadError(exception); 1682 } 1683 } 1684 } 1685 } 1686 1687 /** 1688 * Called by worker task when a tile has loaded. Redraws the view. 1689 */ onTileLoaded()1690 private synchronized void onTileLoaded() { 1691 debug("onTileLoaded"); 1692 checkReady(); 1693 checkImageLoaded(); 1694 if (isBaseLayerReady() && bitmap != null) { 1695 if (!bitmapIsCached) { 1696 bitmap.recycle(); 1697 } 1698 bitmap = null; 1699 if (onImageEventListener != null && bitmapIsCached) { 1700 onImageEventListener.onPreviewReleased(); 1701 } 1702 bitmapIsPreview = false; 1703 bitmapIsCached = false; 1704 } 1705 invalidate(); 1706 } 1707 1708 /** 1709 * Async task used to load bitmap without blocking the UI thread. 1710 */ 1711 private static class BitmapLoadTask extends AsyncTask<Void, Void, Integer> { 1712 private final WeakReference<SubsamplingScaleImageView> viewRef; 1713 private final WeakReference<Context> contextRef; 1714 private final WeakReference<DecoderFactory<? extends ImageDecoder>> decoderFactoryRef; 1715 private final Uri source; 1716 private final boolean preview; 1717 private Bitmap bitmap; 1718 private Exception exception; 1719 BitmapLoadTask(SubsamplingScaleImageView view, Context context, DecoderFactory<? extends ImageDecoder> decoderFactory, Uri source, boolean preview)1720 BitmapLoadTask(SubsamplingScaleImageView view, Context context, DecoderFactory<? extends ImageDecoder> decoderFactory, Uri source, boolean preview) { 1721 this.viewRef = new WeakReference<>(view); 1722 this.contextRef = new WeakReference<>(context); 1723 this.decoderFactoryRef = new WeakReference<DecoderFactory<? extends ImageDecoder>>(decoderFactory); 1724 this.source = source; 1725 this.preview = preview; 1726 } 1727 1728 @Override doInBackground(Void... params)1729 protected Integer doInBackground(Void... params) { 1730 try { 1731 String sourceUri = source.toString(); 1732 Context context = contextRef.get(); 1733 DecoderFactory<? extends ImageDecoder> decoderFactory = decoderFactoryRef.get(); 1734 SubsamplingScaleImageView view = viewRef.get(); 1735 if (context != null && decoderFactory != null && view != null) { 1736 view.debug("BitmapLoadTask.doInBackground"); 1737 bitmap = decoderFactory.make().decode(context, source); 1738 return view.getExifOrientation(context, sourceUri); 1739 } 1740 } catch (Exception e) { 1741 Log.e(TAG, "Failed to load bitmap", e); 1742 this.exception = e; 1743 } catch (OutOfMemoryError e) { 1744 Log.e(TAG, "Failed to load bitmap - OutOfMemoryError", e); 1745 this.exception = new RuntimeException(e); 1746 } 1747 return null; 1748 } 1749 1750 @Override onPostExecute(Integer orientation)1751 protected void onPostExecute(Integer orientation) { 1752 SubsamplingScaleImageView subsamplingScaleImageView = viewRef.get(); 1753 if (subsamplingScaleImageView != null) { 1754 if (bitmap != null && orientation != null) { 1755 if (preview) { 1756 subsamplingScaleImageView.onPreviewLoaded(bitmap); 1757 } else { 1758 subsamplingScaleImageView.onImageLoaded(bitmap, orientation, false); 1759 } 1760 } else if (exception != null && subsamplingScaleImageView.onImageEventListener != null) { 1761 if (preview) { 1762 subsamplingScaleImageView.onImageEventListener.onPreviewLoadError(exception); 1763 } else { 1764 subsamplingScaleImageView.onImageEventListener.onImageLoadError(exception); 1765 } 1766 } 1767 } 1768 } 1769 } 1770 1771 /** 1772 * Called by worker task when preview image is loaded. 1773 */ onPreviewLoaded(Bitmap previewBitmap)1774 private synchronized void onPreviewLoaded(Bitmap previewBitmap) { 1775 debug("onPreviewLoaded"); 1776 if (bitmap != null || imageLoadedSent) { 1777 previewBitmap.recycle(); 1778 return; 1779 } 1780 if (pRegion != null) { 1781 bitmap = Bitmap.createBitmap(previewBitmap, pRegion.left, pRegion.top, pRegion.width(), pRegion.height()); 1782 } else { 1783 bitmap = previewBitmap; 1784 } 1785 bitmapIsPreview = true; 1786 if (checkReady()) { 1787 invalidate(); 1788 requestLayout(); 1789 } 1790 } 1791 1792 /** 1793 * Called by worker task when full size image bitmap is ready (tiling is disabled). 1794 */ onImageLoaded(Bitmap bitmap, int sOrientation, boolean bitmapIsCached)1795 private synchronized void onImageLoaded(Bitmap bitmap, int sOrientation, boolean bitmapIsCached) { 1796 debug("onImageLoaded"); 1797 // If actual dimensions don't match the declared size, reset everything. 1798 if (this.sWidth > 0 && this.sHeight > 0 && (this.sWidth != bitmap.getWidth() || this.sHeight != bitmap.getHeight())) { 1799 reset(false); 1800 } 1801 if (this.bitmap != null && !this.bitmapIsCached) { 1802 this.bitmap.recycle(); 1803 } 1804 1805 if (this.bitmap != null && this.bitmapIsCached && onImageEventListener!=null) { 1806 onImageEventListener.onPreviewReleased(); 1807 } 1808 1809 this.bitmapIsPreview = false; 1810 this.bitmapIsCached = bitmapIsCached; 1811 this.bitmap = bitmap; 1812 this.sWidth = bitmap.getWidth(); 1813 this.sHeight = bitmap.getHeight(); 1814 this.sOrientation = sOrientation; 1815 boolean ready = checkReady(); 1816 boolean imageLoaded = checkImageLoaded(); 1817 if (ready || imageLoaded) { 1818 invalidate(); 1819 requestLayout(); 1820 } 1821 } 1822 1823 /** 1824 * Helper method for load tasks. Examines the EXIF info on the image file to determine the orientation. 1825 * This will only work for external files, not assets, resources or other URIs. 1826 */ 1827 @AnyThread getExifOrientation(Context context, String sourceUri)1828 private int getExifOrientation(Context context, String sourceUri) { 1829 int exifOrientation = ORIENTATION_0; 1830 if (sourceUri.startsWith(ContentResolver.SCHEME_CONTENT)) { 1831 Cursor cursor = null; 1832 try { 1833 String[] columns = { MediaStore.Images.Media.ORIENTATION }; 1834 cursor = context.getContentResolver().query(Uri.parse(sourceUri), columns, null, null, null); 1835 if (cursor != null) { 1836 if (cursor.moveToFirst()) { 1837 int orientation = cursor.getInt(0); 1838 if (VALID_ORIENTATIONS.contains(orientation) && orientation != ORIENTATION_USE_EXIF) { 1839 exifOrientation = orientation; 1840 } else { 1841 Log.w(TAG, "Unsupported orientation: " + orientation); 1842 } 1843 } 1844 } 1845 } catch (Exception e) { 1846 Log.w(TAG, "Could not get orientation of image from media store"); 1847 } finally { 1848 if (cursor != null) { 1849 cursor.close(); 1850 } 1851 } 1852 } else if (sourceUri.startsWith(ImageSource.FILE_SCHEME) && !sourceUri.startsWith(ImageSource.ASSET_SCHEME)) { 1853 try { 1854 ExifInterface exifInterface = new ExifInterface(sourceUri.substring(ImageSource.FILE_SCHEME.length() - 1)); 1855 int orientationAttr = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); 1856 if (orientationAttr == ExifInterface.ORIENTATION_NORMAL || orientationAttr == ExifInterface.ORIENTATION_UNDEFINED) { 1857 exifOrientation = ORIENTATION_0; 1858 } else if (orientationAttr == ExifInterface.ORIENTATION_ROTATE_90) { 1859 exifOrientation = ORIENTATION_90; 1860 } else if (orientationAttr == ExifInterface.ORIENTATION_ROTATE_180) { 1861 exifOrientation = ORIENTATION_180; 1862 } else if (orientationAttr == ExifInterface.ORIENTATION_ROTATE_270) { 1863 exifOrientation = ORIENTATION_270; 1864 } else { 1865 Log.w(TAG, "Unsupported EXIF orientation: " + orientationAttr); 1866 } 1867 } catch (Exception e) { 1868 Log.w(TAG, "Could not get EXIF orientation of image"); 1869 } 1870 } 1871 return exifOrientation; 1872 } 1873 execute(AsyncTask<Void, Void, ?> asyncTask)1874 private void execute(AsyncTask<Void, Void, ?> asyncTask) { 1875 asyncTask.executeOnExecutor(executor); 1876 } 1877 1878 private static class Tile { 1879 1880 private Rect sRect; 1881 private int sampleSize; 1882 private Bitmap bitmap; 1883 private boolean loading; 1884 private boolean visible; 1885 1886 // Volatile fields instantiated once then updated before use to reduce GC. 1887 private Rect vRect; 1888 private Rect fileSRect; 1889 1890 } 1891 1892 private static class Anim { 1893 1894 private float scaleStart; // Scale at start of anim 1895 private float scaleEnd; // Scale at end of anim (target) 1896 private PointF sCenterStart; // Source center point at start 1897 private PointF sCenterEnd; // Source center point at end, adjusted for pan limits 1898 private PointF sCenterEndRequested; // Source center point that was requested, without adjustment 1899 private PointF vFocusStart; // View point that was double tapped 1900 private PointF vFocusEnd; // Where the view focal point should be moved to during the anim 1901 private long duration = 500; // How long the anim takes 1902 private boolean interruptible = true; // Whether the anim can be interrupted by a touch 1903 private int easing = EASE_IN_OUT_QUAD; // Easing style 1904 private int origin = ORIGIN_ANIM; // Animation origin (API, double tap or fling) 1905 private long time = System.currentTimeMillis(); // Start time 1906 private OnAnimationEventListener listener; // Event listener 1907 1908 } 1909 1910 private static class ScaleAndTranslate { ScaleAndTranslate(float scale, PointF vTranslate)1911 private ScaleAndTranslate(float scale, PointF vTranslate) { 1912 this.scale = scale; 1913 this.vTranslate = vTranslate; 1914 } 1915 private float scale; 1916 private final PointF vTranslate; 1917 } 1918 1919 /** 1920 * Set scale, center and orientation from saved state. 1921 */ restoreState(ImageViewState state)1922 private void restoreState(ImageViewState state) { 1923 if (state != null && state.getCenter() != null && VALID_ORIENTATIONS.contains(state.getOrientation())) { 1924 this.orientation = state.getOrientation(); 1925 this.pendingScale = state.getScale(); 1926 this.sPendingCenter = state.getCenter(); 1927 invalidate(); 1928 } 1929 } 1930 1931 /** 1932 * By default the View automatically calculates the optimal tile size. Set this to override this, and force an upper limit to the dimensions of the generated tiles. Passing {@link #TILE_SIZE_AUTO} will re-enable the default behaviour. 1933 * 1934 * @param maxPixels Maximum tile size X and Y in pixels. 1935 */ setMaxTileSize(int maxPixels)1936 public void setMaxTileSize(int maxPixels) { 1937 this.maxTileWidth = maxPixels; 1938 this.maxTileHeight = maxPixels; 1939 } 1940 1941 /** 1942 * By default the View automatically calculates the optimal tile size. Set this to override this, and force an upper limit to the dimensions of the generated tiles. Passing {@link #TILE_SIZE_AUTO} will re-enable the default behaviour. 1943 * 1944 * @param maxPixelsX Maximum tile width. 1945 * @param maxPixelsY Maximum tile height. 1946 */ setMaxTileSize(int maxPixelsX, int maxPixelsY)1947 public void setMaxTileSize(int maxPixelsX, int maxPixelsY) { 1948 this.maxTileWidth = maxPixelsX; 1949 this.maxTileHeight = maxPixelsY; 1950 } 1951 1952 /** 1953 * Use canvas max bitmap width and height instead of the default 2048, to avoid redundant tiling. 1954 */ getMaxBitmapDimensions(Canvas canvas)1955 private Point getMaxBitmapDimensions(Canvas canvas) { 1956 return new Point(Math.min(canvas.getMaximumBitmapWidth(), maxTileWidth), Math.min(canvas.getMaximumBitmapHeight(), maxTileHeight)); 1957 } 1958 1959 /** 1960 * Get source width taking rotation into account. 1961 */ 1962 @SuppressWarnings("SuspiciousNameCombination") sWidth()1963 private int sWidth() { 1964 int rotation = getRequiredRotation(); 1965 if (rotation == 90 || rotation == 270) { 1966 return sHeight; 1967 } else { 1968 return sWidth; 1969 } 1970 } 1971 1972 /** 1973 * Get source height taking rotation into account. 1974 */ 1975 @SuppressWarnings("SuspiciousNameCombination") sHeight()1976 private int sHeight() { 1977 int rotation = getRequiredRotation(); 1978 if (rotation == 90 || rotation == 270) { 1979 return sWidth; 1980 } else { 1981 return sHeight; 1982 } 1983 } 1984 1985 /** 1986 * Converts source rectangle from tile, which treats the image file as if it were in the correct orientation already, 1987 * to the rectangle of the image that needs to be loaded. 1988 */ 1989 @SuppressWarnings("SuspiciousNameCombination") 1990 @AnyThread fileSRect(Rect sRect, Rect target)1991 private void fileSRect(Rect sRect, Rect target) { 1992 if (getRequiredRotation() == 0) { 1993 target.set(sRect); 1994 } else if (getRequiredRotation() == 90) { 1995 target.set(sRect.top, sHeight - sRect.right, sRect.bottom, sHeight - sRect.left); 1996 } else if (getRequiredRotation() == 180) { 1997 target.set(sWidth - sRect.right, sHeight - sRect.bottom, sWidth - sRect.left, sHeight - sRect.top); 1998 } else { 1999 target.set(sWidth - sRect.bottom, sRect.left, sWidth - sRect.top, sRect.right); 2000 } 2001 } 2002 2003 /** 2004 * Determines the rotation to be applied to tiles, based on EXIF orientation or chosen setting. 2005 */ 2006 @AnyThread getRequiredRotation()2007 private int getRequiredRotation() { 2008 if (orientation == ORIENTATION_USE_EXIF) { 2009 return sOrientation; 2010 } else { 2011 return orientation; 2012 } 2013 } 2014 2015 /** 2016 * Pythagoras distance between two points. 2017 */ distance(float x0, float x1, float y0, float y1)2018 private float distance(float x0, float x1, float y0, float y1) { 2019 float x = x0 - x1; 2020 float y = y0 - y1; 2021 return (float) Math.sqrt(x * x + y * y); 2022 } 2023 2024 /** 2025 * Releases all resources the view is using and resets the state, nulling any fields that use significant memory. 2026 * After you have called this method, the view can be re-used by setting a new image. Settings are remembered 2027 * but state (scale and center) is forgotten. You can restore these yourself if required. 2028 */ recycle()2029 public void recycle() { 2030 reset(true); 2031 bitmapPaint = null; 2032 debugTextPaint = null; 2033 debugLinePaint = null; 2034 tileBgPaint = null; 2035 } 2036 2037 /** 2038 * Convert screen to source x coordinate. 2039 */ viewToSourceX(float vx)2040 private float viewToSourceX(float vx) { 2041 if (vTranslate == null) { return Float.NaN; } 2042 return (vx - vTranslate.x)/scale; 2043 } 2044 2045 /** 2046 * Convert screen to source y coordinate. 2047 */ viewToSourceY(float vy)2048 private float viewToSourceY(float vy) { 2049 if (vTranslate == null) { return Float.NaN; } 2050 return (vy - vTranslate.y)/scale; 2051 } 2052 2053 /** 2054 * Converts a rectangle within the view to the corresponding rectangle from the source file, taking 2055 * into account the current scale, translation, orientation and clipped region. This can be used 2056 * to decode a bitmap from the source file. 2057 * 2058 * This method will only work when the image has fully initialised, after {@link #isReady()} returns 2059 * true. It is not guaranteed to work with preloaded bitmaps. 2060 * 2061 * The result is written to the fRect argument. Re-use a single instance for efficiency. 2062 * @param vRect rectangle representing the view area to interpret. 2063 * @param fRect rectangle instance to which the result will be written. Re-use for efficiency. 2064 */ viewToFileRect(Rect vRect, Rect fRect)2065 public void viewToFileRect(Rect vRect, Rect fRect) { 2066 if (vTranslate == null || !readySent) { 2067 return; 2068 } 2069 fRect.set( 2070 (int)viewToSourceX(vRect.left), 2071 (int)viewToSourceY(vRect.top), 2072 (int)viewToSourceX(vRect.right), 2073 (int)viewToSourceY(vRect.bottom)); 2074 fileSRect(fRect, fRect); 2075 fRect.set( 2076 Math.max(0, fRect.left), 2077 Math.max(0, fRect.top), 2078 Math.min(sWidth, fRect.right), 2079 Math.min(sHeight, fRect.bottom) 2080 ); 2081 if (sRegion != null) { 2082 fRect.offset(sRegion.left, sRegion.top); 2083 } 2084 } 2085 2086 /** 2087 * Find the area of the source file that is currently visible on screen, taking into account the 2088 * current scale, translation, orientation and clipped region. This is a convenience method; see 2089 * {@link #viewToFileRect(Rect, Rect)}. 2090 * @param fRect rectangle instance to which the result will be written. Re-use for efficiency. 2091 */ visibleFileRect(Rect fRect)2092 public void visibleFileRect(Rect fRect) { 2093 if (vTranslate == null || !readySent) { 2094 return; 2095 } 2096 fRect.set(0, 0, getWidth(), getHeight()); 2097 viewToFileRect(fRect, fRect); 2098 } 2099 2100 /** 2101 * Convert screen coordinate to source coordinate. 2102 * @param vxy view X/Y coordinate. 2103 * @return a coordinate representing the corresponding source coordinate. 2104 */ viewToSourceCoord(PointF vxy)2105 public final PointF viewToSourceCoord(PointF vxy) { 2106 return viewToSourceCoord(vxy.x, vxy.y, new PointF()); 2107 } 2108 2109 /** 2110 * Convert screen coordinate to source coordinate. 2111 * @param vx view X coordinate. 2112 * @param vy view Y coordinate. 2113 * @return a coordinate representing the corresponding source coordinate. 2114 */ viewToSourceCoord(float vx, float vy)2115 public final PointF viewToSourceCoord(float vx, float vy) { 2116 return viewToSourceCoord(vx, vy, new PointF()); 2117 } 2118 2119 /** 2120 * Convert screen coordinate to source coordinate. 2121 * @param vxy view coordinates to convert. 2122 * @param sTarget target object for result. The same instance is also returned. 2123 * @return source coordinates. This is the same instance passed to the sTarget param. 2124 */ viewToSourceCoord(PointF vxy, PointF sTarget)2125 public final PointF viewToSourceCoord(PointF vxy, PointF sTarget) { 2126 return viewToSourceCoord(vxy.x, vxy.y, sTarget); 2127 } 2128 2129 /** 2130 * Convert screen coordinate to source coordinate. 2131 * @param vx view X coordinate. 2132 * @param vy view Y coordinate. 2133 * @param sTarget target object for result. The same instance is also returned. 2134 * @return source coordinates. This is the same instance passed to the sTarget param. 2135 */ viewToSourceCoord(float vx, float vy, PointF sTarget)2136 public final PointF viewToSourceCoord(float vx, float vy, PointF sTarget) { 2137 if (vTranslate == null) { 2138 return null; 2139 } 2140 sTarget.set(viewToSourceX(vx), viewToSourceY(vy)); 2141 return sTarget; 2142 } 2143 2144 /** 2145 * Convert source to view x coordinate. 2146 */ sourceToViewX(float sx)2147 private float sourceToViewX(float sx) { 2148 if (vTranslate == null) { return Float.NaN; } 2149 return (sx * scale) + vTranslate.x; 2150 } 2151 2152 /** 2153 * Convert source to view y coordinate. 2154 */ sourceToViewY(float sy)2155 private float sourceToViewY(float sy) { 2156 if (vTranslate == null) { return Float.NaN; } 2157 return (sy * scale) + vTranslate.y; 2158 } 2159 2160 /** 2161 * Convert source coordinate to view coordinate. 2162 * @param sxy source coordinates to convert. 2163 * @return view coordinates. 2164 */ sourceToViewCoord(PointF sxy)2165 public final PointF sourceToViewCoord(PointF sxy) { 2166 return sourceToViewCoord(sxy.x, sxy.y, new PointF()); 2167 } 2168 2169 /** 2170 * Convert source coordinate to view coordinate. 2171 * @param sx source X coordinate. 2172 * @param sy source Y coordinate. 2173 * @return view coordinates. 2174 */ sourceToViewCoord(float sx, float sy)2175 public final PointF sourceToViewCoord(float sx, float sy) { 2176 return sourceToViewCoord(sx, sy, new PointF()); 2177 } 2178 2179 /** 2180 * Convert source coordinate to view coordinate. 2181 * @param sxy source coordinates to convert. 2182 * @param vTarget target object for result. The same instance is also returned. 2183 * @return view coordinates. This is the same instance passed to the vTarget param. 2184 */ 2185 @SuppressWarnings("UnusedReturnValue") sourceToViewCoord(PointF sxy, PointF vTarget)2186 public final PointF sourceToViewCoord(PointF sxy, PointF vTarget) { 2187 return sourceToViewCoord(sxy.x, sxy.y, vTarget); 2188 } 2189 2190 /** 2191 * Convert source coordinate to view coordinate. 2192 * @param sx source X coordinate. 2193 * @param sy source Y coordinate. 2194 * @param vTarget target object for result. The same instance is also returned. 2195 * @return view coordinates. This is the same instance passed to the vTarget param. 2196 */ sourceToViewCoord(float sx, float sy, PointF vTarget)2197 public final PointF sourceToViewCoord(float sx, float sy, PointF vTarget) { 2198 if (vTranslate == null) { 2199 return null; 2200 } 2201 vTarget.set(sourceToViewX(sx), sourceToViewY(sy)); 2202 return vTarget; 2203 } 2204 2205 /** 2206 * Convert source rect to screen rect, integer values. 2207 */ sourceToViewRect(Rect sRect, Rect vTarget)2208 private void sourceToViewRect(Rect sRect, Rect vTarget) { 2209 vTarget.set( 2210 (int)sourceToViewX(sRect.left), 2211 (int)sourceToViewY(sRect.top), 2212 (int)sourceToViewX(sRect.right), 2213 (int)sourceToViewY(sRect.bottom) 2214 ); 2215 } 2216 2217 /** 2218 * Get the translation required to place a given source coordinate at the center of the screen, with the center 2219 * adjusted for asymmetric padding. Accepts the desired scale as an argument, so this is independent of current 2220 * translate and scale. The result is fitted to bounds, putting the image point as near to the screen center as permitted. 2221 */ vTranslateForSCenter(float sCenterX, float sCenterY, float scale)2222 private PointF vTranslateForSCenter(float sCenterX, float sCenterY, float scale) { 2223 int vxCenter = getPaddingLeft() + (getWidth() - getPaddingRight() - getPaddingLeft())/2; 2224 int vyCenter = getPaddingTop() + (getHeight() - getPaddingBottom() - getPaddingTop())/2; 2225 if (satTemp == null) { 2226 satTemp = new ScaleAndTranslate(0, new PointF(0, 0)); 2227 } 2228 satTemp.scale = scale; 2229 satTemp.vTranslate.set(vxCenter - (sCenterX * scale), vyCenter - (sCenterY * scale)); 2230 fitToBounds(true, satTemp); 2231 return satTemp.vTranslate; 2232 } 2233 2234 /** 2235 * Given a requested source center and scale, calculate what the actual center will have to be to keep the image in 2236 * pan limits, keeping the requested center as near to the middle of the screen as allowed. 2237 */ limitedSCenter(float sCenterX, float sCenterY, float scale, PointF sTarget)2238 private PointF limitedSCenter(float sCenterX, float sCenterY, float scale, PointF sTarget) { 2239 PointF vTranslate = vTranslateForSCenter(sCenterX, sCenterY, scale); 2240 int vxCenter = getPaddingLeft() + (getWidth() - getPaddingRight() - getPaddingLeft())/2; 2241 int vyCenter = getPaddingTop() + (getHeight() - getPaddingBottom() - getPaddingTop())/2; 2242 float sx = (vxCenter - vTranslate.x)/scale; 2243 float sy = (vyCenter - vTranslate.y)/scale; 2244 sTarget.set(sx, sy); 2245 return sTarget; 2246 } 2247 2248 /** 2249 * Returns the minimum allowed scale. 2250 */ minScale()2251 private float minScale() { 2252 int vPadding = getPaddingBottom() + getPaddingTop(); 2253 int hPadding = getPaddingLeft() + getPaddingRight(); 2254 if (minimumScaleType == SCALE_TYPE_CENTER_CROP || minimumScaleType == SCALE_TYPE_START) { 2255 return Math.max((getWidth() - hPadding) / (float) sWidth(), (getHeight() - vPadding) / (float) sHeight()); 2256 } else if (minimumScaleType == SCALE_TYPE_CUSTOM && minScale > 0) { 2257 return minScale; 2258 } else { 2259 return Math.min((getWidth() - hPadding) / (float) sWidth(), (getHeight() - vPadding) / (float) sHeight()); 2260 } 2261 } 2262 2263 /** 2264 * Adjust a requested scale to be within the allowed limits. 2265 */ limitedScale(float targetScale)2266 private float limitedScale(float targetScale) { 2267 targetScale = Math.max(minScale(), targetScale); 2268 targetScale = Math.min(maxScale, targetScale); 2269 return targetScale; 2270 } 2271 2272 /** 2273 * Apply a selected type of easing. 2274 * @param type Easing type, from static fields 2275 * @param time Elapsed time 2276 * @param from Start value 2277 * @param change Target value 2278 * @param duration Anm duration 2279 * @return Current value 2280 */ ease(int type, long time, float from, float change, long duration)2281 private float ease(int type, long time, float from, float change, long duration) { 2282 switch (type) { 2283 case EASE_IN_OUT_QUAD: 2284 return easeInOutQuad(time, from, change, duration); 2285 case EASE_OUT_QUAD: 2286 return easeOutQuad(time, from, change, duration); 2287 default: 2288 throw new IllegalStateException("Unexpected easing type: " + type); 2289 } 2290 } 2291 2292 /** 2293 * Quadratic easing for fling. With thanks to Robert Penner - http://gizma.com/easing/ 2294 * @param time Elapsed time 2295 * @param from Start value 2296 * @param change Target value 2297 * @param duration Anm duration 2298 * @return Current value 2299 */ easeOutQuad(long time, float from, float change, long duration)2300 private float easeOutQuad(long time, float from, float change, long duration) { 2301 float progress = (float)time/(float)duration; 2302 return -change * progress*(progress-2) + from; 2303 } 2304 2305 /** 2306 * Quadratic easing for scale and center animations. With thanks to Robert Penner - http://gizma.com/easing/ 2307 * @param time Elapsed time 2308 * @param from Start value 2309 * @param change Target value 2310 * @param duration Anm duration 2311 * @return Current value 2312 */ easeInOutQuad(long time, float from, float change, long duration)2313 private float easeInOutQuad(long time, float from, float change, long duration) { 2314 float timeF = time/(duration/2f); 2315 if (timeF < 1) { 2316 return (change/2f * timeF * timeF) + from; 2317 } else { 2318 timeF--; 2319 return (-change/2f) * (timeF * (timeF - 2) - 1) + from; 2320 } 2321 } 2322 2323 /** 2324 * Debug logger 2325 */ 2326 @AnyThread debug(String message, Object... args)2327 private void debug(String message, Object... args) { 2328 if (debug) { 2329 Log.d(TAG, String.format(message, args)); 2330 } 2331 } 2332 2333 /** 2334 * For debug overlays. Scale pixel value according to screen density. 2335 */ px(int px)2336 private int px(int px) { 2337 return (int)(density * px); 2338 } 2339 2340 /** 2341 * 2342 * Swap the default region decoder implementation for one of your own. You must do this before setting the image file or 2343 * asset, and you cannot use a custom decoder when using layout XML to set an asset name. Your class must have a 2344 * public default constructor. 2345 * @param regionDecoderClass The {@link ImageRegionDecoder} implementation to use. 2346 */ setRegionDecoderClass(Class<? extends ImageRegionDecoder> regionDecoderClass)2347 public final void setRegionDecoderClass(Class<? extends ImageRegionDecoder> regionDecoderClass) { 2348 if (regionDecoderClass == null) { 2349 throw new IllegalArgumentException("Decoder class cannot be set to null"); 2350 } 2351 this.regionDecoderFactory = new CompatDecoderFactory<>(regionDecoderClass); 2352 } 2353 2354 /** 2355 * Swap the default region decoder implementation for one of your own. You must do this before setting the image file or 2356 * asset, and you cannot use a custom decoder when using layout XML to set an asset name. 2357 * @param regionDecoderFactory The {@link DecoderFactory} implementation that produces {@link ImageRegionDecoder} 2358 * instances. 2359 */ setRegionDecoderFactory(DecoderFactory<? extends ImageRegionDecoder> regionDecoderFactory)2360 public final void setRegionDecoderFactory(DecoderFactory<? extends ImageRegionDecoder> regionDecoderFactory) { 2361 if (regionDecoderFactory == null) { 2362 throw new IllegalArgumentException("Decoder factory cannot be set to null"); 2363 } 2364 this.regionDecoderFactory = regionDecoderFactory; 2365 } 2366 2367 /** 2368 * Swap the default bitmap decoder implementation for one of your own. You must do this before setting the image file or 2369 * asset, and you cannot use a custom decoder when using layout XML to set an asset name. Your class must have a 2370 * public default constructor. 2371 * @param bitmapDecoderClass The {@link ImageDecoder} implementation to use. 2372 */ setBitmapDecoderClass(Class<? extends ImageDecoder> bitmapDecoderClass)2373 public final void setBitmapDecoderClass(Class<? extends ImageDecoder> bitmapDecoderClass) { 2374 if (bitmapDecoderClass == null) { 2375 throw new IllegalArgumentException("Decoder class cannot be set to null"); 2376 } 2377 this.bitmapDecoderFactory = new CompatDecoderFactory<>(bitmapDecoderClass); 2378 } 2379 2380 /** 2381 * Swap the default bitmap decoder implementation for one of your own. You must do this before setting the image file or 2382 * asset, and you cannot use a custom decoder when using layout XML to set an asset name. 2383 * @param bitmapDecoderFactory The {@link DecoderFactory} implementation that produces {@link ImageDecoder} instances. 2384 */ setBitmapDecoderFactory(DecoderFactory<? extends ImageDecoder> bitmapDecoderFactory)2385 public final void setBitmapDecoderFactory(DecoderFactory<? extends ImageDecoder> bitmapDecoderFactory) { 2386 if (bitmapDecoderFactory == null) { 2387 throw new IllegalArgumentException("Decoder factory cannot be set to null"); 2388 } 2389 this.bitmapDecoderFactory = bitmapDecoderFactory; 2390 } 2391 2392 /** 2393 * Calculate how much further the image can be panned in each direction. The results are set on 2394 * the supplied {@link RectF} and expressed as screen pixels. For example, if the image cannot be 2395 * panned any further towards the left, the value of {@link RectF#left} will be set to 0. 2396 * @param vTarget target object for results. Re-use for efficiency. 2397 */ getPanRemaining(RectF vTarget)2398 public final void getPanRemaining(RectF vTarget) { 2399 if (!isReady()) { 2400 return; 2401 } 2402 2403 float scaleWidth = scale * sWidth(); 2404 float scaleHeight = scale * sHeight(); 2405 2406 if (panLimit == PAN_LIMIT_CENTER) { 2407 vTarget.top = Math.max(0, -(vTranslate.y - (getHeight() / 2))); 2408 vTarget.left = Math.max(0, -(vTranslate.x - (getWidth() / 2))); 2409 vTarget.bottom = Math.max(0, vTranslate.y - ((getHeight() / 2) - scaleHeight)); 2410 vTarget.right = Math.max(0, vTranslate.x - ((getWidth() / 2) - scaleWidth)); 2411 } else if (panLimit == PAN_LIMIT_OUTSIDE) { 2412 vTarget.top = Math.max(0, -(vTranslate.y - getHeight())); 2413 vTarget.left = Math.max(0, -(vTranslate.x - getWidth())); 2414 vTarget.bottom = Math.max(0, vTranslate.y + scaleHeight); 2415 vTarget.right = Math.max(0, vTranslate.x + scaleWidth); 2416 } else { 2417 vTarget.top = Math.max(0, -vTranslate.y); 2418 vTarget.left = Math.max(0, -vTranslate.x); 2419 vTarget.bottom = Math.max(0, (scaleHeight + vTranslate.y) - getHeight()); 2420 vTarget.right = Math.max(0, (scaleWidth + vTranslate.x) - getWidth()); 2421 } 2422 } 2423 2424 /** 2425 * Set the pan limiting style. See static fields. Normally {@link #PAN_LIMIT_INSIDE} is best, for image galleries. 2426 * @param panLimit a pan limit constant. See static fields. 2427 */ setPanLimit(int panLimit)2428 public final void setPanLimit(int panLimit) { 2429 if (!VALID_PAN_LIMITS.contains(panLimit)) { 2430 throw new IllegalArgumentException("Invalid pan limit: " + panLimit); 2431 } 2432 this.panLimit = panLimit; 2433 if (isReady()) { 2434 fitToBounds(true); 2435 invalidate(); 2436 } 2437 } 2438 2439 /** 2440 * Set the minimum scale type. See static fields. Normally {@link #SCALE_TYPE_CENTER_INSIDE} is best, for image galleries. 2441 * @param scaleType a scale type constant. See static fields. 2442 */ setMinimumScaleType(int scaleType)2443 public final void setMinimumScaleType(int scaleType) { 2444 if (!VALID_SCALE_TYPES.contains(scaleType)) { 2445 throw new IllegalArgumentException("Invalid scale type: " + scaleType); 2446 } 2447 this.minimumScaleType = scaleType; 2448 if (isReady()) { 2449 fitToBounds(true); 2450 invalidate(); 2451 } 2452 } 2453 2454 /** 2455 * Set the maximum scale allowed. A value of 1 means 1:1 pixels at maximum scale. You may wish to set this according 2456 * to screen density - on a retina screen, 1:1 may still be too small. Consider using {@link #setMinimumDpi(int)}, 2457 * which is density aware. 2458 * @param maxScale maximum scale expressed as a source/view pixels ratio. 2459 */ setMaxScale(float maxScale)2460 public final void setMaxScale(float maxScale) { 2461 this.maxScale = maxScale; 2462 } 2463 2464 /** 2465 * Set the minimum scale allowed. A value of 1 means 1:1 pixels at minimum scale. You may wish to set this according 2466 * to screen density. Consider using {@link #setMaximumDpi(int)}, which is density aware. 2467 * @param minScale minimum scale expressed as a source/view pixels ratio. 2468 */ setMinScale(float minScale)2469 public final void setMinScale(float minScale) { 2470 this.minScale = minScale; 2471 } 2472 2473 /** 2474 * This is a screen density aware alternative to {@link #setMaxScale(float)}; it allows you to express the maximum 2475 * allowed scale in terms of the minimum pixel density. This avoids the problem of 1:1 scale still being 2476 * too small on a high density screen. A sensible starting point is 160 - the default used by this view. 2477 * @param dpi Source image pixel density at maximum zoom. 2478 */ setMinimumDpi(int dpi)2479 public final void setMinimumDpi(int dpi) { 2480 DisplayMetrics metrics = getResources().getDisplayMetrics(); 2481 float averageDpi = (metrics.xdpi + metrics.ydpi)/2; 2482 setMaxScale(averageDpi/dpi); 2483 } 2484 2485 /** 2486 * This is a screen density aware alternative to {@link #setMinScale(float)}; it allows you to express the minimum 2487 * allowed scale in terms of the maximum pixel density. 2488 * @param dpi Source image pixel density at minimum zoom. 2489 */ setMaximumDpi(int dpi)2490 public final void setMaximumDpi(int dpi) { 2491 DisplayMetrics metrics = getResources().getDisplayMetrics(); 2492 float averageDpi = (metrics.xdpi + metrics.ydpi)/2; 2493 setMinScale(averageDpi / dpi); 2494 } 2495 2496 /** 2497 * Returns the maximum allowed scale. 2498 * @return the maximum scale as a source/view pixels ratio. 2499 */ getMaxScale()2500 public float getMaxScale() { 2501 return maxScale; 2502 } 2503 2504 /** 2505 * Returns the minimum allowed scale. 2506 * @return the minimum scale as a source/view pixels ratio. 2507 */ getMinScale()2508 public final float getMinScale() { 2509 return minScale(); 2510 } 2511 2512 /** 2513 * By default, image tiles are at least as high resolution as the screen. For a retina screen this may not be 2514 * necessary, and may increase the likelihood of an OutOfMemoryError. This method sets a DPI at which higher 2515 * resolution tiles should be loaded. Using a lower number will on average use less memory but result in a lower 2516 * quality image. 160-240dpi will usually be enough. This should be called before setting the image source, 2517 * because it affects which tiles get loaded. When using an untiled source image this method has no effect. 2518 * @param minimumTileDpi Tile loading threshold. 2519 */ setMinimumTileDpi(int minimumTileDpi)2520 public void setMinimumTileDpi(int minimumTileDpi) { 2521 DisplayMetrics metrics = getResources().getDisplayMetrics(); 2522 float averageDpi = (metrics.xdpi + metrics.ydpi)/2; 2523 this.minimumTileDpi = (int)Math.min(averageDpi, minimumTileDpi); 2524 if (isReady()) { 2525 reset(false); 2526 invalidate(); 2527 } 2528 } 2529 2530 /** 2531 * Returns the source point at the center of the view. 2532 * @return the source coordinates current at the center of the view. 2533 */ getCenter()2534 public final PointF getCenter() { 2535 int mX = getWidth()/2; 2536 int mY = getHeight()/2; 2537 return viewToSourceCoord(mX, mY); 2538 } 2539 2540 /** 2541 * Returns the current scale value. 2542 * @return the current scale as a source/view pixels ratio. 2543 */ getScale()2544 public final float getScale() { 2545 return scale; 2546 } 2547 2548 /** 2549 * Externally change the scale and translation of the source image. This may be used with getCenter() and getScale() 2550 * to restore the scale and zoom after a screen rotate. 2551 * @param scale New scale to set. 2552 * @param sCenter New source image coordinate to center on the screen, subject to boundaries. 2553 */ setScaleAndCenter(float scale, PointF sCenter)2554 public final void setScaleAndCenter(float scale, PointF sCenter) { 2555 this.anim = null; 2556 this.pendingScale = scale; 2557 this.sPendingCenter = sCenter; 2558 this.sRequestedCenter = sCenter; 2559 invalidate(); 2560 } 2561 2562 /** 2563 * Fully zoom out and return the image to the middle of the screen. This might be useful if you have a view pager 2564 * and want images to be reset when the user has moved to another page. 2565 */ resetScaleAndCenter()2566 public final void resetScaleAndCenter() { 2567 this.anim = null; 2568 this.pendingScale = limitedScale(0); 2569 if (isReady()) { 2570 this.sPendingCenter = new PointF(sWidth()/2, sHeight()/2); 2571 } else { 2572 this.sPendingCenter = new PointF(0, 0); 2573 } 2574 invalidate(); 2575 } 2576 2577 /** 2578 * Call to find whether the view is initialised, has dimensions, and will display an image on 2579 * the next draw. If a preview has been provided, it may be the preview that will be displayed 2580 * and the full size image may still be loading. If no preview was provided, this is called once 2581 * the base layer tiles of the full size image are loaded. 2582 * @return true if the view is ready to display an image and accept touch gestures. 2583 */ isReady()2584 public final boolean isReady() { 2585 return readySent; 2586 } 2587 2588 /** 2589 * Called once when the view is initialised, has dimensions, and will display an image on the 2590 * next draw. This is triggered at the same time as {@link OnImageEventListener#onReady()} but 2591 * allows a subclass to receive this event without using a listener. 2592 */ 2593 @SuppressWarnings("EmptyMethod") onReady()2594 protected void onReady() { 2595 2596 } 2597 2598 /** 2599 * Call to find whether the main image (base layer tiles where relevant) have been loaded. Before 2600 * this event the view is blank unless a preview was provided. 2601 * @return true if the main image (not the preview) has been loaded and is ready to display. 2602 */ isImageLoaded()2603 public final boolean isImageLoaded() { 2604 return imageLoadedSent; 2605 } 2606 2607 /** 2608 * Called once when the full size image or its base layer tiles have been loaded. 2609 */ 2610 @SuppressWarnings("EmptyMethod") onImageLoaded()2611 protected void onImageLoaded() { 2612 2613 } 2614 2615 /** 2616 * Get source width, ignoring orientation. If {@link #getOrientation()} returns 90 or 270, you can use {@link #getSHeight()} 2617 * for the apparent width. 2618 * @return the source image width in pixels. 2619 */ getSWidth()2620 public final int getSWidth() { 2621 return sWidth; 2622 } 2623 2624 /** 2625 * Get source height, ignoring orientation. If {@link #getOrientation()} returns 90 or 270, you can use {@link #getSWidth()} 2626 * for the apparent height. 2627 * @return the source image height in pixels. 2628 */ getSHeight()2629 public final int getSHeight() { 2630 return sHeight; 2631 } 2632 2633 /** 2634 * Returns the orientation setting. This can return {@link #ORIENTATION_USE_EXIF}, in which case it doesn't tell you 2635 * the applied orientation of the image. For that, use {@link #getAppliedOrientation()}. 2636 * @return the orientation setting. See static fields. 2637 */ getOrientation()2638 public final int getOrientation() { 2639 return orientation; 2640 } 2641 2642 /** 2643 * Returns the actual orientation of the image relative to the source file. This will be based on the source file's 2644 * EXIF orientation if you're using ORIENTATION_USE_EXIF. Values are 0, 90, 180, 270. 2645 * @return the orientation applied after EXIF information has been extracted. See static fields. 2646 */ getAppliedOrientation()2647 public final int getAppliedOrientation() { 2648 return getRequiredRotation(); 2649 } 2650 2651 /** 2652 * Get the current state of the view (scale, center, orientation) for restoration after rotate. Will return null if 2653 * the view is not ready. 2654 * @return an {@link ImageViewState} instance representing the current position of the image. null if the view isn't ready. 2655 */ getState()2656 public final ImageViewState getState() { 2657 if (vTranslate != null && sWidth > 0 && sHeight > 0) { 2658 return new ImageViewState(getScale(), getCenter(), getOrientation()); 2659 } 2660 return null; 2661 } 2662 2663 /** 2664 * Returns true if zoom gesture detection is enabled. 2665 * @return true if zoom gesture detection is enabled. 2666 */ isZoomEnabled()2667 public final boolean isZoomEnabled() { 2668 return zoomEnabled; 2669 } 2670 2671 /** 2672 * Enable or disable zoom gesture detection. Disabling zoom locks the the current scale. 2673 * @param zoomEnabled true to enable zoom gestures, false to disable. 2674 */ setZoomEnabled(boolean zoomEnabled)2675 public final void setZoomEnabled(boolean zoomEnabled) { 2676 this.zoomEnabled = zoomEnabled; 2677 } 2678 2679 /** 2680 * Returns true if double tap & swipe to zoom is enabled. 2681 * @return true if double tap & swipe to zoom is enabled. 2682 */ isQuickScaleEnabled()2683 public final boolean isQuickScaleEnabled() { 2684 return quickScaleEnabled; 2685 } 2686 2687 /** 2688 * Enable or disable double tap & swipe to zoom. 2689 * @param quickScaleEnabled true to enable quick scale, false to disable. 2690 */ setQuickScaleEnabled(boolean quickScaleEnabled)2691 public final void setQuickScaleEnabled(boolean quickScaleEnabled) { 2692 this.quickScaleEnabled = quickScaleEnabled; 2693 } 2694 2695 /** 2696 * Returns true if pan gesture detection is enabled. 2697 * @return true if pan gesture detection is enabled. 2698 */ isPanEnabled()2699 public final boolean isPanEnabled() { 2700 return panEnabled; 2701 } 2702 2703 /** 2704 * Enable or disable pan gesture detection. Disabling pan causes the image to be centered. Pan 2705 * can still be changed from code. 2706 * @param panEnabled true to enable panning, false to disable. 2707 */ setPanEnabled(boolean panEnabled)2708 public final void setPanEnabled(boolean panEnabled) { 2709 this.panEnabled = panEnabled; 2710 if (!panEnabled && vTranslate != null) { 2711 vTranslate.x = (getWidth()/2) - (scale * (sWidth()/2)); 2712 vTranslate.y = (getHeight()/2) - (scale * (sHeight()/2)); 2713 if (isReady()) { 2714 refreshRequiredTiles(true); 2715 invalidate(); 2716 } 2717 } 2718 } 2719 2720 /** 2721 * Set a solid color to render behind tiles, useful for displaying transparent PNGs. 2722 * @param tileBgColor Background color for tiles. 2723 */ setTileBackgroundColor(int tileBgColor)2724 public final void setTileBackgroundColor(int tileBgColor) { 2725 if (Color.alpha(tileBgColor) == 0) { 2726 tileBgPaint = null; 2727 } else { 2728 tileBgPaint = new Paint(); 2729 tileBgPaint.setStyle(Style.FILL); 2730 tileBgPaint.setColor(tileBgColor); 2731 } 2732 invalidate(); 2733 } 2734 2735 /** 2736 * Set the scale the image will zoom in to when double tapped. This also the scale point where a double tap is interpreted 2737 * as a zoom out gesture - if the scale is greater than 90% of this value, a double tap zooms out. Avoid using values 2738 * greater than the max zoom. 2739 * @param doubleTapZoomScale New value for double tap gesture zoom scale. 2740 */ setDoubleTapZoomScale(float doubleTapZoomScale)2741 public final void setDoubleTapZoomScale(float doubleTapZoomScale) { 2742 this.doubleTapZoomScale = doubleTapZoomScale; 2743 } 2744 2745 /** 2746 * A density aware alternative to {@link #setDoubleTapZoomScale(float)}; this allows you to express the scale the 2747 * image will zoom in to when double tapped in terms of the image pixel density. Values lower than the max scale will 2748 * be ignored. A sensible starting point is 160 - the default used by this view. 2749 * @param dpi New value for double tap gesture zoom scale. 2750 */ setDoubleTapZoomDpi(int dpi)2751 public final void setDoubleTapZoomDpi(int dpi) { 2752 DisplayMetrics metrics = getResources().getDisplayMetrics(); 2753 float averageDpi = (metrics.xdpi + metrics.ydpi)/2; 2754 setDoubleTapZoomScale(averageDpi/dpi); 2755 } 2756 2757 /** 2758 * Set the type of zoom animation to be used for double taps. See static fields. 2759 * @param doubleTapZoomStyle New value for zoom style. 2760 */ setDoubleTapZoomStyle(int doubleTapZoomStyle)2761 public final void setDoubleTapZoomStyle(int doubleTapZoomStyle) { 2762 if (!VALID_ZOOM_STYLES.contains(doubleTapZoomStyle)) { 2763 throw new IllegalArgumentException("Invalid zoom style: " + doubleTapZoomStyle); 2764 } 2765 this.doubleTapZoomStyle = doubleTapZoomStyle; 2766 } 2767 2768 /** 2769 * Set the duration of the double tap zoom animation. 2770 * @param durationMs Duration in milliseconds. 2771 */ setDoubleTapZoomDuration(int durationMs)2772 public final void setDoubleTapZoomDuration(int durationMs) { 2773 this.doubleTapZoomDuration = Math.max(0, durationMs); 2774 } 2775 2776 /** 2777 * <p> 2778 * Provide an {@link Executor} to be used for loading images. By default, {@link AsyncTask#THREAD_POOL_EXECUTOR} 2779 * is used to minimise contention with other background work the app is doing. You can also choose 2780 * to use {@link AsyncTask#SERIAL_EXECUTOR} if you want to limit concurrent background tasks. 2781 * Alternatively you can supply an {@link Executor} of your own to avoid any contention. It is 2782 * strongly recommended to use a single executor instance for the life of your application, not 2783 * one per view instance. 2784 * </p><p> 2785 * <b>Warning:</b> If you are using a custom implementation of {@link ImageRegionDecoder}, and you 2786 * supply an executor with more than one thread, you must make sure your implementation supports 2787 * multi-threaded bitmap decoding or has appropriate internal synchronization. From SDK 21, Android's 2788 * {@link android.graphics.BitmapRegionDecoder} uses an internal lock so it is thread safe but 2789 * there is no advantage to using multiple threads. 2790 * </p> 2791 * @param executor an {@link Executor} for image loading. 2792 */ setExecutor(Executor executor)2793 public void setExecutor(Executor executor) { 2794 if (executor == null) { 2795 throw new NullPointerException("Executor must not be null"); 2796 } 2797 this.executor = executor; 2798 } 2799 2800 /** 2801 * Enable or disable eager loading of tiles that appear on screen during gestures or animations, 2802 * while the gesture or animation is still in progress. By default this is enabled to improve 2803 * responsiveness, but it can result in tiles being loaded and discarded more rapidly than 2804 * necessary and reduce the animation frame rate on old/cheap devices. Disable this on older 2805 * devices if you see poor performance. Tiles will then be loaded only when gestures and animations 2806 * are completed. 2807 * @param eagerLoadingEnabled true to enable loading during gestures, false to delay loading until gestures end 2808 */ setEagerLoadingEnabled(boolean eagerLoadingEnabled)2809 public void setEagerLoadingEnabled(boolean eagerLoadingEnabled) { 2810 this.eagerLoadingEnabled = eagerLoadingEnabled; 2811 } 2812 2813 /** 2814 * Enables visual debugging, showing tile boundaries and sizes. 2815 * @param debug true to enable debugging, false to disable. 2816 */ setDebug(boolean debug)2817 public final void setDebug(boolean debug) { 2818 this.debug = debug; 2819 } 2820 2821 /** 2822 * Check if an image has been set. The image may not have been loaded and displayed yet. 2823 * @return If an image is currently set. 2824 */ hasImage()2825 public boolean hasImage() { 2826 return uri != null || bitmap != null; 2827 } 2828 2829 /** 2830 * {@inheritDoc} 2831 */ 2832 @Override setOnLongClickListener(OnLongClickListener onLongClickListener)2833 public void setOnLongClickListener(OnLongClickListener onLongClickListener) { 2834 this.onLongClickListener = onLongClickListener; 2835 } 2836 2837 /** 2838 * Add a listener allowing notification of load and error events. Extend {@link DefaultOnImageEventListener} 2839 * to simplify implementation. 2840 * @param onImageEventListener an {@link OnImageEventListener} instance. 2841 */ setOnImageEventListener(OnImageEventListener onImageEventListener)2842 public void setOnImageEventListener(OnImageEventListener onImageEventListener) { 2843 this.onImageEventListener = onImageEventListener; 2844 } 2845 2846 /** 2847 * Add a listener for pan and zoom events. Extend {@link DefaultOnStateChangedListener} to simplify 2848 * implementation. 2849 * @param onStateChangedListener an {@link OnStateChangedListener} instance. 2850 */ setOnStateChangedListener(OnStateChangedListener onStateChangedListener)2851 public void setOnStateChangedListener(OnStateChangedListener onStateChangedListener) { 2852 this.onStateChangedListener = onStateChangedListener; 2853 } 2854 sendStateChanged(float oldScale, PointF oldVTranslate, int origin)2855 private void sendStateChanged(float oldScale, PointF oldVTranslate, int origin) { 2856 if (onStateChangedListener != null && scale != oldScale) { 2857 onStateChangedListener.onScaleChanged(scale, origin); 2858 } 2859 if (onStateChangedListener != null && !vTranslate.equals(oldVTranslate)) { 2860 onStateChangedListener.onCenterChanged(getCenter(), origin); 2861 } 2862 } 2863 2864 /** 2865 * Creates a panning animation builder, that when started will animate the image to place the given coordinates of 2866 * the image in the center of the screen. If doing this would move the image beyond the edges of the screen, the 2867 * image is instead animated to move the center point as near to the center of the screen as is allowed - it's 2868 * guaranteed to be on screen. 2869 * @param sCenter Target center point 2870 * @return {@link AnimationBuilder} instance. Call {@link SubsamplingScaleImageView.AnimationBuilder#start()} to start the anim. 2871 */ animateCenter(PointF sCenter)2872 public AnimationBuilder animateCenter(PointF sCenter) { 2873 if (!isReady()) { 2874 return null; 2875 } 2876 return new AnimationBuilder(sCenter); 2877 } 2878 2879 /** 2880 * Creates a scale animation builder, that when started will animate a zoom in or out. If this would move the image 2881 * beyond the panning limits, the image is automatically panned during the animation. 2882 * @param scale Target scale. 2883 * @return {@link AnimationBuilder} instance. Call {@link SubsamplingScaleImageView.AnimationBuilder#start()} to start the anim. 2884 */ animateScale(float scale)2885 public AnimationBuilder animateScale(float scale) { 2886 if (!isReady()) { 2887 return null; 2888 } 2889 return new AnimationBuilder(scale); 2890 } 2891 2892 /** 2893 * Creates a scale animation builder, that when started will animate a zoom in or out. If this would move the image 2894 * beyond the panning limits, the image is automatically panned during the animation. 2895 * @param scale Target scale. 2896 * @param sCenter Target source center. 2897 * @return {@link AnimationBuilder} instance. Call {@link SubsamplingScaleImageView.AnimationBuilder#start()} to start the anim. 2898 */ animateScaleAndCenter(float scale, PointF sCenter)2899 public AnimationBuilder animateScaleAndCenter(float scale, PointF sCenter) { 2900 if (!isReady()) { 2901 return null; 2902 } 2903 return new AnimationBuilder(scale, sCenter); 2904 } 2905 2906 /** 2907 * Builder class used to set additional options for a scale animation. Create an instance using {@link #animateScale(float)}, 2908 * then set your options and call {@link #start()}. 2909 */ 2910 public final class AnimationBuilder { 2911 2912 private final float targetScale; 2913 private final PointF targetSCenter; 2914 private final PointF vFocus; 2915 private long duration = 500; 2916 private int easing = EASE_IN_OUT_QUAD; 2917 private int origin = ORIGIN_ANIM; 2918 private boolean interruptible = true; 2919 private boolean panLimited = true; 2920 private OnAnimationEventListener listener; 2921 AnimationBuilder(PointF sCenter)2922 private AnimationBuilder(PointF sCenter) { 2923 this.targetScale = scale; 2924 this.targetSCenter = sCenter; 2925 this.vFocus = null; 2926 } 2927 AnimationBuilder(float scale)2928 private AnimationBuilder(float scale) { 2929 this.targetScale = scale; 2930 this.targetSCenter = getCenter(); 2931 this.vFocus = null; 2932 } 2933 AnimationBuilder(float scale, PointF sCenter)2934 private AnimationBuilder(float scale, PointF sCenter) { 2935 this.targetScale = scale; 2936 this.targetSCenter = sCenter; 2937 this.vFocus = null; 2938 } 2939 AnimationBuilder(float scale, PointF sCenter, PointF vFocus)2940 private AnimationBuilder(float scale, PointF sCenter, PointF vFocus) { 2941 this.targetScale = scale; 2942 this.targetSCenter = sCenter; 2943 this.vFocus = vFocus; 2944 } 2945 2946 /** 2947 * Desired duration of the anim in milliseconds. Default is 500. 2948 * @param duration duration in milliseconds. 2949 * @return this builder for method chaining. 2950 */ withDuration(long duration)2951 public AnimationBuilder withDuration(long duration) { 2952 this.duration = duration; 2953 return this; 2954 } 2955 2956 /** 2957 * Whether the animation can be interrupted with a touch. Default is true. 2958 * @param interruptible interruptible flag. 2959 * @return this builder for method chaining. 2960 */ withInterruptible(boolean interruptible)2961 public AnimationBuilder withInterruptible(boolean interruptible) { 2962 this.interruptible = interruptible; 2963 return this; 2964 } 2965 2966 /** 2967 * Set the easing style. See static fields. {@link #EASE_IN_OUT_QUAD} is recommended, and the default. 2968 * @param easing easing style. 2969 * @return this builder for method chaining. 2970 */ withEasing(int easing)2971 public AnimationBuilder withEasing(int easing) { 2972 if (!VALID_EASING_STYLES.contains(easing)) { 2973 throw new IllegalArgumentException("Unknown easing type: " + easing); 2974 } 2975 this.easing = easing; 2976 return this; 2977 } 2978 2979 /** 2980 * Add an animation event listener. 2981 * @param listener The listener. 2982 * @return this builder for method chaining. 2983 */ withOnAnimationEventListener(OnAnimationEventListener listener)2984 public AnimationBuilder withOnAnimationEventListener(OnAnimationEventListener listener) { 2985 this.listener = listener; 2986 return this; 2987 } 2988 2989 /** 2990 * Only for internal use. When set to true, the animation proceeds towards the actual end point - the nearest 2991 * point to the center allowed by pan limits. When false, animation is in the direction of the requested end 2992 * point and is stopped when the limit for each axis is reached. The latter behaviour is used for flings but 2993 * nothing else. 2994 */ withPanLimited(boolean panLimited)2995 private AnimationBuilder withPanLimited(boolean panLimited) { 2996 this.panLimited = panLimited; 2997 return this; 2998 } 2999 3000 /** 3001 * Only for internal use. Indicates what caused the animation. 3002 */ withOrigin(int origin)3003 private AnimationBuilder withOrigin(int origin) { 3004 this.origin = origin; 3005 return this; 3006 } 3007 3008 /** 3009 * Starts the animation. 3010 */ start()3011 public void start() { 3012 if (anim != null && anim.listener != null) { 3013 try { 3014 anim.listener.onInterruptedByNewAnim(); 3015 } catch (Exception e) { 3016 Log.w(TAG, "Error thrown by animation listener", e); 3017 } 3018 } 3019 3020 int vxCenter = getPaddingLeft() + (getWidth() - getPaddingRight() - getPaddingLeft())/2; 3021 int vyCenter = getPaddingTop() + (getHeight() - getPaddingBottom() - getPaddingTop())/2; 3022 float targetScale = limitedScale(this.targetScale); 3023 PointF targetSCenter = panLimited ? limitedSCenter(this.targetSCenter.x, this.targetSCenter.y, targetScale, new PointF()) : this.targetSCenter; 3024 anim = new Anim(); 3025 anim.scaleStart = scale; 3026 anim.scaleEnd = targetScale; 3027 anim.time = System.currentTimeMillis(); 3028 anim.sCenterEndRequested = targetSCenter; 3029 anim.sCenterStart = getCenter(); 3030 anim.sCenterEnd = targetSCenter; 3031 anim.vFocusStart = sourceToViewCoord(targetSCenter); 3032 anim.vFocusEnd = new PointF( 3033 vxCenter, 3034 vyCenter 3035 ); 3036 anim.duration = duration; 3037 anim.interruptible = interruptible; 3038 anim.easing = easing; 3039 anim.origin = origin; 3040 anim.time = System.currentTimeMillis(); 3041 anim.listener = listener; 3042 3043 if (vFocus != null) { 3044 // Calculate where translation will be at the end of the anim 3045 float vTranslateXEnd = vFocus.x - (targetScale * anim.sCenterStart.x); 3046 float vTranslateYEnd = vFocus.y - (targetScale * anim.sCenterStart.y); 3047 ScaleAndTranslate satEnd = new ScaleAndTranslate(targetScale, new PointF(vTranslateXEnd, vTranslateYEnd)); 3048 // Fit the end translation into bounds 3049 fitToBounds(true, satEnd); 3050 // Adjust the position of the focus point at end so image will be in bounds 3051 anim.vFocusEnd = new PointF( 3052 vFocus.x + (satEnd.vTranslate.x - vTranslateXEnd), 3053 vFocus.y + (satEnd.vTranslate.y - vTranslateYEnd) 3054 ); 3055 } 3056 3057 invalidate(); 3058 } 3059 3060 } 3061 3062 /** 3063 * An event listener for animations, allows events to be triggered when an animation completes, 3064 * is aborted by another animation starting, or is aborted by a touch event. Note that none of 3065 * these events are triggered if the activity is paused, the image is swapped, or in other cases 3066 * where the view's internal state gets wiped or draw events stop. 3067 */ 3068 @SuppressWarnings("EmptyMethod") 3069 public interface OnAnimationEventListener { 3070 3071 /** 3072 * The animation has completed, having reached its endpoint. 3073 */ 3074 void onComplete(); 3075 3076 /** 3077 * The animation has been aborted before reaching its endpoint because the user touched the screen. 3078 */ 3079 void onInterruptedByUser(); 3080 3081 /** 3082 * The animation has been aborted before reaching its endpoint because a new animation has been started. 3083 */ 3084 void onInterruptedByNewAnim(); 3085 3086 } 3087 3088 /** 3089 * Default implementation of {@link OnAnimationEventListener} for extension. This does nothing in any method. 3090 */ 3091 public static class DefaultOnAnimationEventListener implements OnAnimationEventListener { 3092 onComplete()3093 @Override public void onComplete() { } onInterruptedByUser()3094 @Override public void onInterruptedByUser() { } onInterruptedByNewAnim()3095 @Override public void onInterruptedByNewAnim() { } 3096 3097 } 3098 3099 /** 3100 * An event listener, allowing subclasses and activities to be notified of significant events. 3101 */ 3102 @SuppressWarnings("EmptyMethod") 3103 public interface OnImageEventListener { 3104 3105 /** 3106 * Called when the dimensions of the image and view are known, and either a preview image, 3107 * the full size image, or base layer tiles are loaded. This indicates the scale and translate 3108 * are known and the next draw will display an image. This event can be used to hide a loading 3109 * graphic, or inform a subclass that it is safe to draw overlays. 3110 */ 3111 void onReady(); 3112 3113 /** 3114 * Called when the full size image is ready. When using tiling, this means the lowest resolution 3115 * base layer of tiles are loaded, and when tiling is disabled, the image bitmap is loaded. 3116 * This event could be used as a trigger to enable gestures if you wanted interaction disabled 3117 * while only a preview is displayed, otherwise for most cases {@link #onReady()} is the best 3118 * event to listen to. 3119 */ 3120 void onImageLoaded(); 3121 3122 /** 3123 * Called when a preview image could not be loaded. This method cannot be relied upon; certain 3124 * encoding types of supported image formats can result in corrupt or blank images being loaded 3125 * and displayed with no detectable error. The view will continue to load the full size image. 3126 * @param e The exception thrown. This error is logged by the view. 3127 */ 3128 void onPreviewLoadError(Exception e); 3129 3130 /** 3131 * Indicates an error initiliasing the decoder when using a tiling, or when loading the full 3132 * size bitmap when tiling is disabled. This method cannot be relied upon; certain encoding 3133 * types of supported image formats can result in corrupt or blank images being loaded and 3134 * displayed with no detectable error. 3135 * @param e The exception thrown. This error is also logged by the view. 3136 */ 3137 void onImageLoadError(Exception e); 3138 3139 /** 3140 * Called when an image tile could not be loaded. This method cannot be relied upon; certain 3141 * encoding types of supported image formats can result in corrupt or blank images being loaded 3142 * and displayed with no detectable error. Most cases where an unsupported file is used will 3143 * result in an error caught by {@link #onImageLoadError(Exception)}. 3144 * @param e The exception thrown. This error is logged by the view. 3145 */ 3146 void onTileLoadError(Exception e); 3147 3148 /** 3149 * Called when a bitmap set using ImageSource.cachedBitmap is no longer being used by the View. 3150 * This is useful if you wish to manage the bitmap after the preview is shown 3151 */ 3152 void onPreviewReleased(); 3153 } 3154 3155 /** 3156 * Default implementation of {@link OnImageEventListener} for extension. This does nothing in any method. 3157 */ 3158 public static class DefaultOnImageEventListener implements OnImageEventListener { 3159 onReady()3160 @Override public void onReady() { } onImageLoaded()3161 @Override public void onImageLoaded() { } onPreviewLoadError(Exception e)3162 @Override public void onPreviewLoadError(Exception e) { } onImageLoadError(Exception e)3163 @Override public void onImageLoadError(Exception e) { } onTileLoadError(Exception e)3164 @Override public void onTileLoadError(Exception e) { } onPreviewReleased()3165 @Override public void onPreviewReleased() { } 3166 3167 } 3168 3169 /** 3170 * An event listener, allowing activities to be notified of pan and zoom events. Initialisation 3171 * and calls made by your code do not trigger events; touch events and animations do. Methods in 3172 * this listener will be called on the UI thread and may be called very frequently - your 3173 * implementation should return quickly. 3174 */ 3175 @SuppressWarnings("EmptyMethod") 3176 public interface OnStateChangedListener { 3177 3178 /** 3179 * The scale has changed. Use with {@link #getMaxScale()} and {@link #getMinScale()} to determine 3180 * whether the image is fully zoomed in or out. 3181 * @param newScale The new scale. 3182 * @param origin Where the event originated from - one of {@link #ORIGIN_ANIM}, {@link #ORIGIN_TOUCH}. 3183 */ 3184 void onScaleChanged(float newScale, int origin); 3185 3186 /** 3187 * The source center has been changed. This can be a result of panning or zooming. 3188 * @param newCenter The new source center point. 3189 * @param origin Where the event originated from - one of {@link #ORIGIN_ANIM}, {@link #ORIGIN_TOUCH}. 3190 */ 3191 void onCenterChanged(PointF newCenter, int origin); 3192 3193 } 3194 3195 /** 3196 * Default implementation of {@link OnStateChangedListener}. This does nothing in any method. 3197 */ 3198 public static class DefaultOnStateChangedListener implements OnStateChangedListener { 3199 onCenterChanged(PointF newCenter, int origin)3200 @Override public void onCenterChanged(PointF newCenter, int origin) { } onScaleChanged(float newScale, int origin)3201 @Override public void onScaleChanged(float newScale, int origin) { } 3202 3203 } 3204 3205 } 3206