davemorrissey | a033698 | 2015-02-23 21:49:23 +0000 | [diff] [blame] | 1 | Subsampling Scale Image View |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 2 | =========================== |
davemorrissey | b064b0b | 2013-08-26 09:45:26 -0700 | [diff] [blame] | 3 | |
David Morrissey | 71b94c2 | 2015-03-04 23:10:45 +0000 | [diff] [blame] | 4 | A custom image view for Android, designed for photo galleries and displaying huge images (e.g. maps and building plans) without `OutOfMemoryError`s. Includes pinch to zoom, panning, rotation and animation support, and allows easy extension so you can add your own overlays and touch event detection. |
David Morrissey | d419186 | 2014-10-18 23:14:07 +0100 | [diff] [blame] | 5 | |
David Morrissey | 71b94c2 | 2015-03-04 23:10:45 +0000 | [diff] [blame] | 6 | The view optionally uses subsampling and tiles to support very large images - a low resolution base layer is loaded and as you zoom in, it is overlaid with smaller high resolution tiles for the visible area. This avoids holding too much data in memory. It's ideal for displaying large images while allowing you to zoom in to the high resolution details. You can disable tiling for smaller images and when displaying a bitmap object. There are some advantages and disadvantages to disabling tiling so to decide which is best, see below. |
| 7 | |
| 8 | #### 2.x.x to 3.x.x migration |
| 9 | |
| 10 | **Version 3 includes breaking changes. To upgrade, you will need to make a few simple changes to your activities and subclasses.** |
| 11 | |
| 12 | **What's new** |
| 13 | |
| 14 | * Support for preview images, allowing a low resolution preview to be displayed and gestures to be enabled while base layer tiles of the full size image are loaded. This is useful for very big images (over 10,000px). |
| 15 | |
| 16 | * Revised lifecycle events, so you can display a loading message until the base layer is fully loaded. |
| 17 | |
| 18 | * A listener interface to allow activities to respond to lifecycle events and image loading errors. |
| 19 | |
| 20 | * `ScaleImageView` has been merged into `SubsamplingScaleImageView`, which now supports display of large images with tiling, and small images without tiling or from a bitmap object. |
| 21 | |
| 22 | **Migrating to version 3** |
| 23 | |
| 24 | **1)** Change the method for setting the image source. |
| 25 | |
| 26 | | Old method | New method | |
| 27 | | ---------- | ---------- | |
| 28 | | `view.setImageAsset("map.png")` | `view.setImage(ImageSource.asset("map.png"))` | |
| 29 | | `view.setImageResource(R.drawable.map)` | `view.setImage(ImageSource.resource(R.drawable.map, context))` | |
| 30 | | `view.setImageUri("/sdcard/map.png")` | `view.setImage(ImageSource.uri("/sdcard/map.png"))` | |
| 31 | | `view.setImageBitmap(map)` | `view.getImage(ImageSource.bitmap(map))` | |
| 32 | |
| 33 | **2)** Replace uses of `ScaleImageView` with `SubsamplingScaleImageView` and disable tiling if required. For example: |
| 34 | |
| 35 | SubsamplingScaleImageView view = (SubsamplingScaleImageView)findViewById(id.imageView); |
| 36 | view.setImage(ImageSource.asset("map.png").withTilingDisabled()); |
| 37 | |
| 38 | **3)** Replace uses of `isImageReady()` with `isReady()`. |
| 39 | |
| 40 | **4)** In subclasses, replace overrides of `onImageReady()` with `onReady()`. |
David Morrissey | 365ccab | 2014-07-31 00:08:41 +0100 | [diff] [blame] | 41 | |
David Morrissey | ef41d8a | 2014-06-10 01:00:09 +0100 | [diff] [blame] | 42 | #### Download the sample app |
| 43 | |
| 44 | [](https://play.google.com/store/apps/details?id=com.davemorrissey.labs.subscaleview.sample) |
| 45 | |
David Morrissey | d419186 | 2014-10-18 23:14:07 +0100 | [diff] [blame] | 46 | #### Hall of fame |
David Morrissey | 0e895c2 | 2013-08-26 20:07:35 +0100 | [diff] [blame] | 47 | |
David Morrissey | d419186 | 2014-10-18 23:14:07 +0100 | [diff] [blame] | 48 | **Are you using this library in your app? Let me know and I'll add it to this list.** |
| 49 | |
davemorrissey | e2d8c46 | 2015-02-24 19:21:58 +0000 | [diff] [blame] | 50 | | [](https://play.google.com/store/apps/details?id=com.sleetworks.serenity.android) | [](https://play.google.com/store/apps/details?id=com.laurencedawson.reddit_sync) | [](https://play.google.com/store/apps/details?id=com.journey.app) | |
davemorrissey | c7e461c | 2015-02-24 15:33:01 +0000 | [diff] [blame] | 51 | |---|---|---| |
| 52 | | **Fourth Mate** | **Sync for reddit** | **Journal** | |
| 53 | | [](https://play.google.com/store/apps/details?id=org.floens.chan) | [](https://play.google.com/store/apps/details?id=me.snapdiary.us.taggallery) | [](https://play.google.com/store/apps/details?id=com.nyctrans.it) | |
| 54 | | **Clover** | **Tag Gallery** | **nycTrans.it** | |
David Morrissey | d419186 | 2014-10-18 23:14:07 +0100 | [diff] [blame] | 55 | |
David Morrissey | 36cc125 | 2015-01-12 21:42:02 +0000 | [diff] [blame] | 56 | ## Features |
David Morrissey | 0e895c2 | 2013-08-26 20:07:35 +0100 | [diff] [blame] | 57 | |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 58 | #### Image display |
David Morrissey | 365ccab | 2014-07-31 00:08:41 +0100 | [diff] [blame] | 59 | |
David Morrissey | 71b94c2 | 2015-03-04 23:10:45 +0000 | [diff] [blame] | 60 | * Display images from assets, resources, the file system or bitmaps |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 61 | * Automatically rotate images from the file system (e.g. the camera or gallery) according to EXIF |
| 62 | * Manually rotate images in 90° increments |
David Morrissey | 0d041ff | 2015-03-08 21:44:44 +0000 | [diff] [blame^] | 63 | * Display a region of the source image |
| 64 | * Use a preview image while large images load |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 65 | * Swap images at runtime |
David Morrissey | 36cc125 | 2015-01-12 21:42:02 +0000 | [diff] [blame] | 66 | * Use a custom bitmap decoder |
David Morrissey | 0e895c2 | 2013-08-26 20:07:35 +0100 | [diff] [blame] | 67 | |
David Morrissey | 71b94c2 | 2015-03-04 23:10:45 +0000 | [diff] [blame] | 68 | *With tiling enabled:* |
David Morrissey | 365ccab | 2014-07-31 00:08:41 +0100 | [diff] [blame] | 69 | |
| 70 | * Display huge images, larger than can be loaded into memory |
| 71 | * Show high resolution detail on zooming in |
David Morrissey | 71b94c2 | 2015-03-04 23:10:45 +0000 | [diff] [blame] | 72 | * Tested up to 20,000x20,000px, though larger images are slower |
David Morrissey | 365ccab | 2014-07-31 00:08:41 +0100 | [diff] [blame] | 73 | |
David Morrissey | 71b94c2 | 2015-03-04 23:10:45 +0000 | [diff] [blame] | 74 | *This view doesn't extend `ImageView` and isn't intended as a general purpose replacement for it. It's specialised for the display of photos and other large images, not the display of 9-patches, shapes and the other types of drawable that ImageView supports.* |
David Morrissey | 365ccab | 2014-07-31 00:08:41 +0100 | [diff] [blame] | 75 | |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 76 | #### Gesture detection |
| 77 | * One finger pan |
| 78 | * Two finger pinch to zoom |
davemorrissey | a033698 | 2015-02-23 21:49:23 +0000 | [diff] [blame] | 79 | * Quick scale (one finger zoom) |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 80 | * Pan while zooming |
| 81 | * Seamless switch between pan and zoom |
| 82 | * Fling momentum after panning |
David Morrissey | 77096ba | 2014-06-05 21:22:44 +0100 | [diff] [blame] | 83 | * Double tap to zoom in and out |
David Morrissey | 02ceb3d | 2014-05-30 20:48:51 +0100 | [diff] [blame] | 84 | * Options to disable pan and/or zoom gestures |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 85 | |
David Morrissey | 9f3fad1 | 2014-06-08 10:16:49 +0100 | [diff] [blame] | 86 | #### Animation |
| 87 | * Public methods for animating the scale and center |
| 88 | * Customisable duration and easing |
| 89 | * Optional uninterruptible animations |
| 90 | |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 91 | #### Overridable event detection |
| 92 | * Supports `OnClickListener` and `OnLongClickListener` |
| 93 | * Supports interception of events using `GestureDetector` and `OnTouchListener` |
| 94 | * Extend to add your own gestures |
| 95 | |
| 96 | #### Easy integration |
| 97 | * Use within a `ViewPager` to create a photo gallery |
| 98 | * Easily restore scale, center and orientation after screen rotation |
| 99 | * Can be extended to add overlay graphics that move and scale with the image |
| 100 | * Handles view resizing and `wrap_content` layout |
| 101 | |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 102 | #### Limitations |
David Morrissey | 71b94c2 | 2015-03-04 23:10:45 +0000 | [diff] [blame] | 103 | * Tiling requires SDK 10 (Gingerbread). You can work around this with a custom decoder - see the section below. |
| 104 | * With tiling enabled, the view cannot display grayscale PNGs on Android Lollipop, due to bugs in the skia library and/or BitmapRegionDecoder. Earlier versions of Android also have issues displaying some grayscale PNGs, but not all. I have reported these bugs to Google. For a workaround, see the section on custom bitmap decoders below. |
| 105 | * The view does not extend ImageView so attributes including android:tint and android:src are not supported. Support for android:scaleType is limited and it must be set from code. |
David Morrissey | 36cc125 | 2015-01-12 21:42:02 +0000 | [diff] [blame] | 106 | * Images stored in resources and assets cannot be rotated based on EXIF, you'll need to do it manually. You probably know the orientation of your own files :-) |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 107 | |
David Morrissey | 71b94c2 | 2015-03-04 23:10:45 +0000 | [diff] [blame] | 108 | ## When to use tiling |
David Morrissey | 365ccab | 2014-07-31 00:08:41 +0100 | [diff] [blame] | 109 | |
David Morrissey | 71b94c2 | 2015-03-04 23:10:45 +0000 | [diff] [blame] | 110 | Tiling is enabled by default. You may wish to disable it under some circumstances. |
| 111 | |
| 112 | Enable tiling if: |
David Morrissey | 365ccab | 2014-07-31 00:08:41 +0100 | [diff] [blame] | 113 | |
| 114 | * You want to zoom into very large images without losing detail. |
| 115 | * You need to display images of unknown size e.g. from the camera or gallery. |
| 116 | * You don't know if the images may be too large to fit in memory on some devices. |
| 117 | * You need to display images larger than 2048px. |
| 118 | * You don't need to support devices older than SDK 10. |
| 119 | |
David Morrissey | 71b94c2 | 2015-03-04 23:10:45 +0000 | [diff] [blame] | 120 | Disable tiling if: |
David Morrissey | 365ccab | 2014-07-31 00:08:41 +0100 | [diff] [blame] | 121 | |
| 122 | * You know the size of the images you're displaying. |
| 123 | * You know the images are small enough to fit in memory on all your target devices. |
| 124 | * Your images are no larger than 2048px, or you are able to scale them down. |
| 125 | * You need to support devices older than SDK 10. |
| 126 | |
David Morrissey | 36cc125 | 2015-01-12 21:42:02 +0000 | [diff] [blame] | 127 | ## Installation |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 128 | |
David Morrissey | 2b6553c | 2015-01-09 00:05:43 +0000 | [diff] [blame] | 129 | Add the library to your app using one of these methods: |
David Morrissey | 64f2ef0 | 2014-11-15 15:39:35 +0000 | [diff] [blame] | 130 | |
David Morrissey | 0d041ff | 2015-03-08 21:44:44 +0000 | [diff] [blame^] | 131 | * Add `com.davemorrissey.labs:subsampling-scale-image-view:3.0.0` as a dependency in your build.gradle file |
David Morrissey | 2b6553c | 2015-01-09 00:05:43 +0000 | [diff] [blame] | 132 | * *or* download the library aar file from the releases page and add to your app manually |
| 133 | * *or* clone the project and import the library subproject as a module in your app |
David Morrissey | 64f2ef0 | 2014-11-15 15:39:35 +0000 | [diff] [blame] | 134 | * *or* clone the project and copy the resources and classes from `com.davemorrissey.labs.subscaleview` into your project |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 135 | |
| 136 | Add the view to your layout XML as shown below. Normally you should set width and height to `match_parent`. |
| 137 | |
| 138 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
| 139 | android:layout_width="match_parent" |
| 140 | android:layout_height="match_parent" > |
| 141 | |
| 142 | <com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView |
| 143 | android:id="@+id/imageView" |
David Morrissey | e11ee3e | 2014-05-30 14:09:41 +0100 | [diff] [blame] | 144 | android:layout_width="match_parent" |
| 145 | android:layout_height="match_parent"/> |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 146 | |
| 147 | </RelativeLayout> |
| 148 | |
David Morrissey | 36cc125 | 2015-01-12 21:42:02 +0000 | [diff] [blame] | 149 | Now, in your fragment or activity, set the image resource, asset name or file path. |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 150 | |
| 151 | SubsamplingScaleImageView imageView = (SubsamplingScaleImageView)findViewById(id.imageView); |
David Morrissey | 71b94c2 | 2015-03-04 23:10:45 +0000 | [diff] [blame] | 152 | imageView.setImage(ImageSource.resource(R.drawable.monkey)); |
David Morrissey | 36cc125 | 2015-01-12 21:42:02 +0000 | [diff] [blame] | 153 | // ... or ... |
David Morrissey | 71b94c2 | 2015-03-04 23:10:45 +0000 | [diff] [blame] | 154 | imageView.setImage(ImageSource.asset("map.png")) |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 155 | // ... or ... |
David Morrissey | 71b94c2 | 2015-03-04 23:10:45 +0000 | [diff] [blame] | 156 | imageView.setImage(ImageSource.uri("/sdcard/DCIM/DSCM00123.JPG")); |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 157 | |
| 158 | That's it! Keep reading for some more options. |
| 159 | |
| 160 | ## Define asset name in XML |
| 161 | |
| 162 | For a zero code approach to showing an image from your assets, you need to define the custom namespace in your layout. |
| 163 | |
| 164 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
| 165 | xmlns:ssiv="http://schemas.android.com/apk/res-auto" |
| 166 | android:layout_width="match_parent" |
| 167 | android:layout_height="match_parent" > |
| 168 | |
| 169 | <com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView |
| 170 | ssiv:assetName="map.png" |
David Morrissey | e11ee3e | 2014-05-30 14:09:41 +0100 | [diff] [blame] | 171 | android:layout_width="match_parent" |
| 172 | android:layout_height="match_parent"/> |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 173 | |
| 174 | </RelativeLayout> |
| 175 | |
| 176 | **This method doesn't support restoring state after a screen orientation change.** |
| 177 | |
| 178 | ## Handle screen orientation changes |
| 179 | |
| 180 | If you want the current scale, center and orientation to be preserved when the screen is rotated, you can request it from the view's `getState` method, and restore it after rotation, by passing it to the view along with the image asset name or file path. Here's a simple example of how you might do this in a fragment. |
| 181 | |
| 182 | private static final String BUNDLE_STATE = "ImageViewState"; |
| 183 | |
| 184 | @Override |
| 185 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { |
| 186 | View rootView = inflater.inflate(R.layout.my_fragment, container, false); |
| 187 | |
| 188 | ImageViewState imageViewState = null; |
| 189 | if (savedInstanceState != null && savedInstanceState.containsKey(BUNDLE_STATE)) { |
| 190 | imageViewState = (ImageViewState)savedInstanceState.getSerializable(BUNDLE_STATE); |
| 191 | } |
| 192 | SubsamplingScaleImageView imageView = (SubsamplingScaleImageView)rootView.findViewById(id.imageView); |
David Morrissey | 71b94c2 | 2015-03-04 23:10:45 +0000 | [diff] [blame] | 193 | imageView.setImage(ImageSource.asset("map.png"), imageViewState); |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 194 | |
| 195 | return rootView; |
| 196 | } |
| 197 | |
| 198 | @Override |
| 199 | public void onSaveInstanceState(Bundle outState) { |
| 200 | View rootView = getView(); |
| 201 | if (rootView != null) { |
| 202 | SubsamplingScaleImageView imageView = (SubsamplingScaleImageView)rootView.findViewById(id.imageView); |
| 203 | ImageViewState state = imageView.getState(); |
| 204 | if (state != null) { |
| 205 | outState.putSerializable(BUNDLE_STATE, imageView.getState()); |
| 206 | } |
| 207 | } |
| 208 | } |
| 209 | |
David Morrissey | 36cc125 | 2015-01-12 21:42:02 +0000 | [diff] [blame] | 210 | ## Custom bitmap decoders |
| 211 | |
| 212 | Android's `BitmapRegionDecoder` class is based on the Skia library. Some users have reported problems with this library, particularly when displaying grayscale JPGs. Unfortunately it seems to be less reliable in recent versions of Android. If you don't control the format of the images displayed in your app (for example, they are user generated content) you may require a more reliable decoder. |
| 213 | |
| 214 | To use your own decoder based on a different library, implement the `ImageRegionDecoder` class (do not include a constructor) and enable it using this call: |
| 215 | |
| 216 | imageView.setDecoderClass(MyImageRegionDecoder.class); |
| 217 | |
| 218 | As an example, see the [`RapidImageRegionDecoder`](https://github.com/davemorrissey/subsampling-scale-image-view/blob/master/sample/src/com/davemorrissey/labs/subscaleview/sample/imagedisplay/decoders/RapidImageRegionDecoder.java) class, which is based on [RapidDecoder](https://github.com/suckgamony/RapidDecoder). This library is better at decoding grayscale JPG images but does not handle large images as well as `BitmapRegionDecoder` - it is significantly slower and more likely to throw out of memory errors. It appears to be very fast and reliable for PNG images. If you can detect the size and type of an image before displaying it, you can use different decoders for different images to get the best results. |
| 219 | |
| 220 | Whenever possible, convert your images to a format Android's Skia library can support, and test with a variety of devices. |
| 221 | |
| 222 | ## Quality notes |
| 223 | |
| 224 | Images are decoded as dithered RGB_565 bitmaps by default, because this requires half as much memory as ARGB_8888. For most |
| 225 | JPGs you won't notice the difference in quality. If you are displaying large PNGs with alpha channels, Android will probably |
| 226 | decode them as ARGB_8888, and this may cause `OutOfMemoryError`s. **If possible, remove the alpha channel from PNGs larger than about 2,000x2,000.** |
| 227 | This allows them to be decoded as RGB_565. |
| 228 | |
David Morrissey | a0bf802 | 2014-05-30 14:08:31 +0100 | [diff] [blame] | 229 | ## Extending functionality |
| 230 | |
David Morrissey | 365ccab | 2014-07-31 00:08:41 +0100 | [diff] [blame] | 231 | Take a look at the sample app for examples of classes that overlay graphics on top of the image so that they move and scale with it. `FreehandView` adds event detection, capturing only the touch events it needs so pan and zoom still work normally. |
David Morrissey | 5833e30 | 2014-06-06 22:08:14 +0100 | [diff] [blame] | 232 | |
| 233 | ## About |
| 234 | |
David Morrissey | 71b94c2 | 2015-03-04 23:10:45 +0000 | [diff] [blame] | 235 | Copyright 2015 David Morrissey, and licensed under the Apache License, Version 2.0. No attribution is necessary but it's very much appreciated. Star this project if you like it, and send a link to your project on GitHub or app in Google Play if you'd like me to add it to this page. |