• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 &amp; swipe to zoom is enabled.
2681      * @return true if double tap &amp; swipe to zoom is enabled.
2682      */
isQuickScaleEnabled()2683     public final boolean isQuickScaleEnabled() {
2684         return quickScaleEnabled;
2685     }
2686 
2687     /**
2688      * Enable or disable double tap &amp; 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