diff --git a/README.md b/README.md index 9eac05e4..0728d5d7 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,17 @@ ![Release Badge](https://img.shields.io/github/release/moagrius/TileView.svg) -_**03/18/16** if you're using a version earlier than 2.1, there were significant performance gains realized with 2.1 so we'd advise you to start using the most recent version (2.1 or later) immediately. The improvements made also make fast-render viable, especially if not fetching tile data from across a network, so we'd also encourage you to try `TileView.setShouldRenderWhilePanning(true);` if you'd like more responsive tile rendering._ +#TileView +The TileView widget is a subclass of ViewGroup that provides a mechanism to asynchronously display tile-based images, with additional functionality for 2D dragging, flinging, pinch or double-tap to zoom, adding overlaying Views (markers), built-in Hot Spot support, dynamic path drawing, multiple levels of detail, and support for any relative positioning or coordinate system. + +![Demo](https://cloud.githubusercontent.com/assets/701344/17538476/6933099e-5e6b-11e6-9e18-45e924c19c91.gif) +_Properly configured, TileView can render tiles quickly enough to create the illusion of a seamless image_ +[](https://cloud.githubusercontent.com/assets/701344/10954033/d20843bc-8310-11e5-83ad-4e062b9b1be0.gif) + +_**08/07/16** 2.2 is released, and provides some much-needed improvements in how tiles are rendered - please consider upgrading, but be aware there are some minor potential breaking changes (that should not affect 99% of users)._ + +_**03/18/16** if you're using a version earlier than 2.1, there were significant performance gains realized with 2.1 so we'd advise you to start using the most recent version (2.1 or later) immediately. The improvements made also make fast-render viable, so we'd also encourage you to try `TileView.setShouldRenderWhilePanning(true);` if you'd like more responsive tile rendering._ -#Version 2.0 +##Version 2.0 **Version 2.0 released 10.25.15** @@ -20,23 +29,19 @@ Major goals were: 6. General refactoring. There are too many simplifications and optimization to mention, but each class and each method has been revisited. 7. Hooks hooks hooks! While pan and zoom events are broadcast using a familiar listener mechanism, and should be sufficient for most use-cases, public hooks exist for a large number of operations that can be overriden by subclasses for custom functionality. -#Change Log +##Change Log (Only major and minor changes are tracked here, consult git history for patches) +**2.2** Rewrite of tile rendering strategy, again with the help of @peterLaurence. Peak memory consumption should be reduced, and Tile render performance should be improved. **2.1** Rewrite of threading strategy, thanks to @peterLaurence and @bnsantos. Tile render performance is substantially improved. -#TileView -The TileView widget is a subclass of ViewGroup that provides a mechanism to asynchronously display tile-based images, with additional functionality for 2D dragging, flinging, pinch or double-tap to zoom, adding overlaying Views (markers), built-in Hot Spot support, dynamic path drawing, multiple levels of detail, and support for any relative positioning or coordinate system. - -![Demo](https://cloud.githubusercontent.com/assets/701344/10954033/d20843bc-8310-11e5-83ad-4e062b9b1be0.gif) - ###Documentation Javadocs are [here](http://moagrius.github.io/TileView/index.html?com/qozix/tileview/TileView.html). Wiki is [here](https://github.com/moagrius/TileView/wiki). ###Installation Gradle: ``` -compile 'com.qozix:tileview:2.1.8' +compile 'com.qozix:tileview:2.2.0' ``` The library is hosted on jcenter, and is not currently available from maven. @@ -56,7 +61,7 @@ A demo application, built in Android Studio, is available in the `demo` folder o at the 2nd column from left and 3rd row from top. 1. Create a new application with a single activity ('Main'). 1. Save the image tiles to your `assets` directory. -1. Add `compile 'com.qozix:tileview:2.1.8'` to your gradle dependencies. +1. Add `compile 'com.qozix:tileview:2.2.0'` to your gradle dependencies. 1. In the Main Activity, use this for `onCreate`: ``` @Override @@ -76,15 +81,15 @@ That's it. You should have a tiled image that only renders the pieces of the im ![detail-levels](https://cloud.githubusercontent.com/assets/701344/10954031/d2059c3e-8310-11e5-821d-26dd8691d4d3.gif) -A TileView instance can have any number of detail levels, which is a single image made up of many tiles. These tiles are positioned appropriately to show the portion of the image that the device's viewport is displayed - other tiles are recyled (and their memory freed) as they move out of the visible area. Detail levels often showing the same content at different magnifications, but may show different details as well - for example, a detail level showing a larger area will probably label features differently than a detail level showing a smaller area (imagine a TileView representing the United States may show the Rocky Mountains at a very low detail level, while a higher detail level may show individual streets or addresses. +A TileView instance can have any number of detail levels, which is a single image made up of many tiles; each DetailLevel exists in the same space, but are useful to show different levels of details (thus the class name), and to further break down large images into smaller tiles sets. These tiles are positioned appropriately to show the portion of the image that the device's viewport is displayed - other tiles are recycled (and their memory freed) as they move out of the visible area. Detail levels often show the same content at different magnifications, but may show different details as well - for example, a detail level showing a larger area will probably label features differently than a detail level showing a smaller area (imagine a TileView representing the United States may show the Rocky Mountains at a very low detail level, while a higher detail level may show individual streets or addresses. -Each detail level is passed a float value, indicating the scale value that it represents (e.g., a detail level passed 0.5f scale would be displayed when the TileView was zoomed out by 50%). Additionally, each detail level is passed an aribtrary data object that is attached to each tile and can provide instructions on how to generate the tile's bitmap. +Each detail level is passed a float value, indicating the scale value that it represents (e.g., a detail level passed 0.5f scale would be displayed when the TileView was zoomed out by 50%). Additionally, each detail level is passed an arbitrary data object that is attached to each tile and can provide instructions on how to generate the tile's bitmap. That data object is often a String, formatted to provide the path to the bitmap image for that Tile, but can be any kind of Object whatsoever - during the decode process, each tile has access to the data object for the detail level. ####Tiles -A Tile is a class instance that represents a Bitmap which is a portion of the total image. Each Tile provides position information, and methods to manage the Bitmap, and is also passed to the TileView's `BitmapProvider` implementation, which is how individual bitmaps are generated. +A Tile is a class instance that represents a Bitmap - a portion of the total image. Each Tile provides position information, and methods to manage the Bitmap's state and behavior. Each Tile instanced is also passed to the TileView's `BitmapProvider` implementation, which is how individual bitmaps are generated. Tile instances uses an `equals` method that compares only row, column and detail level, and are often passed in `Set` collections, so that Tile instances already in process are simply excluded by the unique nature of the Set if the program or user tries to add a single Tile more than once. -Each TileView uses a `BitmapProvider` implementation to generate tile bitmaps. The interface defines a single method: `public Bitmap getBitmap( Tile tile, Context context );`. This method is called each time a bitmap is required, and has access to the Tile instance for that position and detail level, and a Context object to access system resources. The `BitmapProvider` implementation can generate the bitmap in any way it chooses - assets, resources, http requests, dynamically drawn, SVG, decoded regions, etc. The default implementation, `BitmapProviderAssets`, parses a String (the data object passed to the DetailLevel) and returns a bitmap found by file name in the app's assets directory. +Each TileView instance must reference a `BitmapProvider` implementation to generate tile bitmaps. The interface defines a single method: `public Bitmap getBitmap( Tile tile, Context context );`. This method is called each time a bitmap is required, and has access to the Tile instance for that position and detail level, and a Context object to access system resources. The `BitmapProvider` implementation can generate the bitmap in any way it chooses - assets, resources, http requests, dynamically drawn, SVG, decoded regions, etc. The default implementation, `BitmapProviderAssets`, parses a String (the data object passed to the DetailLevel) and returns a bitmap found by file name in the app's assets directory. ####Markers & Callouts @@ -145,14 +150,14 @@ tileView.addPath( drawablePath ); ####Scaling The `setScale(1)` method sets the initial scale of the TileView. -`setScaleLimits(0, 1)` sets the minimum and maximum scale which controls how far a TileView can be zoomed in or out. `0` means completely zoomed out, `1` means zoomed in to the most detailled level (with the pixels of the tiles matching the screen dpi). For example by using `setScaleLimits(0, 3)` you allow users to zoom in even further then the most detailled level (stretching the image). +`setScaleLimits(0, 1)` sets the minimum and maximum scale which controls how far a TileView can be zoomed in or out. `0` means completely zoomed out, `1` means zoomed in to the most detailed level (with the pixels of the tiles matching the screen dpi). For example by using `setScaleLimits(0, 3)` you allow users to zoom in even further then the most detailed level (stretching the image). `setMinimumScaleMode(ZoomPanLayout.MinimumScaleMode.FILL)` controls how far a image can be zoomed out based on the dimensions of the image: - `FILL`: Limit the minimum scale to no less than what would be required to fill the container - `FIT`: Limit the minimum scale to no less than what would be required to fit inside the container - `NONE`: Limit to the minimum scale level set by `setScaleLimits` -_When using `FILL` or `FIT`, the minimum scale level of `setScaleLimits` is ignored_ +_When using `FILL` or `FIT`, the minimum scale level of `setScaleLimits` is ignored._ ####Hooks and Listeners @@ -173,7 +178,7 @@ tileView.addZoomPanListener( new ZoomPanListener(){ }); ``` -Additionally, TileView reports most significant operations to hooks. TileView implements `ZoomPanLayout.ZoomPanListener`, `TileCanvasViewGroup.TileRenderListener`, and `DetailLevelManager.DetailLevelChangeListener`, and it's super class implements `GestureDetector.OnGestureListener`, `GestureDetector.OnDoubleTapListener`,`ScaleGestureDetector.OnScaleGestureListener`, and `TouchUpGestureDetector.OnTouchUpListener`. As such, the following hooks are available to be overriden by subclasses of TileView: +Additionally, TileView reports most significant operations to hooks. TileView implements `ZoomPanLayout.ZoomPanListener`, `TileCanvasViewGroup.TileRenderListener`, and `DetailLevelManager.DetailLevelChangeListener`, and it's super class implements `GestureDetector.OnGestureListener`, `GestureDetector.OnDoubleTapListener`,`ScaleGestureDetector.OnScaleGestureListener`, and `TouchUpGestureDetector.OnTouchUpListener`. As such, the following hooks are available to be overridden by subclasses of TileView: ``` protected void onScrollChanged( int l, int t, int oldl, int oldt ); @@ -214,7 +219,7 @@ See the [wiki entry here](https://github.com/moagrius/TileView/wiki/Creating-Til ####...use relative coordinates (like latitude and longitude)? The TileView method `defineBounds( double left, double top, double right, double bottom )` establishes a coordinate system for further positioning method calls (e.g., `scrollTo`, `addMarker`, etc). After relative coordinates are established by invoking the `defineBounds` method, any subsequent method invocations that affect position *and* accept `double` parameters will compute the value as relative of the provided bounds, rather than absolute pixels. That's to say that: - 1. A TileView instance is intialized with `setSize( 5000, 5000 );` + 1. A TileView instance is initialized with `setSize( 5000, 5000 );` 1. That TileView instance calls `defineBounds( 0, 100, 0, 100 );` 1. That TileView instance calls `scrollTo( 25d, 50d );` 1. That TileView will immediately scroll to the pixel at 1250, 2500. @@ -298,3 +303,6 @@ tileView.addView( downSample, 0 ); ###Contributing See [here](https://github.com/moagrius/TileView/wiki/Contributing). + +###Contributors +Several members of the github community have contributed and made `TileView` better, but over the last year or so, @peterLaurence has been as involved as myself and been integral in the last few major updates. Thanks Peter. diff --git a/build.gradle b/build.gradle index be515a81..aff4f415 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.3.0' + classpath 'com.android.tools.build:gradle:2.1.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/demo/src/main/assets/samples/boston-overview.jpg b/demo/src/main/assets/samples/boston-overview.jpg deleted file mode 100644 index 9054ed74..00000000 Binary files a/demo/src/main/assets/samples/boston-overview.jpg and /dev/null differ diff --git a/demo/src/main/assets/samples/boston-pedestrian.jpg b/demo/src/main/assets/samples/boston-pedestrian.jpg deleted file mode 100644 index 0e97fd71..00000000 Binary files a/demo/src/main/assets/samples/boston-pedestrian.jpg and /dev/null differ diff --git a/demo/src/main/assets/samples/middle-earth.jpg b/demo/src/main/assets/samples/middle-earth.jpg deleted file mode 100644 index 891532dc..00000000 Binary files a/demo/src/main/assets/samples/middle-earth.jpg and /dev/null differ diff --git a/demo/src/main/assets/samples/mona-lisa.jpg b/demo/src/main/assets/samples/mona-lisa.jpg deleted file mode 100644 index 31b400ef..00000000 Binary files a/demo/src/main/assets/samples/mona-lisa.jpg and /dev/null differ diff --git a/demo/src/main/assets/samples/plans.JPG b/demo/src/main/assets/samples/plans.JPG deleted file mode 100644 index 6a1eb3c3..00000000 Binary files a/demo/src/main/assets/samples/plans.JPG and /dev/null differ diff --git a/demo/src/main/java/tileview/demo/BuildingPlansTileViewActivity.java b/demo/src/main/java/tileview/demo/BuildingPlansTileViewActivity.java index 339f1ce3..bacb67ca 100644 --- a/demo/src/main/java/tileview/demo/BuildingPlansTileViewActivity.java +++ b/demo/src/main/java/tileview/demo/BuildingPlansTileViewActivity.java @@ -21,6 +21,12 @@ public void onCreate( Bundle savedInstanceState ) { // size of original image at 100% mScale tileView.setSize( 2736, 2880 ); + // small map, let's let it resize to 200% + tileView.setScaleLimits( 0, 2 ); + + // we're running from assets, should be fairly fast decodes, go ahead and render asap + tileView.setShouldRenderWhilePanning( true ); + // detail levels tileView.addDetailLevel( 1.000f, "tiles/plans/1000/%d_%d.jpg"); tileView.addDetailLevel( 0.500f, "tiles/plans/500/%d_%d.jpg"); diff --git a/demo/src/main/java/tileview/demo/FictionalMapTileViewActivity.java b/demo/src/main/java/tileview/demo/FictionalMapTileViewActivity.java index 89f2f8fc..d20a00bc 100644 --- a/demo/src/main/java/tileview/demo/FictionalMapTileViewActivity.java +++ b/demo/src/main/java/tileview/demo/FictionalMapTileViewActivity.java @@ -17,6 +17,9 @@ public void onCreate( Bundle savedInstanceState ) { // size of original image at 100% mScale tileView.setSize( 4015, 4057 ); + + // we're running from assets, should be fairly fast decodes, go ahead and render asap + tileView.setShouldRenderWhilePanning( true ); // detail levels tileView.addDetailLevel( 1.000f, "tiles/fantasy/1000/%d_%d.jpg"); diff --git a/demo/src/main/java/tileview/demo/LargeImageTileViewActivity.java b/demo/src/main/java/tileview/demo/LargeImageTileViewActivity.java index a6ef7c40..d189b48a 100644 --- a/demo/src/main/java/tileview/demo/LargeImageTileViewActivity.java +++ b/demo/src/main/java/tileview/demo/LargeImageTileViewActivity.java @@ -13,9 +13,9 @@ public void onCreate( Bundle savedInstanceState ) { // multiple references TileView tileView = getTileView(); - - // by disabling transitions, we won't see a flicker of background color when moving between tile sets - tileView.setTransitionsEnabled( false ); + + // let the image explode + tileView.setScaleLimits( 0, 2 ); // size of original image at 100% mScale tileView.setSize( 2835, 4289 ); diff --git a/demo/src/main/java/tileview/demo/RealMapInternetTileViewActivity.java b/demo/src/main/java/tileview/demo/RealMapInternetTileViewActivity.java index 4f132374..090b9bd9 100644 --- a/demo/src/main/java/tileview/demo/RealMapInternetTileViewActivity.java +++ b/demo/src/main/java/tileview/demo/RealMapInternetTileViewActivity.java @@ -1,12 +1,27 @@ package tileview.demo; +import android.graphics.CornerPathEffect; +import android.graphics.Paint; import android.os.Bundle; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.View; +import android.widget.ImageView; import com.qozix.tileview.TileView; +import com.qozix.tileview.markers.MarkerLayout; + +import java.util.ArrayList; import tileview.demo.provider.BitmapProviderPicasso; public class RealMapInternetTileViewActivity extends TileViewActivity { + + public static final double NORTH_WEST_LATITUDE = 39.9639998777094; + public static final double NORTH_WEST_LONGITUDE = -75.17261900652977; + public static final double SOUTH_EAST_LATITUDE = 39.93699709962642; + public static final double SOUTH_EAST_LONGITUDE = -75.12462846235614; + @Override public void onCreate( Bundle savedInstanceState ) { @@ -15,28 +30,156 @@ public void onCreate( Bundle savedInstanceState ) { // multiple references TileView tileView = getTileView(); + tileView.addDetailLevel( 0.0125f, "https://raw.githubusercontent.com/moagrius/TileView/master/demo/src/main/assets/tiles/map/phi-62500-%d_%d.jpg" ); + tileView.addDetailLevel( 0.2500f, "https://raw.githubusercontent.com/moagrius/TileView/master/demo/src/main/assets/tiles/map/phi-125000-%d_%d.jpg" ); + tileView.addDetailLevel( 0.5000f, "https://raw.githubusercontent.com/moagrius/TileView/master/demo/src/main/assets/tiles/map/phi-250000-%d_%d.jpg" ); + tileView.addDetailLevel( 1.0000f, "https://raw.githubusercontent.com/moagrius/TileView/master/demo/src/main/assets/tiles/map/phi-500000-%d_%d.jpg" ); + // simple http provider tileView.setBitmapProvider( new BitmapProviderPicasso() ); - // by disabling transitions, we won't see a flicker of background color when moving between tile sets - tileView.setTransitionsEnabled( false ); - // size and geolocation tileView.setSize( 8967, 6726 ); + // markers should align to the coordinate along the horizontal center and vertical bottom + tileView.setMarkerAnchorPoints( -0.5f, -1.0f ); + + // render asap + tileView.setShouldRenderWhilePanning( true ); + + // provide the corner coordinates for relative positioning + tileView.defineBounds( + NORTH_WEST_LONGITUDE, + NORTH_WEST_LATITUDE, + SOUTH_EAST_LONGITUDE, + SOUTH_EAST_LATITUDE + ); + + // get metrics for programmatic DP + DisplayMetrics metrics = getResources().getDisplayMetrics(); + + // get the default paint and style it. the same effect could be achieved by passing a custom Paint instnace + Paint paint = tileView.getDefaultPathPaint(); + + // add markers for all the points + for( double[] point : points ) { + // any view will do... + ImageView marker = new ImageView( this ); + // save the coordinate for centering and callout positioning + marker.setTag( point ); + // give it a standard marker icon - this indicator points down and is centered, so we'll use appropriate anchors + marker.setImageResource( Math.random() < 0.75 ? R.drawable.map_marker_normal : R.drawable.map_marker_featured ); + // on tap show further information about the area indicated + // this could be done using a OnClickListener, which is a little more "snappy", since + // MarkerTapListener uses GestureDetector.onSingleTapConfirmed, which has a delay of 300ms to + // confirm it's not the start of a double-tap. But this would consume the touch event and + // interrupt dragging + tileView.getMarkerLayout().setMarkerTapListener( markerTapListener ); + // add it to the view tree + tileView.addMarker( marker, point[0], point[1], null, null ); + } + + // let's start off framed to the center of all points + double x = 0; + double y = 0; + for( double[] point : points ) { + x = x + point[0]; + y = y + point[1]; + } + int size = points.size(); + x = x / size; + y = y / size; + frameTo( x, y ); + + // dress up the path effects and draw it between some points + paint.setShadowLayer( + TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 4, metrics ), + TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 2, metrics ), + TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 2, metrics ), + 0x66000000 + ); + paint.setStrokeWidth( TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 5, metrics ) ); + paint.setPathEffect( + new CornerPathEffect( + TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 5, metrics ) + ) + ); + tileView.drawPath( points.subList( 1, 5 ), null ); + // we won't use a downsample here, so color it similarly to tiles tileView.setBackgroundColor( 0xFFe7e7e7 ); - tileView.addDetailLevel( 0.0125f, "https://raw.githubusercontent.com/moagrius/TileView/master/demo/src/main/assets/tiles/map/phi-62500-%d_%d.jpg" ); - tileView.addDetailLevel( 0.2500f, "https://raw.githubusercontent.com/moagrius/TileView/master/demo/src/main/assets/tiles/map/phi-125000-%d_%d.jpg" ); - tileView.addDetailLevel( 0.5000f, "https://raw.githubusercontent.com/moagrius/TileView/master/demo/src/main/assets/tiles/map/phi-250000-%d_%d.jpg" ); - tileView.addDetailLevel( 1.0000f, "https://raw.githubusercontent.com/moagrius/TileView/master/demo/src/main/assets/tiles/map/phi-500000-%d_%d.jpg" ); + // this will result in a lot more network work, but should be a better UX + // set to false to minimize work on the wire + tileView.setShouldRenderWhilePanning( true ); + + // since there's going to be significant time between tiles loaded over http, + // using a low res version of the image stretched to fill all available space can + // create the illusion of progressive rendering + ImageView downSample = new ImageView( this ); + downSample.setImageResource( R.drawable.downsample ); + tileView.addView( downSample, 0 ); + + } + + private MarkerLayout.MarkerTapListener markerTapListener = new MarkerLayout.MarkerTapListener() { - // let's use 0-1 positioning... - tileView.defineBounds( 0, 0, 1, 1 ); + @Override + public void onMarkerTap( View view, int x, int y ) { + // get reference to the TileView + TileView tileView = getTileView(); + // we saved the coordinate in the marker's tag + double[] position = (double[]) view.getTag(); + // lets center the screen to that coordinate + tileView.slideToAndCenter( position[0], position[1] ); + // create a simple callout + SampleCallout callout = new SampleCallout( view.getContext() ); + // add it to the view tree at the same position and offset as the marker that invoked it + tileView.addCallout( callout, position[0], position[1], -0.5f, -1.0f ); + // a little sugar + callout.transitionIn(); + // stub out some text + callout.setTitle( "MAP CALLOUT" ); + callout.setSubtitle( "Info window at coordinate:\n" + position[1] + ", " + position[0] ); + } + }; - // frame to center - frameTo( 0.5, 0.5 ); + // a list of points to demonstrate markers and paths + private ArrayList points = new ArrayList<>(); + { + points.add( new double[] {-75.1489070, 39.9484760} ); + points.add( new double[] {-75.1494000, 39.9487722} ); + points.add( new double[] {-75.1468350, 39.9474180} ); + points.add( new double[] {-75.1472000, 39.9482000} ); + points.add( new double[] {-75.1437980, 39.9508290} ); + points.add( new double[] {-75.1479650, 39.9523130} ); + points.add( new double[] {-75.1445500, 39.9472960} ); + points.add( new double[] {-75.1506100, 39.9490630} ); + points.add( new double[] {-75.1521278, 39.9508083} ); + points.add( new double[] {-75.1477600, 39.9475320} ); + points.add( new double[] {-75.1503800, 39.9489900} ); + points.add( new double[] {-75.1464200, 39.9482000} ); + points.add( new double[] {-75.1464850, 39.9498500} ); + points.add( new double[] {-75.1487030, 39.9524300} ); + points.add( new double[] {-75.1500167, 39.9488750} ); + points.add( new double[] {-75.1458360, 39.9479700} ); + points.add( new double[] {-75.1498222, 39.9515389} ); + points.add( new double[] {-75.1501990, 39.9498900} ); + points.add( new double[] {-75.1460060, 39.9474210} ); + points.add( new double[] {-75.1490230, 39.9533960} ); + points.add( new double[] {-75.1471980, 39.9485350} ); + points.add( new double[] {-75.1493500, 39.9490200} ); + points.add( new double[] {-75.1500910, 39.9503850} ); + points.add( new double[] {-75.1483930, 39.9485040} ); + points.add( new double[] {-75.1517260, 39.9473720} ); + points.add( new double[] {-75.1525630, 39.9471360} ); + points.add( new double[] {-75.1438400, 39.9473390} ); + points.add( new double[] {-75.1468240, 39.9495400} ); + points.add( new double[] {-75.1466410, 39.9499900} ); + points.add( new double[] {-75.1465050, 39.9501110} ); + points.add( new double[] {-75.1473460, 39.9436200} ); + points.add( new double[] {-75.1501570, 39.9480430} ); } + } diff --git a/demo/src/main/java/tileview/demo/RealMapTileViewActivity.java b/demo/src/main/java/tileview/demo/RealMapTileViewActivity.java index 2112179b..35580b90 100644 --- a/demo/src/main/java/tileview/demo/RealMapTileViewActivity.java +++ b/demo/src/main/java/tileview/demo/RealMapTileViewActivity.java @@ -1,10 +1,6 @@ package tileview.demo; -import android.graphics.CornerPathEffect; -import android.graphics.Paint; import android.os.Bundle; -import android.util.DisplayMetrics; -import android.util.TypedValue; import android.view.View; import android.widget.ImageView; @@ -50,27 +46,6 @@ public void onCreate( Bundle savedInstanceState ) { SOUTH_EAST_LATITUDE ); - // get the default paint and style it. the same effect could be achieved by passing a custom Paint instnace - Paint paint = tileView.getDefaultPathPaint(); - - // get metrics for programmatic DP - DisplayMetrics metrics = getResources().getDisplayMetrics(); - - // dress up the path effects and draw it between some points - paint.setShadowLayer( - TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 4, metrics ), - TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 2, metrics ), - TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 2, metrics ), - 0x66000000 - ); - paint.setStrokeWidth( TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 5, metrics ) ); - paint.setPathEffect( - new CornerPathEffect( - TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 5, metrics ) - ) - ); - tileView.drawPath( points.subList( 1, 5 ), null ); - // add markers for all the points for( double[] point : points ) { // any view will do... @@ -107,12 +82,12 @@ public void onCreate( Bundle savedInstanceState ) { // start small and allow zoom tileView.setScale( 0.5f ); + // with padding, we might be fast enough to create the illusion of a seamless image + tileView.setViewportPadding( 256 ); + // we're running from assets, should be fairly fast decodes, go ahead and render asap tileView.setShouldRenderWhilePanning( true ); - // for quickly drawn tiles _without_ a downsample, transitions aren't particularly useful - tileView.setTransitionsEnabled( false ); - } private MarkerLayout.MarkerTapListener markerTapListener = new MarkerLayout.MarkerTapListener() { diff --git a/demo/src/main/java/tileview/demo/provider/BitmapProviderPicasso.java b/demo/src/main/java/tileview/demo/provider/BitmapProviderPicasso.java index a3db360a..506c69d6 100644 --- a/demo/src/main/java/tileview/demo/provider/BitmapProviderPicasso.java +++ b/demo/src/main/java/tileview/demo/provider/BitmapProviderPicasso.java @@ -8,8 +8,6 @@ import com.squareup.picasso.MemoryPolicy; import com.squareup.picasso.Picasso; -import java.io.IOException; - /** * @author Mike Dunn, 2/19/16. */ @@ -21,10 +19,10 @@ public Bitmap getBitmap( Tile tile, Context context ) { String formattedFileName = String.format( unformattedFileName, tile.getColumn(), tile.getRow() ); try { return Picasso.with( context ).load( formattedFileName ).memoryPolicy( MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE ).get(); - } catch( IOException e ) { - // probably couldn't find the file + } catch( Throwable t ) { + // probably couldn't find the file, maybe OOME } } return null; } -} +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index dc59ab80..9972ac36 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun Jul 24 20:21:42 CDT 2016 +#Sun Aug 07 15:38:33 CDT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip diff --git a/tileview/build.gradle b/tileview/build.gradle index 531e4fe1..0b045fd1 100644 --- a/tileview/build.gradle +++ b/tileview/build.gradle @@ -1,13 +1,13 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 22 + compileSdkVersion 23 buildToolsVersion "21.1.2" defaultConfig { minSdkVersion 11 - targetSdkVersion 22 + targetSdkVersion 23 versionCode 32 - versionName "2.1.8" + versionName "2.2.0" } buildTypes { release { diff --git a/tileview/src/main/java/com/qozix/tileview/TileView.java b/tileview/src/main/java/com/qozix/tileview/TileView.java index 71142586..566760c5 100644 --- a/tileview/src/main/java/com/qozix/tileview/TileView.java +++ b/tileview/src/main/java/com/qozix/tileview/TileView.java @@ -267,6 +267,13 @@ public void suppressRender() { mTileCanvasViewGroup.suppressRender(); } + /** + * Notify the TileView that it should resume tiles rendering. + */ + public void resumeRender() { + mTileCanvasViewGroup.resumeRender(); + } + /** * Sets a custom class to perform the getBitmap operation when tile bitmaps are requested for * tile images only. @@ -295,6 +302,7 @@ public void setTransitionsEnabled( boolean enabled ) { * * The default value is true. * + * @deprecated This value is no longer considered - bitmaps are always recycled when they're no longer used. * @param shouldRecycleBitmaps True if bitmaps should call Bitmap.recycle when they are removed from view. */ public void setShouldRecycleBitmaps( boolean shouldRecycleBitmaps ) { @@ -798,7 +806,7 @@ public void onScaleChanged( float scale, float previous ) { @Override public void onPanBegin( int x, int y, Origination origin ) { - suppressRender(); + } @Override @@ -813,8 +821,8 @@ public void onPanEnd( int x, int y, Origination origin ) { @Override public void onZoomBegin( float scale, Origination origin ) { - if( !mShouldUpdateDetailLevelWhileZooming ) { - mDetailLevelManager.lockDetailLevel(); + if ( origin == null ) { + mTileCanvasViewGroup.suppressRender(); } mDetailLevelManager.setScale( scale ); } @@ -826,7 +834,9 @@ public void onZoomUpdate( float scale, Origination origin ) { @Override public void onZoomEnd( float scale, Origination origin ) { - mDetailLevelManager.unlockDetailLevel(); + if ( origin == null ) { + mTileCanvasViewGroup.resumeRender(); + } mDetailLevelManager.setScale( scale ); requestRender(); } diff --git a/tileview/src/main/java/com/qozix/tileview/detail/DetailLevel.java b/tileview/src/main/java/com/qozix/tileview/detail/DetailLevel.java index edbdbe6e..8af26b66 100644 --- a/tileview/src/main/java/com/qozix/tileview/detail/DetailLevel.java +++ b/tileview/src/main/java/com/qozix/tileview/detail/DetailLevel.java @@ -19,6 +19,8 @@ public class DetailLevel implements Comparable { private StateSnapshot mLastStateSnapshot; + private Set mTilesVisibleInViewport = new HashSet<>(); + public DetailLevel( DetailLevelManager detailLevelManager, float scale, Object data, int tileWidth, int tileHeight ) { mDetailLevelManager = detailLevelManager; mScale = scale; @@ -27,6 +29,10 @@ public DetailLevel( DetailLevelManager detailLevelManager, float scale, Object d mTileHeight = tileHeight; } + public DetailLevelManager getDetailLevelManager() { + return mDetailLevelManager; + } + /** * Returns true if there has been a change, false otherwise. * @@ -62,20 +68,27 @@ public Set getVisibleTilesFromLastViewportComputation() { if( mLastStateSnapshot == null ) { throw new StateNotComputedException(); } - Set intersections = new HashSet<>(); + return mTilesVisibleInViewport; + } + + public boolean hasComputedState() { + return mLastStateSnapshot != null; + } + + public void computeVisibleTilesFromViewport() { + mTilesVisibleInViewport.clear(); for( int rowCurrent = mLastStateSnapshot.rowStart; rowCurrent < mLastStateSnapshot.rowEnd; rowCurrent++ ) { for( int columnCurrent = mLastStateSnapshot.columnStart; columnCurrent < mLastStateSnapshot.columnEnd; columnCurrent++ ) { Tile tile = new Tile( columnCurrent, rowCurrent, mTileWidth, mTileHeight, mData, this ); - intersections.add( tile ); + mTilesVisibleInViewport.add( tile ); } } - return intersections; } /** * Ensures that computeCurrentState will return true, indicating a change has occurred. */ - public void invalidate(){ + public void invalidate() { mLastStateSnapshot = null; } @@ -123,10 +136,10 @@ public int hashCode() { } public static class StateNotComputedException extends IllegalStateException { - public StateNotComputedException(){ - super("Grid has not been computed; " + + public StateNotComputedException() { + super( "Grid has not been computed; " + "you must call computeCurrentState at some point prior to calling " + - "getVisibleTilesFromLastViewportComputation."); + "getVisibleTilesFromLastViewportComputation." ); } } diff --git a/tileview/src/main/java/com/qozix/tileview/detail/DetailLevelManager.java b/tileview/src/main/java/com/qozix/tileview/detail/DetailLevelManager.java index 68fb8bbd..f7b9164c 100644 --- a/tileview/src/main/java/com/qozix/tileview/detail/DetailLevelManager.java +++ b/tileview/src/main/java/com/qozix/tileview/detail/DetailLevelManager.java @@ -26,6 +26,7 @@ public class DetailLevelManager { private Rect mViewport = new Rect(); private Rect mComputedViewport = new Rect(); + private Rect mComputedScaledViewport = new Rect(); private DetailLevel mCurrentDetailLevel; @@ -101,6 +102,16 @@ public Rect getComputedViewport() { return mComputedViewport; } + public Rect getComputedScaledViewport(float scale){ + mComputedScaledViewport.set( + (int) (mComputedViewport.left * scale), + (int) (mComputedViewport.top * scale), + (int) (mComputedViewport.right * scale), + (int) (mComputedViewport.bottom * scale) + ); + return mComputedScaledViewport; + } + /** * While the detail level is locked (after this method is invoked, and before unlockDetailLevel is invoked), * the DetailLevel will not change, and the current DetailLevel will be scaled beyond the normal @@ -118,6 +129,10 @@ public void unlockDetailLevel() { mDetailLevelLocked = false; } + public boolean getIsLocked() { + return mDetailLevelLocked; + } + public void resetDetailLevels() { mDetailLevelLinkedList.clear(); update(); diff --git a/tileview/src/main/java/com/qozix/tileview/paths/CompositePathView.java b/tileview/src/main/java/com/qozix/tileview/paths/CompositePathView.java index 263540b4..3fe0625d 100644 --- a/tileview/src/main/java/com/qozix/tileview/paths/CompositePathView.java +++ b/tileview/src/main/java/com/qozix/tileview/paths/CompositePathView.java @@ -43,7 +43,7 @@ public float getScale() { public void setScale( float scale ) { mScale = scale; - mMatrix.setScale( mScale, mScale ); // TODO: test this + mMatrix.setScale( mScale, mScale ); invalidate(); } diff --git a/tileview/src/main/java/com/qozix/tileview/tiles/Tile.java b/tileview/src/main/java/com/qozix/tileview/tiles/Tile.java old mode 100644 new mode 100755 index 85c69663..4b2738aa --- a/tileview/src/main/java/com/qozix/tileview/tiles/Tile.java +++ b/tileview/src/main/java/com/qozix/tileview/tiles/Tile.java @@ -4,27 +4,50 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.Rect; import android.view.animation.AnimationUtils; import com.qozix.tileview.detail.DetailLevel; +import com.qozix.tileview.geom.FloatMathHelper; import com.qozix.tileview.graphics.BitmapProvider; +import java.lang.ref.WeakReference; + public class Tile { + public enum State { + UNASSIGNED, + PENDING_DECODE, + DECODED + } + private static final int DEFAULT_TRANSITION_DURATION = 200; + private State mState = State.UNASSIGNED; + private int mWidth; private int mHeight; private int mLeft; private int mTop; + private int mRight; + private int mBottom; + + private float mProgress; private int mRow; private int mColumn; + private float mDetailLevelScale; + private Object mData; private Bitmap mBitmap; - public double renderTimestamp; + private Rect mIntrinsicRect = new Rect(); + private Rect mBaseRect = new Rect(); + private Rect mRelativeRect = new Rect(); + private Rect mScaledRect = new Rect(); + + public Long mRenderTimeStamp; private boolean mTransitionsEnabled; @@ -32,10 +55,10 @@ public class Tile { private Paint mPaint; - private TileCanvasView mParentTileCanvasView; - private DetailLevel mDetailLevel; + private WeakReference mTileRenderRunnableWeakReference; + public Tile( int column, int row, int width, int height, Object data, DetailLevel detailLevel ) { mRow = row; mColumn = column; @@ -43,8 +66,20 @@ public Tile( int column, int row, int width, int height, Object data, DetailLeve mHeight = height; mLeft = column * width; mTop = row * height; + mRight = mLeft + mWidth; + mBottom = mTop + mHeight; mData = data; mDetailLevel = detailLevel; + mDetailLevelScale = mDetailLevel.getScale(); + mIntrinsicRect.set( 0, 0, mWidth, mHeight ); + mBaseRect.set( mLeft, mTop, mRight, mBottom ); + mRelativeRect.set( + FloatMathHelper.unscale( mLeft, mDetailLevelScale ), + FloatMathHelper.unscale( mTop, mDetailLevelScale ), + FloatMathHelper.unscale( mRight, mDetailLevelScale ), + FloatMathHelper.unscale( mBottom, mDetailLevelScale ) + ); + mScaledRect.set( mRelativeRect ); } public int getWidth() { @@ -83,49 +118,103 @@ public boolean hasBitmap() { return mBitmap != null; } + public Rect getBaseRect() { + return mBaseRect; + } + + public Rect getRelativeRect() { + return mRelativeRect; + } + + /** + * @deprecated + * @return + */ + public float getRendered() { + return mProgress; + } + + /** + * @deprecated + */ + public void stampTime() { + // no op + } + + public Rect getScaledRect( float scale ) { + mScaledRect.set( + (int) (mRelativeRect.left * scale), + (int) (mRelativeRect.top * scale), + (int) (mRelativeRect.right * scale), + (int) (mRelativeRect.bottom * scale) + ); + return mScaledRect; + } + public void setTransitionDuration( int transitionDuration ) { mTransitionDuration = transitionDuration; } - public void stampTime() { - renderTimestamp = AnimationUtils.currentAnimationTimeMillis(); + public State getState() { + return mState; } - public void setTransitionsEnabled( boolean enabled ) { - mTransitionsEnabled = enabled; + public void setState( State state ) { + mState = state; } - public DetailLevel getDetailLevel() { - return mDetailLevel; + public void execute( TileRenderPoolExecutor tileRenderPoolExecutor ) { + if(mState != State.UNASSIGNED){ + return; + } + mState = State.PENDING_DECODE; + TileRenderRunnable runnable = new TileRenderRunnable(); + mTileRenderRunnableWeakReference = new WeakReference<>( runnable ); + runnable.setTile( this ); + runnable.setTileRenderPoolExecutor( tileRenderPoolExecutor ); + tileRenderPoolExecutor.execute( runnable ); } - public float getRendered() { + public void computeProgress(){ if( !mTransitionsEnabled ) { - return 1; + return; } - double now = AnimationUtils.currentAnimationTimeMillis(); - double ellapsed = now - renderTimestamp; - float progress = (float) Math.min( 1, ellapsed / mTransitionDuration ); - if( progress == 1 ) { + if( mRenderTimeStamp == null ) { + mRenderTimeStamp = AnimationUtils.currentAnimationTimeMillis(); + mProgress = 0; + return; + } + double elapsed = AnimationUtils.currentAnimationTimeMillis() - mRenderTimeStamp; + mProgress = (float) Math.min( 1, elapsed / mTransitionDuration ); + if( mProgress == 1f ) { + mRenderTimeStamp = null; mTransitionsEnabled = false; } - return progress; + } + + public void setTransitionsEnabled( boolean enabled ) { + mTransitionsEnabled = enabled; + if( enabled ) { + mProgress = 0f; + } + } + + public DetailLevel getDetailLevel() { + return mDetailLevel; } public boolean getIsDirty() { - return mTransitionsEnabled && getRendered() < 1f; + return mTransitionsEnabled && mProgress < 1f; } public Paint getPaint() { if( !mTransitionsEnabled ) { - return null; + return mPaint = null; } if( mPaint == null ) { mPaint = new Paint(); } - float rendered = getRendered(); - int opacity = (int) (rendered * 255); - mPaint.setAlpha( opacity ); + mPaint.setAlpha( (int) (255 * mProgress) ); return mPaint; } @@ -134,35 +223,41 @@ void generateBitmap( Context context, BitmapProvider bitmapProvider ) { return; } mBitmap = bitmapProvider.getBitmap( this, context ); + mState = State.DECODED; } - void setParentTileCanvasView( TileCanvasView tileCanvasView ) { - mParentTileCanvasView = tileCanvasView; - } - - void destroy( boolean shouldRecycle ) { - destroy( shouldRecycle, true ); + /** + * Deprecated + * @param b + */ + void destroy( boolean b ) { + reset(); } - void destroy( boolean shouldRecycle, boolean shouldRemove ) { - if( shouldRecycle && mBitmap != null && !mBitmap.isRecycled() ) { + void reset() { + if( mState == State.PENDING_DECODE ) { + if ( mTileRenderRunnableWeakReference != null ) { + TileRenderRunnable runnable = mTileRenderRunnableWeakReference.get(); + if( runnable != null ) { + runnable.cancel( true ); + } + } + } + mState = State.UNASSIGNED; + mRenderTimeStamp = null; + if( mBitmap != null && !mBitmap.isRecycled() ) { mBitmap.recycle(); } mBitmap = null; - if( shouldRemove && mParentTileCanvasView != null ) { - mParentTileCanvasView.removeTile( this ); - } } /** * @param canvas The canvas the tile's bitmap should be drawn into - * @return True if the tile is dirty (drawing output has changed and needs parent validation) */ - boolean draw( Canvas canvas ) { + public void draw( Canvas canvas ) { if( mBitmap != null ) { - canvas.drawBitmap( mBitmap, mLeft, mTop, getPaint() ); + canvas.drawBitmap( mBitmap, mIntrinsicRect, mRelativeRect, getPaint() ); } - return getIsDirty(); } @Override @@ -176,7 +271,7 @@ public int hashCode() { @Override public boolean equals( Object o ) { - if( this == o ){ + if( this == o ) { return true; } if( o instanceof Tile ) { @@ -188,4 +283,8 @@ public boolean equals( Object o ) { return false; } + public String toShortString(){ + return mColumn + ":" + mRow; + } + } diff --git a/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasView.java b/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasView.java deleted file mode 100644 index f6d2fb58..00000000 --- a/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasView.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.qozix.tileview.tiles; - -import android.content.Context; -import android.graphics.Canvas; -import android.view.View; - -import java.util.HashSet; -import java.util.Set; - -public class TileCanvasView extends View { - - private float mScale = 1; - - private Set mTiles = new HashSet<>(); - - private TileCanvasDrawListener mTileCanvasDrawListener; - - private boolean mHasHadPendingUpdatesSinceLastCompleteDraw; - - public TileCanvasView( Context context ) { - super( context ); - } - - public Set getTiles(){ - return mTiles; - } - - public void setScale( float factor ) { - mScale = factor; - invalidate(); - } - - public float getScale() { - return mScale; - } - - public void setTileCanvasDrawListener( TileCanvasDrawListener tileCanvasDrawListener ) { - mTileCanvasDrawListener = tileCanvasDrawListener; - } - - public void addTile( Tile tile ) { - if( !mTiles.contains( tile ) ) { - mTiles.add( tile ); - tile.setParentTileCanvasView( this ); - invalidate(); - } - } - - public void removeTile( Tile tile ) { - if( mTiles.contains( tile ) ) { - mTiles.remove( tile ); - tile.setParentTileCanvasView( null ); - invalidate(); - } - } - - public void clearTiles( boolean shouldRecycle ) { - for( Tile tile : mTiles ) { - tile.destroy( shouldRecycle, false ); - } - mTiles.clear(); - invalidate(); - } - - /** - * Draw tile bitmaps into the surface canvas displayed by this View. - * @param canvas The Canvas instance to draw tile bitmaps into. - * @return True if there are incomplete tile transitions pending, false otherwise. - */ - private boolean drawTiles( Canvas canvas ) { - boolean pending = false; - for( Tile tile : mTiles ) { - pending = tile.draw( canvas ) || pending; - } - return pending; - } - - /** - * During a draw operation, if any tiles are transitioning in, the operation is considered pending, - * and another redraw is requested immediately (via invalidate). - * - * NOTE: the invalidate invocation in this method should not be necessary, since the - * TileCanvasViewGroup that contains this View should also be listening for onDrawPending, - * at which time it will call invalidate on itself. - * - * @param pending True if tile transitions states are not complete, and an immediate redraw is required. - */ - private void handleDrawState( boolean pending ) { - if( pending ) { - invalidate(); - mHasHadPendingUpdatesSinceLastCompleteDraw = true; - if( mTileCanvasDrawListener != null ) { - mTileCanvasDrawListener.onDrawPending( this ); - } - } else { - if( mHasHadPendingUpdatesSinceLastCompleteDraw ) { - mHasHadPendingUpdatesSinceLastCompleteDraw = false; - if( mTileCanvasDrawListener != null ) { - mTileCanvasDrawListener.onDrawComplete( this ); - } - } - } - } - - @Override - public void onDraw( Canvas canvas ) { - super.onDraw( canvas ); - canvas.save(); - canvas.scale( mScale, mScale ); - boolean pending = drawTiles( canvas ); - canvas.restore(); - handleDrawState( pending ); - } - - /** - * Interface definition for callbacks to be invoked when drawing is complete. - * A "pending" draw is one that indicates tile transitions states are still pending and - * and immediate redraw should be requested. - * A "complete" draw callback will only be invoked if transitions are enabled, and will occur - * the first time a "complete" draw is complete after a "pending" draw. - */ - public interface TileCanvasDrawListener { - void onDrawComplete( TileCanvasView tileCanvasView ); - void onDrawPending( TileCanvasView tileCanvasView ); - } - -} diff --git a/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasViewGroup.java b/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasViewGroup.java old mode 100644 new mode 100755 index 4adf68f5..2f63835b --- a/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasViewGroup.java +++ b/tileview/src/main/java/com/qozix/tileview/tiles/TileCanvasViewGroup.java @@ -1,21 +1,28 @@ package com.qozix.tileview.tiles; import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.Region; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.view.ViewGroup; import com.qozix.tileview.detail.DetailLevel; import com.qozix.tileview.graphics.BitmapProvider; import com.qozix.tileview.graphics.BitmapProviderAssets; -import com.qozix.tileview.widgets.ScalingLayout; import java.lang.ref.WeakReference; -import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.Set; -public class TileCanvasViewGroup extends ScalingLayout implements TileCanvasView.TileCanvasDrawListener { +/** + * This class extends ViewGroup for legacy reasons, and may be changed to extend View at + * some future point; consider all ViewGroup methods deprecated. + */ +public class TileCanvasViewGroup extends ViewGroup { private static final int RENDER_FLAG = 1; @@ -24,13 +31,13 @@ public class TileCanvasViewGroup extends ScalingLayout implements TileCanvasView private static final int DEFAULT_TRANSITION_DURATION = 200; + private float mScale = 1; + private BitmapProvider mBitmapProvider; - private HashMap mTileCanvasViewHashMap = new HashMap<>(); private DetailLevel mDetailLevelToRender; - private DetailLevel mLastRequestedDetailLevel; private DetailLevel mLastRenderedDetailLevel; - private TileCanvasView mCurrentTileCanvasView; + private boolean mRenderIsCancelled = false; private boolean mRenderIsSuppressed = false; @@ -43,14 +50,19 @@ public class TileCanvasViewGroup extends ScalingLayout implements TileCanvasView private TileRenderThrottleHandler mTileRenderThrottleHandler; private TileRenderListener mTileRenderListener; + private TileRenderThrowableListener mTileRenderThrowableListener; private int mRenderBuffer = DEFAULT_RENDER_BUFFER; private TileRenderPoolExecutor mTileRenderPoolExecutor; private Set mTilesInCurrentViewport = new HashSet<>(); - private Set mTilesNotInCurrentViewport = new HashSet<>(); - private Set mTilesAlreadyRendered = new HashSet<>(); + private Set mPreviouslyDrawnTiles = new HashSet<>(); + private Set mDecodedTilesInCurrentViewport = new HashSet<>(); + + private Region mDirtyRegion = new Region(); + + private boolean mHasInvalidatedOnCleanOnce; public TileCanvasViewGroup( Context context ) { super( context ); @@ -59,6 +71,24 @@ public TileCanvasViewGroup( Context context ) { mTileRenderPoolExecutor = new TileRenderPoolExecutor(); } + @Override + protected void onLayout( boolean changed, int l, int t, int r, int b ) { + + } + + public void setScale( float factor ) { + mScale = factor; + invalidate(); + } + + public float getScale() { + return mScale; + } + + public float getInvertedScale() { + return 1f / mScale; + } + public boolean getTransitionsEnabled() { return mTransitionsEnabled; } @@ -75,7 +105,7 @@ public void setTransitionDuration( int duration ) { mTransitionDuration = duration; } - public BitmapProvider getBitmapProvider(){ + public BitmapProvider getBitmapProvider() { if( mBitmapProvider == null ) { mBitmapProvider = new BitmapProviderAssets(); } @@ -98,14 +128,25 @@ public void setRenderBuffer( int renderBuffer ) { mRenderBuffer = renderBuffer; } + /** + * @return True if tile bitmaps should be recycled. + * @deprecated This value is no longer considered - bitmaps are always recycled when they're no longer used. + */ public boolean getShouldRecycleBitmaps() { return mShouldRecycleBitmaps; } + /** + * @param shouldRecycleBitmaps True if tile bitmaps should be recycled. + * @deprecated This value is no longer considered - bitmaps are always recycled when they're no longer used. + */ public void setShouldRecycleBitmaps( boolean shouldRecycleBitmaps ) { mShouldRecycleBitmaps = shouldRecycleBitmaps; } + public void setTileRenderThrowableListener( TileRenderThrowableListener tileRenderThrowableListener ) { + mTileRenderThrowableListener = tileRenderThrowableListener; + } /** * The layout dimensions supplied to this ViewGroup will be exactly as large as the scaled @@ -116,7 +157,6 @@ public void setShouldRecycleBitmaps( boolean shouldRecycleBitmaps ) { public void requestRender() { mRenderIsCancelled = false; - mRenderIsSuppressed = false; if( mDetailLevelToRender == null ) { return; } @@ -131,7 +171,7 @@ public void requestRender() { */ public void cancelRender() { mRenderIsCancelled = true; - if( mTileRenderPoolExecutor != null ){ + if( mTileRenderPoolExecutor != null ) { mTileRenderPoolExecutor.cancel(); } } @@ -143,103 +183,216 @@ public void suppressRender() { mRenderIsSuppressed = true; } - public void updateTileSet( DetailLevel detailLevel ) { - mDetailLevelToRender = detailLevel; - if( mDetailLevelToRender == null ) { - return; - } - if( mDetailLevelToRender.equals( mLastRequestedDetailLevel ) ) { - return; - } - mLastRequestedDetailLevel = mDetailLevelToRender; - mCurrentTileCanvasView = getCurrentTileCanvasView(); - mCurrentTileCanvasView.bringToFront(); - cancelRender(); - requestRender(); + /** + * Enables new render tasks to start. + */ + public void resumeRender() { + mRenderIsSuppressed = false; } + /** + * Returns true if the TileView has threads currently decoding tile Bitmaps. + * + * @return True if the TileView has threads currently decoding tile Bitmaps. + */ public boolean getIsRendering() { return mIsRendering; } + /** + * Clears existing tiles and cancels any existing render tasks. + */ public void clear() { suppressRender(); cancelRender(); mTilesInCurrentViewport.clear(); - mCurrentTileCanvasView.clearTiles( mShouldRecycleBitmaps ); + invalidate(); } /** - * Effectively adds any new tiles, without replacing existing tiles, and removes those not in passed set. - * @param recentlyComputedVisibleTileSet Tile Set that should be visible, based on DetailLevel inspection of viewport size and position. + * This function is now a no-op + * + * @param recentlyComputedVisibleTileSet + * @deprecated */ - public void reconcile( Set recentlyComputedVisibleTileSet ){ + public void reconcile( Set recentlyComputedVisibleTileSet ) { + // noop + } + + void renderTiles() { + if( !mRenderIsCancelled && !mRenderIsSuppressed && mDetailLevelToRender != null ) { + beginRenderTask(); + } + } + + private Rect getComputedViewport() { + if( mDetailLevelToRender == null ) { + return null; + } + return mDetailLevelToRender.getDetailLevelManager().getComputedScaledViewport( getInvertedScale() ); + } + + private boolean establishDirtyRegion() { + boolean shouldInvalidate = false; + mDirtyRegion.set( getComputedViewport() ); for( Tile tile : mTilesInCurrentViewport ) { - if( !recentlyComputedVisibleTileSet.contains( tile ) ) { - mTilesNotInCurrentViewport.add( tile ); + if( tile.getState() == Tile.State.DECODED ) { + tile.computeProgress(); + mDecodedTilesInCurrentViewport.add( tile ); + if( tile.getIsDirty() ) { + shouldInvalidate = true; + } else { + mDirtyRegion.op( tile.getRelativeRect(), Region.Op.DIFFERENCE ); + } + } + } + return shouldInvalidate; + } + + private boolean drawPreviousTiles( Canvas canvas ) { + boolean shouldInvalidate = false; + Iterator tilesFromLastDetailLevelIterator = mPreviouslyDrawnTiles.iterator(); + while( tilesFromLastDetailLevelIterator.hasNext() ) { + Tile tile = tilesFromLastDetailLevelIterator.next(); + Rect rect = tile.getRelativeRect(); + if( mDirtyRegion.quickReject( rect ) ) { + tilesFromLastDetailLevelIterator.remove(); + } else { + tile.computeProgress(); + tile.draw( canvas ); + shouldInvalidate |= tile.getIsDirty(); } } - mTilesInCurrentViewport.addAll( recentlyComputedVisibleTileSet ); - mTilesInCurrentViewport.removeAll( mTilesNotInCurrentViewport ); - mTilesNotInCurrentViewport.clear(); + return shouldInvalidate; } - private float getCurrentDetailLevelScale() { - if( mDetailLevelToRender != null ) { - return mDetailLevelToRender.getScale(); + private boolean drawAndClearCurrentDecodedTiles( Canvas canvas ) { + boolean shouldInvalidate = false; + for( Tile tile : mDecodedTilesInCurrentViewport ) { + // these tiles should already have progress computed by the time they get here + tile.draw( canvas ); + shouldInvalidate |= tile.getIsDirty(); } - return 1; + mDecodedTilesInCurrentViewport.clear(); + return shouldInvalidate; } - private TileCanvasView getCurrentTileCanvasView() { - float levelScale = getCurrentDetailLevelScale(); - if( mTileCanvasViewHashMap.containsKey( levelScale ) ) { - return mTileCanvasViewHashMap.get( levelScale ); + private void handleInvalidation( boolean shouldInvalidate ) { + if( shouldInvalidate ) { + // there's more work to do, partially opaque tiles were drawn + mHasInvalidatedOnCleanOnce = false; + invalidate(); + } else { + // if all tiles were fully opaque, we need another pass to clear our tiles from last level + if( !mHasInvalidatedOnCleanOnce ) { + mHasInvalidatedOnCleanOnce = true; + invalidate(); + } } - TileCanvasView tileGroup = new TileCanvasView( getContext() ); - tileGroup.setTileCanvasDrawListener( this ); - tileGroup.setScale( 1 / levelScale ); - mTileCanvasViewHashMap.put( levelScale, tileGroup ); - addView( tileGroup ); - return tileGroup; } - void renderTiles() { - if( !mRenderIsCancelled && !mRenderIsSuppressed && mDetailLevelToRender != null ) { - beginRenderTask(); + private void drawTilesWithoutConsideringPreviouslyDrawnLevel( Canvas canvas ) { + boolean shouldInvalidate = false; + for( Tile tile : mTilesInCurrentViewport ) { + if( tile.getState() == Tile.State.DECODED ) { + tile.computeProgress(); + tile.draw( canvas ); + shouldInvalidate |= tile.getIsDirty(); + } } + handleInvalidation( shouldInvalidate ); + } + + private void drawTilesConsideringPreviouslyDrawnLevel( Canvas canvas ) { + // compute states, populate opaque region + boolean shouldInvalidate = establishDirtyRegion(); + // draw any previous tiles that are in viewport and not under full opaque current tiles + shouldInvalidate |= drawPreviousTiles( canvas ); + // draw the current tile set + shouldInvalidate |= drawAndClearCurrentDecodedTiles( canvas ); + // depending on transition states and previous tile draw ops, add'l invalidation might be needed + handleInvalidation( shouldInvalidate ); + } + + /** + * Draw tile bitmaps into the surface canvas displayed by this View. + * + * @param canvas The Canvas instance to draw tile bitmaps into. + */ + private void drawTiles( Canvas canvas ) { + if( mPreviouslyDrawnTiles.size() > 0 ) { + drawTilesConsideringPreviouslyDrawnLevel( canvas ); + } else { + drawTilesWithoutConsideringPreviouslyDrawnLevel( canvas ); + } + } + + public void updateTileSet( DetailLevel detailLevel ) { + if( detailLevel == null ) { + return; + } + if( detailLevel.equals( mDetailLevelToRender ) ) { + return; + } + cancelRender(); + markTilesAsPrevious(); + mDetailLevelToRender = detailLevel; + requestRender(); + } + + private void markTilesAsPrevious() { + for( Tile tile : mTilesInCurrentViewport ) { + if( tile.getState() == Tile.State.DECODED ) { + mPreviouslyDrawnTiles.add( tile ); + } + } + mTilesInCurrentViewport.clear(); } private void beginRenderTask() { + // if visible columns and rows are same as previously computed, fast-fail boolean changed = mDetailLevelToRender.computeCurrentState(); if( !changed && mDetailLevelToRender.equals( mLastRenderedDetailLevel ) ) { return; } - Set visibleTiles = mDetailLevelToRender.getVisibleTilesFromLastViewportComputation(); - reconcile( visibleTiles ); - if( mTileRenderPoolExecutor != null ){ - mTileRenderPoolExecutor.queue( this, getRenderSet() ); + // determine tiles are mathematically within the current viewport; force re-computation + mDetailLevelToRender.computeVisibleTilesFromViewport(); + // get rid of anything outside, use previously computed intersections + cleanup(); + // are there any new tiles the Executor isn't already aware of? + boolean wereTilesAdded = mTilesInCurrentViewport.addAll( mDetailLevelToRender.getVisibleTilesFromLastViewportComputation() ); + // if so, start up a new batch + if( wereTilesAdded ) { + mTileRenderPoolExecutor.queue( this, mTilesInCurrentViewport ); } } - private void clearOutOfViewportTiles(){ - Set condemned = new HashSet<>( mTilesAlreadyRendered ); - condemned.removeAll( mTilesInCurrentViewport ); - mTilesAlreadyRendered.removeAll( condemned ); - for( Tile tile : condemned ) { - tile.destroy( mShouldRecycleBitmaps ); + /** + * This should seldom be necessary, as it's built into beginRenderTask + */ + public void cleanup() { + if( mDetailLevelToRender == null || !mDetailLevelToRender.hasComputedState() ) { + return; + } + // these tiles are mathematically within the current viewport, and should be already computed + Set recentlyComputedVisibleTileSet = mDetailLevelToRender.getVisibleTilesFromLastViewportComputation(); + // use an iterator to avoid concurrent modification + Iterator tilesInCurrentViewportIterator = mTilesInCurrentViewport.iterator(); + while( tilesInCurrentViewportIterator.hasNext() ) { + Tile tile = tilesInCurrentViewportIterator.next(); + // this tile was visible previously, but is no longer, destroy and de-list it + if( !recentlyComputedVisibleTileSet.contains( tile ) ) { + tile.reset(); + tilesInCurrentViewportIterator.remove(); + } } - mCurrentTileCanvasView.invalidate(); } - private void cleanup() { - clearOutOfViewportTiles(); - for( TileCanvasView tileGroup : mTileCanvasViewHashMap.values() ) { - if( mCurrentTileCanvasView != tileGroup ) { - tileGroup.clearTiles( mShouldRecycleBitmaps ); - } + // this tile has been decoded by the time it gets passed here + void addTileToCanvas( final Tile tile ) { + if( mTilesInCurrentViewport.contains( tile ) ) { + invalidate(); } - invalidate(); } void onRenderTaskPreExecute() { @@ -261,30 +414,9 @@ void onRenderTaskPostExecute() { mTileRenderThrottleHandler.post( mRenderPostExecuteRunnable ); } - Set getRenderSet() { - Set renderSet = new HashSet<>( mTilesInCurrentViewport ); - renderSet.removeAll( mTilesAlreadyRendered ); - return renderSet; - } - - void generateTileBitmap( Tile tile ) { - tile.generateBitmap( getContext(), getBitmapProvider() ); - } - - void addTileToCurrentTileCanvasView( final Tile tile ) { - if( !mTilesInCurrentViewport.contains( tile ) ) { - return; - } - tile.setTransitionsEnabled( mTransitionsEnabled ); - tile.setTransitionDuration( mTransitionDuration ); - tile.stampTime(); - mTilesAlreadyRendered.add( tile ); - mCurrentTileCanvasView.addTile( tile ); - } - void handleTileRenderException( Throwable throwable ) { - if( throwable instanceof OutOfMemoryError ){ - cleanup(); + if( mTileRenderThrowableListener != null ) { + mTileRenderThrowableListener.onRenderThrow( throwable ); } } @@ -292,30 +424,23 @@ boolean getRenderIsCancelled() { return mRenderIsCancelled; } - @Override - public void onDrawComplete( TileCanvasView tileCanvasView ) { - if( mTransitionsEnabled && tileCanvasView == mCurrentTileCanvasView ) { - cleanup(); - } - } - - @Override - public void onDrawPending( TileCanvasView tileCanvasView ) { - invalidate(); - } - - public void destroy(){ + public void destroy() { mTileRenderPoolExecutor.shutdownNow(); clear(); - for( TileCanvasView tileGroup : mTileCanvasViewHashMap.values() ) { - tileGroup.clearTiles( mShouldRecycleBitmaps ); - } - mTileCanvasViewHashMap.clear(); if( !mTileRenderThrottleHandler.hasMessages( RENDER_FLAG ) ) { mTileRenderThrottleHandler.removeMessages( RENDER_FLAG ); } } + @Override + public void onDraw( Canvas canvas ) { + super.onDraw( canvas ); + canvas.save(); + canvas.scale( mScale, mScale ); + drawTiles( canvas ); + canvas.restore(); + } + private static class TileRenderThrottleHandler extends Handler { private final WeakReference mTileCanvasViewGroupWeakReference; @@ -343,19 +468,22 @@ public interface TileRenderListener { void onRenderComplete(); } + // ideally this would be part of TileRenderListener, but that's a breaking change + public interface TileRenderThrowableListener { + void onRenderThrow( Throwable throwable ); + } + // This runnable is required to run on UI thread - private Runnable mRenderPostExecuteRunnable = new Runnable() { + private Runnable mRenderPostExecuteRunnable = new Runnable() { @Override public void run() { - if ( !mTransitionsEnabled ) { - cleanup(); - } + cleanup(); if( mTileRenderListener != null ) { mTileRenderListener.onRenderComplete(); } mLastRenderedDetailLevel = mDetailLevelToRender; - invalidate(); requestRender(); } }; + } diff --git a/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderHandler.java b/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderHandler.java old mode 100644 new mode 100755 index eb170ede..c71b5623 --- a/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderHandler.java +++ b/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderHandler.java @@ -70,7 +70,7 @@ public void handleMessage( Message message ) { tileCanvasViewGroup.handleTileRenderException( tileRenderRunnable.getThrowable() ); break; case RENDER_COMPLETE: - tileCanvasViewGroup.addTileToCurrentTileCanvasView( tile ); + tileCanvasViewGroup.addTileToCanvas( tile ); break; } } diff --git a/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderPoolExecutor.java b/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderPoolExecutor.java old mode 100644 new mode 100755 index 6ac5d5a1..51a85ed4 --- a/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderPoolExecutor.java +++ b/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderPoolExecutor.java @@ -1,8 +1,6 @@ package com.qozix.tileview.tiles; -import android.content.Context; - -import com.qozix.tileview.graphics.BitmapProvider; +import android.os.Handler; import java.lang.ref.WeakReference; import java.util.Set; @@ -36,8 +34,6 @@ public TileRenderPoolExecutor() { public void queue( TileCanvasViewGroup tileCanvasViewGroup, Set renderSet ) { mTileCanvasViewGroupWeakReference = new WeakReference<>( tileCanvasViewGroup ); mHandler.setTileCanvasViewGroup( tileCanvasViewGroup ); - final Context context = tileCanvasViewGroup.getContext(); - final BitmapProvider bitmapProvider = tileCanvasViewGroup.getBitmapProvider(); tileCanvasViewGroup.onRenderTaskPreExecute(); for( Runnable runnable : getQueue() ) { if( runnable instanceof TileRenderRunnable ) { @@ -46,14 +42,8 @@ public void queue( TileCanvasViewGroup tileCanvasViewGroup, Set renderSet continue; } Tile tile = tileRenderRunnable.getTile(); - if( tile == null ) { - continue; - } - if( renderSet.contains( tile ) ) { - renderSet.remove( tile ); - } else { - tileRenderRunnable.cancel( true ); - remove( tileRenderRunnable ); + if( tile != null && !renderSet.contains( tile ) ) { + tile.reset(); } } } @@ -61,13 +51,19 @@ public void queue( TileCanvasViewGroup tileCanvasViewGroup, Set renderSet if( isShutdownOrTerminating() ) { return; } - TileRenderRunnable runnable = new TileRenderRunnable(); - runnable.setTile( tile ); - runnable.setContext( context ); - runnable.setBitmapProvider( bitmapProvider ); - runnable.setHandler( mHandler ); - execute( runnable ); + tile.execute( this ); + } + } + + public Handler getHandler(){ + return mHandler; + } + + public TileCanvasViewGroup getTileCanvasViewGroup(){ + if( mTileCanvasViewGroupWeakReference == null ) { + return null; } + return mTileCanvasViewGroupWeakReference.get(); } private void broadcastCancel() { @@ -84,6 +80,10 @@ public void cancel() { if( runnable instanceof TileRenderRunnable ) { TileRenderRunnable tileRenderRunnable = (TileRenderRunnable) runnable; tileRenderRunnable.cancel( true ); + Tile tile = tileRenderRunnable.getTile(); + if( tile != null ) { + tile.reset(); + } } } getQueue().clear(); diff --git a/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderRunnable.java b/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderRunnable.java old mode 100644 new mode 100755 index 30a19d3d..92a629e3 --- a/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderRunnable.java +++ b/tileview/src/main/java/com/qozix/tileview/tiles/TileRenderRunnable.java @@ -1,12 +1,9 @@ package com.qozix.tileview.tiles; -import android.content.Context; import android.os.Handler; import android.os.Message; import android.os.Process; -import com.qozix.tileview.graphics.BitmapProvider; - import java.lang.ref.WeakReference; /** @@ -15,9 +12,7 @@ class TileRenderRunnable implements Runnable { private WeakReference mTileWeakReference; - private WeakReference mHandlerWeakReference; - private WeakReference mContextWeakReference; - private WeakReference mBitmapProviderWeakReference; + private WeakReference mTileRenderPoolExecutorWeakReference; private boolean mCancelled = false; private boolean mComplete = false; @@ -32,6 +27,12 @@ public boolean cancel( boolean mayInterrupt ) { } boolean cancelled = mCancelled; mCancelled = true; + if( mTileRenderPoolExecutorWeakReference != null ) { + TileRenderPoolExecutor tileRenderPoolExecutor = mTileRenderPoolExecutorWeakReference.get(); + if(tileRenderPoolExecutor != null){ + tileRenderPoolExecutor.remove( this ); + } + } return !cancelled; } @@ -43,37 +44,8 @@ public boolean isDone() { return mComplete; } - public void setHandler( Handler handler ) { - mHandlerWeakReference = new WeakReference<>( handler ); - } - - public Handler getHandler() { - if( mHandlerWeakReference == null ) { - return null; - } - return mHandlerWeakReference.get(); - } - - public void setContext( Context context ) { - mContextWeakReference = new WeakReference<>( context ); - } - - public void setBitmapProvider( BitmapProvider bitmapProvider ) { - mBitmapProviderWeakReference = new WeakReference<>( bitmapProvider ); - } - - public Context getContext() { - if( mContextWeakReference == null ) { - return null; - } - return mContextWeakReference.get(); - } - - public BitmapProvider getBitmapProvider() { - if( mBitmapProviderWeakReference == null ) { - return null; - } - return mBitmapProviderWeakReference.get(); + public void setTileRenderPoolExecutor(TileRenderPoolExecutor tileRenderPoolExecutor ) { + mTileRenderPoolExecutorWeakReference = new WeakReference<>(tileRenderPoolExecutor); } public void setTile( Tile tile ) { @@ -103,22 +75,22 @@ public TileRenderHandler.Status renderTile() { if( tile == null ) { return TileRenderHandler.Status.INCOMPLETE; } - Context context = getContext(); - if( context == null ) { + TileRenderPoolExecutor tileRenderPoolExecutor = mTileRenderPoolExecutorWeakReference.get(); + if( tileRenderPoolExecutor == null ) { return TileRenderHandler.Status.INCOMPLETE; } - BitmapProvider bitmapProvider = getBitmapProvider(); - if( bitmapProvider == null ) { + TileCanvasViewGroup tileCanvasViewGroup = tileRenderPoolExecutor.getTileCanvasViewGroup(); + if(tileCanvasViewGroup == null ) { return TileRenderHandler.Status.INCOMPLETE; } try { - tile.generateBitmap( context, bitmapProvider ); + tile.generateBitmap( tileCanvasViewGroup.getContext(), tileCanvasViewGroup.getBitmapProvider() ); } catch( Throwable throwable ) { mThrowable = throwable; return TileRenderHandler.Status.ERROR; } if( mCancelled || tile.getBitmap() == null || mThread.isInterrupted() ) { - tile.destroy( true ); + tile.reset(); return TileRenderHandler.Status.INCOMPLETE; } return TileRenderHandler.Status.COMPLETE; @@ -134,10 +106,24 @@ public void run() { if( status == TileRenderHandler.Status.COMPLETE ) { mComplete = true; } - Handler handler = getHandler(); - if( handler != null ) { - Message message = handler.obtainMessage( status.getMessageCode(), this ); - message.sendToTarget(); + TileRenderPoolExecutor tileRenderPoolExecutor = mTileRenderPoolExecutorWeakReference.get(); + if( tileRenderPoolExecutor != null ) { + TileCanvasViewGroup tileCanvasViewGroup = tileRenderPoolExecutor.getTileCanvasViewGroup(); + if( tileCanvasViewGroup != null ) { + Tile tile = getTile(); + if( tile != null ) { + Handler handler = tileRenderPoolExecutor.getHandler(); + if( handler != null ) { + // need to stamp time now, since it'll be drawn before the handler posts + tile.setTransitionsEnabled( tileCanvasViewGroup.getTransitionsEnabled() ); + tile.setTransitionDuration( tileCanvasViewGroup.getTransitionDuration() ); + Message message = handler.obtainMessage( status.getMessageCode(), this ); + message.sendToTarget(); + } + } + } + } } + }