This project isn't maintained anymore. It is now recommended to use https://github.com/peterLaurence/MapView.
MapView is maintained by Peter, one of our main contributors. MapView is an highly optimized Kotlin version (which can be used from Java projects) that's very cool and fast. It's 100% production ready and open source. Peter uses this widget in his app at https://github.com/peterLaurence/TrekMe/.
The TileView widget is a subclass of ViewGroup renders and positions bitmap tiles to compose a larger, original image, often one too large to display normally.
Vesion 4 is effectively the promotion of version 3 from beta to production-ready, fixing the issues brought up by you, the users. While normally this'd probably be done using a series of minor version bumps, and this might be 3.4.11, there was a change to the base package name, and this requires a major change according to semver, since it's a breaking change for past versions.
Also of note, the universal (2-D) ScrollView class, and related classes like ScalingScrollView (which is a subclass of the 2D universal ScrollView, but also manages scaling and scale gestures) is now it's own repository: https://github.com/moagrius/ScrollView, and available with implementation 'com.moagrius:scrollview:1.0.3'
. It is now included in the TileView project using normal gradle dependency operations, specifically using api
rather than implementation
but otherwise similar to other dependencies and is being pulled down from jcenter and reconciled normally.
Demos for ScrollView are in the ScrollView repo. Demos for TileView are in this repository.
Feel free to use ScrollView as a standalone widget if you don't need image tiling.
Handler
. This is a serious memory leak and all users on versions 4.0.3 and 4.0.4 should immediately upgrade to 4.0.5.ScalingScrollView
to version 1.0.10, which provides additional methods and method exposure (many private
access methods became protected
)LowFidelityBackgroundPlugin
now scaled and positions appropriately with the TileView
host.COVER
and CONTAIN
MinimumScaleModes
should work properly now. com.moagrius:scrollview:1.0.4
to 1.0.9
Very large images in Android will often result in an OutOfMemoryError
. Memory is finite, and bitmaps take a great deal of it. TileView
solves this by stitching together small pieces of the image (tiles) and displaying them reconstructed in the area of the screen visible to your user.
Out of the box, the TileView will manage the tiled image, including subsampling when needed, flinging, dragging, scaling, and multiple levels of detail.
Additional plugins are provided to allow adding overlaying Views (markers), info windows, hot spots, path drawing, and coordinate translation.
Version 4 is fairly young. If you're need a stable version, the last release of version 2 is 2.7.7, and is available as a branch, here: https://github.com/moagrius/TileView/tree/version-2.7.7, or from the releases page. You can also get it from gradle using the old namespace:
implementation 'com.qozix:tileview:2.7.7'
No further work will be done on version 2.
This is truly open source. I'm very happy to accept improvements or bug fixes - no caveats; create an issue, branch, and create a PR. While I'm not super interested in finding bugs and don't want the issues page to become a wasteland (I already know what most of them are, and will start to document them as this make its way into the wild), I am very interested in fixing those bugs - if you have the time to locate and correct a bug, please open a PR.
Add this to your app module's build.gradle.
implementation 'com.moagrius:tileview:4.0.7'
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TileView tileView = new TileView.Builder(this)
.setSize(3000, 3000)
.defineZoomLevel("tile-%d-%d.png")
.build();
setContentView(tileView);
}
That's it. You should have a tiled image that only renders the pieces of the image that are within the current viewport, and pans and zooms with gestures.
Note that String replacements for rows and columns is not required - you can supply literally any Object instance to a DetailLevel, and a BitmapProvider implementation can use that Object to generate a Bitmap instance however you want.
As a user, the biggest things you'll notice are:
defineDetail(anyObject)
is sufficient.BitmapProvider
. We're just doing way too much management of Bitmaps, including re-use and caching, to allow any stray Bitmap to wander in. This may change in the future, but the replacement for now is StreamProvider
- basically the same thing but you just return an InputStream
_instead of a Bitmap
, and we take care of the rest.ScrollView
, so you can assign normal OnClickListeners
or similiar gesture management devices on things like markers and callouts, without interrupting drag or fling operations.TileViewDemoSimple
for a very bare implementation, and TileViewDemoAdvanced
for a more dressed up version.Tile
or TileView
- if you understand those 2 classes, you understand the majority of the project.ScrollView
, ScalingScrollView
, and TileView
. Each inherits from the last. You'll notice the demo module has Activities
for each of these classes.com.moagrius.widgets.ScrollView
is a clone of AOSP's ScrollView
and HorizontalScrollView
, with some inspiration taken from RecyclerView
and GestureDetector
. That means
the APIs will be very familiar to anyone who's used the framework-provided ScrollView
. The big difference here is that com.moagrius.widgets.ScrollView
functions along both
axes, without configuation. For example, if you have a layout that matches parent width and expands vertically, ScrollView
will scroll it vertically - the converse holds. If you have a layout that larger in both directions than the parent, ScrollView
will scroll in any direction.
Again, no configration is required - there is no orientation
attribute - it does it's best to figure out what makes sense.
ScalingScrollView
extends ScrollView
and adds scaling APIs, as well as built-in scaling gesture managent (pinch and double tap to zoom). By default, it will visually scale its content, but that can be disabled with a single setter setShouldVisuallyScaleContents(false)
, and you can manage your content with a porportional scaled value. Similarly, it will lay itself out to the current scale value, unless that too is disabled setWillHandleContentSize(true)
At some point, ScalingScrollView
will be it's own repository, and will be a dependency of TileView
.
TileView
inherits from ScalingScrollView
, and adds tiling functionality.
From here down may be a bit dry but might be interesting to people interested in contributing to the process.
You have an image too large to display with an OutOfMemoryError
.
Chop the image into tiles and reconstruct them to fill the viewport, and only the viewport. As the user scrolls (or scales), aggressively recycle any bitmap data that is not visible to the user at any given time, and render any new "tiles" that are not within the viewable area.
Use the width and height of the view that contains this image, plus it's scroll position, to determine space we call the viewport.
Divide the corners of the viewport (left, top, right, bottom) by the size of the cell, which provide the start and end row and column. Round down the left and top and round up the right and bottom to make sure there are not empty patches - as long as any pixel of a tile is visible in the viewport, it must be rendered (one reason - among many - to carefully consider tile size, and not default to as large as possible).
From start column to end column and start row to end row are the tiles (imagine a 2D array) that should be rendered at that moment.
For example, let's consider...
so grid is
Iterate between those points...
for (int i = grid.columns.start; i < grid.columns.end; i++) {
for (int j = grid.rows.start; j < grid.rows.end; j++) {
And your grid looks like this:
(03, 02), (04, 02), (05, 02), (06, 02), (07, 02), (08, 02), (09, 02)
(03, 03), (04, 03), (05, 03), (06, 03), (07, 03), (08, 03), (09, 03)
(03, 04), (04, 04), (05, 04), (06, 04), (07, 04), (08, 04), (09, 04)
(03, 05), (04, 05), (05, 05), (06, 05), (07, 05), (08, 05), (09, 05)
(03, 06), (04, 06), (05, 06), (06, 06), (07, 06), (08, 06), (09, 06)
(03, 07), (04, 07), (05, 07), (06, 07), (07, 07), (08, 07), (09, 07)
(03, 08), (04, 08), (05, 08), (06, 08), (07, 08), (08, 08), (09, 08)
(03, 09), (04, 09), (05, 09), (06, 09), (07, 09), (08, 09), (09, 09)
(03, 10), (04, 10), (05, 10), (06, 10), (07, 10), (08, 10), (09, 10)
As long as there's only the one size, scale is uniformly applied - just multiple tile size in the previous math by the current scale, and your grid remains intact.
At every half way point, the image is at 50% or smaller, so subsample it to save memory: https://developer.android.com/topic/performance/graphics/load-bitmap#load-bitmap
Remember that each subsample (representing half the size) will actually quarter the amount of memory required for the bitmap. Do this at every half: 50%, 25, 12.5, 6.25, 3.625, etc.
When you add another detail level, things get a little more complicated, but not terribly...
for (int i = grid.columns.start; i < grid.columns.end; i += imageSample) {
for (int j = grid.rows.start; j < grid.rows.end; j += imageSample) {
And for each of those tiles, we grab the last good detail level and fill in the blanks, so tile (0,4) would draw it's neighbors:
(0,0), (0,1), (0,2), (0,3)
(1,0), (1,1), (1,2), (1,3)
(2,0), (2,1), (2,2), (2,3)
(3,0), (3,1), (3,2), (3,3)
This allows us to then save the "patched" bitmap tile to both a memory and a disk cache, so we're not decoding very large files and discarding excess data after the first time.
The next thing that comes up is when changing between zoom levels. If we just swap out levels, the screen will be blank for a second, regardless of whether that's a defined, supplied detail level, or just subsampling the tiles. Since View.onDraw
uses a Canvas
, we need to redraw the entire thing each time invalidation occurs. That means if you switch between zoom level, the canvas will clear the old tiles before drawing the new tiles. This behavior is generally good (and what ensures that we're generally only going to drawing as much bitmap data as we need to fill the screen, even if we do a bad job of cleaning up), but in this case the visual effect is jarring.
To remedy this, we:
Region.quickReject
to determine if that previous tile intersects any part of the virtual viewport that will not be filled with current tile pixel data.
So all the preceding "works" pretty well, but decoding the same data over and over is a lot of work that we can probably skip if we're smart about it.
Emphasis on and reuse - the reuse bit is as important as (maybe even more than) the traditional caching piece.
The default providers assume we're reading tiles from disk already, so the only thing that goes into disk-cache are the patched tiles, mentioned above (this behavior can be modified, if for example you're reading tiles from a server). Everything, however, is subject to a memory cache. The default is one-quarter of available RAM, but the more you can spare for this, the better your performance will be. This serves as both a straight bitmap cache, and a pool of re-usable objects: When we first go to decode a tile, we check if that exact bitmap is in the memory cache - if it is, great, use it - if it's not, grab the oldest bitmap in the cache and re-use the underlying bitmap (BitmapFactory.Options.inBitmap)
Reusing bimaps prevents frequent, large memory allocation and de-allocation, and can greatly improve the perceived performance of your app. I'll let Colt explain: https://www.youtube.com/watch?v=_ioFW3cyRV0
Threading is accomplished using a dedicated ThreadPoolExecutor
. It's basically a fixed size pool with a pool size equal to the number of cores on the device (so 2 or 4 commonly, on newer phones as much as 8). There's a 0 keep-alive and tasks are queued with a LinkedBlockingQueue
. A custom factory provides Thread
instances that use Thread.MIN_PRIORITY
to we don't consume resource the main thread needs to perform nice scrolls and flings. Going further on this, each Tile
(which is a Runnable
submitted to the ThreadPoolExecutor
also calls Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
which gives a larger range than Thread.setPriority
, and this is actually very noticeable. On a real Nexus 6, the jank without call to setThreadPriority
isnt' terrible but is obvious, and immediately remedied with the call just mentioned.
This general approach is very similar to what you'd get with Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
.