├── .gitattributes ├── .gitignore ├── README.md ├── assembly.xml ├── pom.xml └── src └── com └── qozix ├── animation ├── AnimationListener.java ├── Animator.java ├── Tween.java ├── TweenListener.java └── easing │ ├── EasingEquation.java │ ├── Linear.java │ └── Strong.java ├── geom ├── Coordinate.java └── Geolocator.java ├── layouts ├── AnchorLayout.java ├── FixedLayout.java ├── ScalingLayout.java ├── StaticLayout.java ├── TranslationLayout.java └── ZoomPanLayout.java ├── mapview ├── MapView.java ├── geom │ └── ManagedGeolocator.java ├── hotspots │ ├── HotSpot.java │ └── HotSpotManager.java ├── markers │ ├── CalloutManager.java │ └── MarkerManager.java ├── paths │ ├── PathManager.java │ └── PathView.java ├── tiles │ ├── MapTile.java │ ├── MapTileCache.java │ ├── MapTileDecoder.java │ ├── MapTileDecoderAssets.java │ ├── MapTileDecoderHttp.java │ ├── MapTilePool.java │ ├── TileManager.java │ ├── TileRenderHandler.java │ ├── TileRenderListener.java │ └── TileRenderTask.java ├── viewmanagers │ ├── DownsampleManager.java │ ├── ViewCurator.java │ ├── ViewFactory.java │ ├── ViewPool.java │ └── ViewSetManager.java └── zoom │ ├── ZoomLevel.java │ ├── ZoomLevelSet.java │ ├── ZoomListener.java │ ├── ZoomManager.java │ └── ZoomSetupListener.java └── widgets ├── AsyncTask.java ├── Scroller.java └── SealedHandler.java /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Maven 34 | ################# 35 | target/ 36 | 37 | ################# 38 | ## Visual Studio 39 | ################# 40 | 41 | ## Ignore Visual Studio temporary files, build results, and 42 | ## files generated by popular Visual Studio add-ons. 43 | 44 | # User-specific files 45 | *.suo 46 | *.user 47 | *.sln.docstates 48 | 49 | # Build results 50 | [Dd]ebug/ 51 | [Rr]elease/ 52 | *_i.c 53 | *_p.c 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.vspscc 68 | .builds 69 | *.dotCover 70 | 71 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 72 | #packages/ 73 | 74 | # Visual C++ cache files 75 | ipch/ 76 | *.aps 77 | *.ncb 78 | *.opensdf 79 | *.sdf 80 | 81 | # Visual Studio profiler 82 | *.psess 83 | *.vsp 84 | 85 | # ReSharper is a .NET coding add-in 86 | _ReSharper* 87 | 88 | # Installshield output folder 89 | [Ee]xpress 90 | 91 | # DocProject is a documentation generator add-in 92 | DocProject/buildhelp/ 93 | DocProject/Help/*.HxT 94 | DocProject/Help/*.HxC 95 | DocProject/Help/*.hhc 96 | DocProject/Help/*.hhk 97 | DocProject/Help/*.hhp 98 | DocProject/Help/Html2 99 | DocProject/Help/html 100 | 101 | # Click-Once directory 102 | publish 103 | 104 | # Others 105 | [Bb]in 106 | [Oo]bj 107 | sql 108 | TestResults 109 | *.Cache 110 | ClientBin 111 | stylecop.* 112 | ~$* 113 | *.dbmdl 114 | Generated_Code #added for RIA/Silverlight projects 115 | 116 | # Backup & report files from converting an old project file to a newer 117 | # Visual Studio version. Backup files are not needed, because we have git ;-) 118 | _UpgradeReport_Files/ 119 | Backup*/ 120 | UpgradeLog*.XML 121 | 122 | 123 | 124 | ############ 125 | ## Windows 126 | ############ 127 | 128 | # Windows image file caches 129 | Thumbs.db 130 | 131 | # Folder config file 132 | Desktop.ini 133 | 134 | 135 | ############# 136 | ## Python 137 | ############# 138 | 139 | *.py[co] 140 | 141 | # Packages 142 | *.egg 143 | *.egg-info 144 | dist 145 | build 146 | eggs 147 | parts 148 | bin 149 | var 150 | sdist 151 | develop-eggs 152 | .installed.cfg 153 | 154 | # Installer logs 155 | pip-log.txt 156 | 157 | # Unit test / coverage reports 158 | .coverage 159 | .tox 160 | 161 | #Translations 162 | *.mo 163 | 164 | #Mr Developer 165 | .mr.developer.cfg 166 | 167 | # Mac crap 168 | .DS_Store 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Update (August 5, 2013)

2 |

This widget has been superceded by TileView. All future commits will be to TileView. MapView will be eventually discontinued entirely as TileView supercedes it.

3 | 4 |

Update (April 20, 2013):

5 |

6 | Updating to 1.0.2, fixing a couple bugs, adding support for custom Bitmap decoders, improved intersection calculation 7 | (thanks to frankowskid), and some standardization. Also added 8 | the source for a sample app and a working .apk 9 |

10 |

Changes:

11 |
    12 |
  1. Fixed bug in pixel-based positioning created in 1.0.1
  2. 13 |
  3. Marker anchors are no longer "flipped" (if you want the marker to be offset by a negative value equal to half its width, use -0.5f... It used to be non-negative 0.5)
  4. 14 |
  5. Added new method (and interface) `setTileDecoder`, which allows the user to provide an implementation of a class to decode Bitmaps in an arbitrary fashion (assets, resources, http, SVG, dynamically-drawn, etc)
  6. 15 |
  7. Added concrete implementation of `MapEventListener` called `MapEventListenerImplementation`, that impelemts all signatures so you can just override the one's you're using
  8. 16 |
  9. Updated documentation
  10. 17 |
18 | 19 |

Update (March 28, 2013):

20 |

Updating to 1.0.1, fixing several bugs, reinstituting undocumented or previously removed features, and adding some experimental stuff.

21 | 22 |

Fixes:

23 |
    24 |
  1. Fixed bug in draw path where start y position was incorrectly reading x position
  2. 25 |
  3. Fixed bug in setZoom where it was maxing to the minimum (this method should work properly now)
  4. 26 |
  5. Fixed bug in geolocation math where the relative scale of the current zoom level was not being considered
  6. 27 |
  7. Fixed bug in all slideTo methods where postInvalidate was not being called
  8. 28 |
  9. Fixed bug where a render request was not issued at the conclusion of a slideTo animation
  10. 29 |
  11. Fixed signature of removeHotSpot
  12. 30 |
31 | 32 |

Enhancements:

33 |
    34 |
  1. Reinstated support for downsamples images. See MapView.java for new signatures
  2. 35 |
  3. No longer intercepts touch events by default. This can be enabled with `setShouldIntercept(boolean)`
  4. 36 |
  5. No longer caches tile images by default. This can be enabled with `setCacheEnabled(boolean)`
  6. 37 |
  7. added `clear` and `destroy` methods. The former is appropriate for `onPause`, the latter for `onDestroy` (incl. orientation changes)
  8. 38 |
  9. added `resetZoomLevels` to allow different sets to be used during runtime
  10. 39 |
  11. onFlingComplete now passes the x and y value of the final position
  12. 40 |
  13. added onScrollComplete (fires when a slideTo method finishes)
  14. 41 |
  15. exposed lockZoom and unlockZoom methods. These can be used to prevent a tile set from changing (useful during animated or user-event driven scale changes)
  16. 42 |
43 | 44 |

Known issues:

45 |
    46 |
  1. The positioning logic has gotten a little crummy. It works but is a little bloated and distributed across more parties than it should. I don't think experimental status fits, but it's close.
  2. 47 |
48 | 49 |

Note that the documentation has not been updated, nor has the jar - will get to those as soon as I get some time

50 | 51 |

Update (March 8, 2013):

52 | 53 |

While this component is still in beta, the version now available should behave predictably.

54 |

55 | This commit entails a complete rewrite to more closely adhere to Android framework conventions, especially as regards layout mechanics. 56 | The changes were more significant than even a major version update would justify, and in practice this version should be considered an 57 | entirely new component. Since we're still debugging I'm not bothering to deprecate methods or even make any attempt at backwards compatability; 58 | the previous version in it's entirely should be considered deprecated, and replaced with this release. 59 |

60 |

61 | The documentation has been updated as well. 62 |

63 | 64 |

An quick-n-dirty, undated, unversioned and incomplete changelog:

65 | 66 |
    67 |
  1. 68 | The component no longer requires (or even supports) initialization. Rendering is throttled through handlers and managed directly 69 | via onLayout and event listeners. This also means no more "onReady" anything - it just runs. Just add some zoom levels and start using it. In theory, zoom levels can be added dynamically later 70 | (after the code block it was instantiated - e.g., through a user action), but this is untested. 71 |
  2. 72 |
  3. 73 | Zoom levels are now managed in a TreeSet, so are naturally unique and ordered by area (so they can now be added in any order, and you're no 74 | longer required to add them smallest-to-largest) 75 |
  4. 76 |
  5. 77 | Image tiles now use caching, both in-memory (25% of total space available) and on-disk (up to 8MB). For the on-disk cache, you'll now need to include 78 | external-write permission:
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    . 79 | Testing for permission is on the todo list - right now you'll just get a runtime failure. 80 |
  6. 81 |
  7. 82 | Now supports multiple listeners (addMapEventListener instead of setMapEventListener). Note that the interface is now MapEventListener, not OnMapEventListener. 83 | We now also provide access to a lot more events, including events for the rendering process and zoom events (where zoom indicates a change in zoom level, 84 | in addition to the already existing events when the scale changed) 85 |
  8. 86 |
  9. 87 | We now intercept touch move events, so Markers won't prevent dragging. The platform is still janky about consuming and bubbling events, but I think this is about as good as it's going to get. 88 |
  10. 89 |
  11. 90 | Removed support for downsamples. That behavior wasn't visible in most other similar programs (e.g., google maps, CATiledLayer); it seemed to confuse some people; and it really hurt performance. 91 | I may consider a single low-res image as a background (rather than a different downsample per zoom level), depending on user feedback. 92 |
  12. 93 |
  13. 94 | Removed support for tile transitions. There was no way to keep this from eating up too much memory when abused (fast, repeated pinches between zoom levels), when we wanted to keep the previous 95 | tile set visible until the new one was rendered. 96 |
  14. 97 |
  15. 98 | Added support for zoom-level specific Markers and Paths - you can now specify a zoom index to addMarker/Path calls, which will hide those Views on all other zoom levels. 99 |
  16. 100 |
  17. 101 | Drastic refactorization and optimization of the core classes. 102 |
  18. 103 |
104 | 105 |

106 | If you test this on a device, please let me know the results - in the case of either success or failure. I've personally run it on several devices running several different versions of the OS, but 107 | would be very interested in results from the wild. I'll be working on putting up a jar and a sample app. 108 |

109 | 110 |

Finally, thanks to everyone that's been in touch with comments and ideas on how to make this widget better. I appreciate all the input

111 | 112 |

MapView

113 |

The MapView widget is a subclass of ViewGroup that provides a mechanism to asynchronously display tile-based images, 114 | with additional functionality for 2D dragging, flinging, pinch or double-tap to zoom, adding overlaying Views (markers), 115 | multiple levels of detail, and support for faux-geolocation (by specifying top-left and bottom-right coordinates).

116 | 117 |

It might be best described as a hybrid of com.google.android.maps.MapView and iOS's CATiledLayer, and is appropriate for a variety of uses 118 | but was intended for map-type applications, especially high-detail or custom implementations (e.g., inside a building).

119 | 120 |

A minimal implementation might look like this:

121 | 122 |
MapView mapView = new MapView(this);
123 | mapView.addZoomLevel(1440, 900, "path/to/tiles/%col%-%row%.jpg");
124 | 125 | A more advanced implementation might look like this: 126 |
MapView mapView = new MapView(this);
127 | mapView.registerGeolocator(42.379676, -71.094919, 42.346550, -71.040280);
128 | mapView.addZoomLevel(6180, 5072, "tiles/boston-1000-%col%_%row%.jpg", 512, 512);
129 | mapView.addZoomLevel(3090, 2536, "tiles/boston-500-%col%_%row%.jpg", 256, 256);
130 | mapView.addZoomLevel(1540, 1268, "tiles/boston-250-%col%_%row%.jpg", 256, 256);
131 | mapView.addZoomLevel(770, 634, "tiles/boston-125-%col%_%row%.jpg", 128, 128);
132 | mapView.addMarker(someView, 42.35848, -71.063736);
133 | mapView.addMarker(anotherView, 42.3665, -71.05224);
134 | 
135 | 136 |

Installation

137 |

138 | The widget is straight java, so you can just use the .java files found here (with the dependencies mentioned below), or you can download 139 | the jar. 140 | Simple instructions are available here. 141 |

142 | 143 |

Example

144 |

145 | The source for a working app using the MapView is here. 146 | The compiled .apk for that demo is here. 147 |

148 | 149 |

Dependencies

150 |

151 | If you're targetting APIs less than 12, you'll need the 152 | Android compatability lib 153 | for the LruCache implementation. 154 |

155 |

156 | Jake Wharton's DiskLruCache is also used. 157 | Here's a direct link to that jar. 158 | However, that package is bundled with mapviewlib.jar so is only needed if you're using the java files directly in your project. 159 |

160 | 161 |

Maven users

162 | ```xml 163 | 164 | com.github.moagrius 165 | MapView 166 | 1.0.0 167 | 168 | ``` 169 | 170 |

Documentation

171 |

Javadocs are here.

172 | 173 |

License

174 |

Licensed under Creative Commons

175 | -------------------------------------------------------------------------------- /assembly.xml: -------------------------------------------------------------------------------- 1 | 4 | jar-with-dependencies 5 | 6 | jar 7 | 8 | false 9 | 10 | 11 | / 12 | true 13 | true 14 | runtime 15 | 16 | android.support:compatibility-v4 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | com.github.moagrius 7 | MapView 8 | 1.0.2 9 | jar 10 | 11 | 12 | android 13 | android 14 | 4.2_r1 15 | provided 16 | 17 | 18 | com.jakewharton 19 | disklrucache 20 | 1.3.1 21 | 22 | 23 | android.support 24 | compatibility-v4 25 | 12 26 | 27 | 28 | 29 | 30 | src 31 | target/classes 32 | 33 | 34 | 35 | com.jayway.maven.plugins.android.generation2 36 | android-maven-plugin 37 | 3.5.3 38 | true 39 | 40 | 41 | 17 42 | 43 | 44 | false 45 | false 46 | false 47 | true 48 | false 49 | true 50 | ${project.build.directory}/lint-results 51 | true 52 | ${project.build.directory}/lint-results/lint-results.xml 53 | 54 | 55 | 56 | 57 | lint 58 | 59 | lint 60 | 61 | install 62 | 63 | 64 | 65 | 66 | maven-assembly-plugin 67 | 2.4 68 | 69 | assembly.xml 70 | 71 | 72 | 73 | make-assembly 74 | package 75 | 76 | single 77 | 78 | 79 | 80 | 81 | 82 | org.apache.maven.plugins 83 | maven-compiler-plugin 84 | 3.0 85 | 86 | 1.6 87 | 1.6 88 | true 89 | 1.6 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/com/qozix/animation/AnimationListener.java: -------------------------------------------------------------------------------- 1 | package com.qozix.animation; 2 | 3 | import java.util.HashMap; 4 | 5 | public interface AnimationListener { 6 | public void onAnimationStart(); 7 | public void onAnimationProgress(HashMap values); 8 | public void onAnimationComplete(); 9 | } 10 | -------------------------------------------------------------------------------- /src/com/qozix/animation/Animator.java: -------------------------------------------------------------------------------- 1 | package com.qozix.animation; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | import com.qozix.animation.easing.EasingEquation; 8 | import com.qozix.animation.easing.Linear; 9 | 10 | import android.os.Handler; 11 | import android.os.Message; 12 | 13 | public class Animator { 14 | 15 | private double ellapsed; 16 | private double startTime; 17 | private double duration = 500; 18 | 19 | private HashMap properties = new HashMap(); 20 | private HashMap values = new HashMap(); 21 | 22 | private ArrayList listeners = new ArrayList(); 23 | private EasingEquation ease = Linear.EaseNone; 24 | 25 | public void setAnimationEase(EasingEquation e) { 26 | if(e == null || !( e instanceof EasingEquation)){ 27 | e = Linear.EaseNone; 28 | } 29 | ease = e; 30 | } 31 | 32 | public void addAnimationListener(AnimationListener l) { 33 | listeners.add(l); 34 | } 35 | 36 | public void removeAnimationListener(AnimationListener l){ 37 | listeners.remove(l); 38 | } 39 | 40 | public double getDuration() { 41 | return duration; 42 | } 43 | 44 | public void setDuration(double time) { 45 | duration = time; 46 | } 47 | 48 | public HashMap getProperties() { 49 | return properties; 50 | } 51 | 52 | public void setProperties(HashMap p) { 53 | properties = p; 54 | } 55 | 56 | public void addProperties(HashMap p){ 57 | properties.putAll(p); 58 | } 59 | 60 | public void addProperty(String s, Double d){ 61 | properties.put(s, d); 62 | } 63 | 64 | public void start() { 65 | values.putAll(properties); 66 | ellapsed = 0; 67 | startTime = System.currentTimeMillis(); 68 | handler.sendEmptyMessage(0); 69 | for(AnimationListener l : listeners){ 70 | l.onAnimationStart(); 71 | } 72 | } 73 | 74 | private Handler handler = new Handler() { 75 | @Override 76 | public void handleMessage(final Message message) { 77 | ellapsed = System.currentTimeMillis() - startTime; 78 | for(Map.Entry e : values.entrySet()) { 79 | String key = e.getKey(); 80 | Double value = e.getValue(); 81 | Double originalValue = properties.get(key); 82 | Double computedValue = ease.compute(ellapsed, originalValue, originalValue - value, duration); 83 | e.setValue(computedValue); 84 | } 85 | for(AnimationListener l : listeners){ 86 | l.onAnimationProgress(values); 87 | } 88 | if (ellapsed >= duration) { 89 | if (hasMessages(0)) { 90 | removeMessages(0); 91 | } 92 | for(AnimationListener l : listeners){ 93 | l.onAnimationComplete(); 94 | } 95 | } else { 96 | sendEmptyMessage(0); 97 | } 98 | 99 | } 100 | }; 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/com/qozix/animation/Tween.java: -------------------------------------------------------------------------------- 1 | package com.qozix.animation; 2 | 3 | import java.util.ArrayList; 4 | 5 | import android.os.Handler; 6 | import android.os.Message; 7 | 8 | import com.qozix.animation.easing.EasingEquation; 9 | import com.qozix.animation.easing.Linear; 10 | 11 | public class Tween { 12 | 13 | private double ellapsed; 14 | private double startTime; 15 | private double duration = 500; 16 | 17 | private ArrayList listeners = new ArrayList(); 18 | private EasingEquation ease = Linear.EaseNone; 19 | 20 | public double getProgress() { 21 | return ellapsed / duration; 22 | } 23 | 24 | public double getEasedProgress() { 25 | return ease.compute( ellapsed, 0, 1, duration ); 26 | } 27 | 28 | public void setAnimationEase( EasingEquation e ) { 29 | if ( e == null ) { 30 | e = Linear.EaseNone; 31 | } 32 | ease = e; 33 | } 34 | 35 | public void addTweenListener( TweenListener l ) { 36 | listeners.add( l ); 37 | } 38 | 39 | public void removeTweenListener( TweenListener l ) { 40 | listeners.remove( l ); 41 | } 42 | 43 | public double getDuration() { 44 | return duration; 45 | } 46 | 47 | public void setDuration( double time ) { 48 | duration = time; 49 | } 50 | 51 | public void start() { 52 | stop(); 53 | ellapsed = 0; 54 | startTime = System.currentTimeMillis(); 55 | for ( TweenListener l : listeners ) { 56 | l.onTweenStart(); 57 | } 58 | handler.sendEmptyMessage( 0 ); 59 | } 60 | 61 | public void stop() { 62 | if ( handler.hasMessages( 0 ) ) { 63 | handler.removeMessages( 0 ); 64 | } 65 | } 66 | 67 | private Handler handler = new Handler() { 68 | @Override 69 | public void handleMessage( final Message message ) { 70 | ellapsed = System.currentTimeMillis() - startTime; 71 | ellapsed = Math.min( ellapsed, duration ); 72 | double progress = getProgress(); 73 | double eased = getEasedProgress(); 74 | for ( TweenListener l : listeners ) { 75 | l.onTweenProgress( progress, eased ); 76 | } 77 | if ( ellapsed >= duration ) { 78 | if ( hasMessages( 0 ) ) { 79 | removeMessages( 0 ); 80 | } 81 | for ( TweenListener l : listeners ) { 82 | l.onTweenComplete(); 83 | } 84 | } else { 85 | sendEmptyMessage( 0 ); 86 | } 87 | 88 | } 89 | }; 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/com/qozix/animation/TweenListener.java: -------------------------------------------------------------------------------- 1 | package com.qozix.animation; 2 | 3 | public interface TweenListener { 4 | public void onTweenStart(); 5 | public void onTweenProgress(double progress, double eased); 6 | public void onTweenComplete(); 7 | } 8 | -------------------------------------------------------------------------------- /src/com/qozix/animation/easing/EasingEquation.java: -------------------------------------------------------------------------------- 1 | package com.qozix.animation.easing; 2 | 3 | public abstract class EasingEquation { 4 | public double compute(double t, double b, double c, double d){ 5 | return c * ( t / d ) + b; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/com/qozix/animation/easing/Linear.java: -------------------------------------------------------------------------------- 1 | package com.qozix.animation.easing; 2 | 3 | public abstract class Linear extends EasingEquation { 4 | public static final Linear EaseNone = new Linear(){ 5 | 6 | }; 7 | public static final Linear EaseIn = EaseNone; 8 | public static final Linear EaseOut = EaseNone; 9 | } 10 | -------------------------------------------------------------------------------- /src/com/qozix/animation/easing/Strong.java: -------------------------------------------------------------------------------- 1 | package com.qozix.animation.easing; 2 | 3 | public abstract class Strong extends EasingEquation { 4 | public static final Strong EaseNone = new Strong(){ 5 | 6 | }; 7 | public static final Strong EaseIn = new Strong(){ 8 | @Override 9 | public double compute(double t, double b, double c, double d){ 10 | return c * Math.pow(t / d, 5) + b; 11 | } 12 | }; 13 | public static final Strong EaseOut = new Strong(){ 14 | @Override 15 | public double compute(double t, double b, double c, double d){ 16 | return c * ( 1 - Math.pow( 1 - ( t / d), 5) ) + b; 17 | } 18 | }; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/com/qozix/geom/Coordinate.java: -------------------------------------------------------------------------------- 1 | package com.qozix.geom; 2 | 3 | public class Coordinate { 4 | 5 | public double latitude; 6 | public double longitude; 7 | 8 | public Coordinate() { 9 | } 10 | 11 | public Coordinate( double lat, double lng ) { 12 | set( lat, lng ); 13 | } 14 | 15 | public Coordinate( Coordinate src ) { 16 | set( src.latitude, src.longitude ); 17 | } 18 | 19 | public void set( double lat, double lng ) { 20 | latitude = lat; 21 | longitude = lng; 22 | } 23 | 24 | public final boolean equals( double lat, double lng ) { 25 | return latitude == lat && longitude == lng; 26 | } 27 | 28 | @Override 29 | public boolean equals( Object o ) { 30 | if ( o instanceof Coordinate ) { 31 | Coordinate c = (Coordinate) o; 32 | return latitude == c.latitude && longitude == c.longitude; 33 | } 34 | return false; 35 | } 36 | 37 | @Override 38 | public int hashCode() { 39 | return (int) ( latitude * 32713 + longitude ); 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | return "Coordinate(" + latitude + ", " + longitude + ")"; 45 | } 46 | } -------------------------------------------------------------------------------- /src/com/qozix/geom/Geolocator.java: -------------------------------------------------------------------------------- 1 | package com.qozix.geom; 2 | 3 | import java.util.ArrayList; 4 | 5 | import android.graphics.Point; 6 | 7 | public class Geolocator { 8 | 9 | private Coordinate topLeft = new Coordinate(); 10 | private Coordinate bottomRight = new Coordinate(); 11 | 12 | private int width = 0; 13 | private int height = 0; 14 | 15 | public void setCoordinates( Coordinate tl, Coordinate br ) { 16 | topLeft = tl; 17 | bottomRight = br; 18 | } 19 | 20 | public void setCoordinates( double left, double top, double right, double bottom ){ 21 | topLeft = new Coordinate( left, top ); 22 | bottomRight = new Coordinate( right, bottom ); 23 | } 24 | 25 | public void setSize( int w, int h ) { 26 | width = w; 27 | height = h; 28 | } 29 | 30 | public Point translate( Coordinate c ) { 31 | 32 | Point p = new Point(); 33 | 34 | double longitudanalDelta = bottomRight.longitude - topLeft.longitude; 35 | double longitudanalDifference = c.longitude - topLeft.longitude; 36 | double longitudanalFactor = longitudanalDifference / longitudanalDelta; 37 | p.x = (int) ( longitudanalFactor * width ); 38 | 39 | double latitudanalDelta = bottomRight.latitude - topLeft.latitude; 40 | double latitudanalDifference = c.latitude - topLeft.latitude; 41 | double latitudanalFactor = latitudanalDifference / latitudanalDelta; 42 | p.y = (int) ( latitudanalFactor * height ); 43 | 44 | return p; 45 | 46 | } 47 | 48 | public Coordinate translate( Point p ) { 49 | 50 | Coordinate c = new Coordinate(); 51 | 52 | double relativeX = p.x / (double) width; 53 | double deltaX = bottomRight.longitude - topLeft.longitude; 54 | c.longitude = topLeft.longitude + deltaX * relativeX; 55 | 56 | double relativeY = p.y / (double) height; 57 | double deltaY = bottomRight.latitude - topLeft.latitude; 58 | c.latitude = topLeft.latitude + deltaY * relativeY; 59 | 60 | return c; 61 | } 62 | 63 | public int[] coordinatesToPixels( double lat, double lng ) { 64 | 65 | int[] positions = new int[2]; 66 | 67 | double longitudanalDelta = bottomRight.longitude - topLeft.longitude; 68 | double longitudanalDifference = lng - topLeft.longitude; 69 | double longitudanalFactor = longitudanalDifference / longitudanalDelta; 70 | positions[0] = (int) ( longitudanalFactor * width ); 71 | 72 | double latitudanalDelta = bottomRight.latitude - topLeft.latitude; 73 | double latitudanalDifference = lat - topLeft.latitude; 74 | double latitudanalFactor = latitudanalDifference / latitudanalDelta; 75 | positions[1] = (int) ( latitudanalFactor * height ); 76 | 77 | return positions; 78 | 79 | } 80 | 81 | public ArrayList getPointsFromCoordinates( ArrayList coordinates ) { 82 | ArrayList points = new ArrayList(); 83 | for ( Coordinate coordinate : coordinates ) { 84 | Point point = translate( coordinate ); 85 | points.add( point ); 86 | } 87 | return points; 88 | } 89 | 90 | public ArrayList getCoordinatesFromPoints( ArrayList points ) { 91 | ArrayList coordinates = new ArrayList(); 92 | for ( Point point : points ) { 93 | Coordinate coordinate = translate( point ); 94 | coordinates.add( coordinate ); 95 | } 96 | return coordinates; 97 | } 98 | 99 | public boolean contains( Coordinate coordinate ) { 100 | double minLat = Math.min( topLeft.latitude, bottomRight.latitude ); 101 | double maxLat = Math.max( topLeft.latitude, bottomRight.latitude ); 102 | double minLng = Math.min( topLeft.longitude, bottomRight.longitude ); 103 | double maxLng = Math.max( topLeft.longitude, bottomRight.longitude ); 104 | return coordinate.latitude >= minLat 105 | && coordinate.latitude <= maxLat 106 | && coordinate.longitude >= minLng 107 | && coordinate.longitude <= maxLng; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/com/qozix/layouts/AnchorLayout.java: -------------------------------------------------------------------------------- 1 | package com.qozix.layouts; 2 | 3 | import android.content.Context; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | 7 | /** 8 | * The AnchorLayout positions it's children using absolute pixel values, 9 | * offset by anchors. An anchor exists on each axis (x and y), and is 10 | * determined by multiplying the relevant dimension of the child (width for x, 11 | * height for y) by a float. This float can be supplied to each child via 12 | * LayoutParams, or to the AnchorLayout ViewGroup directly. If a child's 13 | * LayoutParams are not specified (null), then it will be positioned using 14 | * the Layout's anchor values. 15 | * 16 | * For example, passing an -0.5f anchorX and -1.0f anchorY will position the 17 | * view entirely above, and centered horizontally, relative to the pixel 18 | * coordinates supplied. 19 | * 20 | * This is useful for positioning elements as indicators for another view, 21 | * or graphical feature. Tooltips, map markers, instructional elements, etc 22 | * could benefit from anchored layouts. 23 | */ 24 | 25 | public class AnchorLayout extends ViewGroup { 26 | 27 | protected float anchorX = 0f; 28 | protected float anchorY = 0f; 29 | 30 | public AnchorLayout( Context context ) { 31 | super( context ); 32 | } 33 | 34 | /** 35 | * Sets the anchor values used by this ViewGroup if it's children do not 36 | * have anchor values supplied directly (via LayoutParams) 37 | * @param aX (float) x-axis anchor value (offset computed by multiplying this value by the child's width 38 | * @param aY (float) y-axis anchor value (offset computed by multiplying this value by the child's height 39 | */ 40 | public void setAnchors( float aX, float aY ) { 41 | anchorX = aX; 42 | anchorY = aY; 43 | requestLayout(); 44 | } 45 | 46 | @Override 47 | protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec ) { 48 | 49 | measureChildren( widthMeasureSpec, heightMeasureSpec ); 50 | 51 | int width = 0; 52 | int height = 0; 53 | 54 | int count = getChildCount(); 55 | for ( int i = 0; i < count; i++ ) { 56 | View child = getChildAt( i ); 57 | if ( child.getVisibility() != GONE ) { 58 | AnchorLayout.LayoutParams lp = (AnchorLayout.LayoutParams) child.getLayoutParams(); 59 | // get anchor offsets 60 | float aX = ( lp.anchorX == null ) ? anchorX : lp.anchorX; 61 | float aY = ( lp.anchorY == null ) ? anchorY : lp.anchorY; 62 | // offset dimensions by anchor values 63 | int computedWidth = (int) ( child.getMeasuredWidth() * aX ); 64 | int computedHeight = (int) ( child.getMeasuredHeight() * aY ); 65 | // add computed dimensions to actual position 66 | int right = lp.x + computedWidth; 67 | int bottom = lp.y + computedHeight; 68 | // if it's larger, use that 69 | width = Math.max( width, right ); 70 | height = Math.max( height, bottom ); 71 | } 72 | } 73 | 74 | height = Math.max( height, getSuggestedMinimumHeight() ); 75 | width = Math.max( width, getSuggestedMinimumWidth() ); 76 | width = resolveSize( width, widthMeasureSpec ); 77 | height = resolveSize( height, heightMeasureSpec ); 78 | setMeasuredDimension( width, height ); 79 | 80 | } 81 | 82 | @Override 83 | protected void onLayout( boolean changed, int l, int t, int r, int b ) { 84 | int count = getChildCount(); 85 | for ( int i = 0; i < count; i++ ) { 86 | View child = getChildAt( i ); 87 | if ( child.getVisibility() != GONE ) { 88 | LayoutParams lp = (LayoutParams) child.getLayoutParams(); 89 | // get sizes 90 | int w = child.getMeasuredWidth(); 91 | int h = child.getMeasuredHeight(); 92 | // user child's layout params anchor position if set, otherwise 93 | // default to anchor position of layout 94 | float aX = ( lp.anchorX == null ) ? anchorX : lp.anchorX; 95 | float aY = ( lp.anchorY == null ) ? anchorY : lp.anchorY; 96 | // apply anchor offset to position 97 | int x = lp.x + (int) ( w * aX ); 98 | int y = lp.y + (int) ( h * aY ); 99 | // set it 100 | child.layout( x, y, x + w, y + h ); 101 | } 102 | } 103 | } 104 | 105 | @Override 106 | protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 107 | return new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0 ); 108 | } 109 | 110 | @Override 111 | protected boolean checkLayoutParams( ViewGroup.LayoutParams p ) { 112 | return p instanceof AnchorLayout.LayoutParams; 113 | } 114 | 115 | @Override 116 | protected ViewGroup.LayoutParams generateLayoutParams( ViewGroup.LayoutParams p ) { 117 | return new LayoutParams( p ); 118 | } 119 | 120 | /** 121 | * Per-child layout information associated with AnchorLayout. 122 | */ 123 | public static class LayoutParams extends ViewGroup.LayoutParams { 124 | 125 | /** 126 | * The absolute left position of the child in pixels 127 | */ 128 | public int x = 0; 129 | /** 130 | * The absolute right position of the child in pixels 131 | */ 132 | public int y = 0; 133 | /** 134 | * Float value to determine the child's horizontal offset. This float is multiplied by the child's width. If null, the containing AnchorLayout's anchor values will be used. 135 | */ 136 | public Float anchorX = null; 137 | /** 138 | * Float value to determine the child's vertical offset. This float is multiplied by the child's height. If null, the containing AnchorLayout's anchor values will be used. 139 | */ 140 | public Float anchorY = null; 141 | 142 | /** 143 | * Copy constructor 144 | * @param source (LayoutParams) LayoutParams instance to copy properties from 145 | */ 146 | public LayoutParams( ViewGroup.LayoutParams source ) { 147 | super( source ); 148 | } 149 | 150 | /** 151 | * Creates a new set of layout parameters with the specified values. 152 | * @param width (int) Information about how wide the view wants to be. This should generally be WRAP_CONTENT or a fixed value. 153 | * @param height (int) Information about how tall the view wants to be. This should generally be WRAP_CONTENT or a fixed value. 154 | */ 155 | public LayoutParams( int width, int height ) { 156 | super( width, height ); 157 | } 158 | 159 | /** 160 | * Creates a new set of layout parameters with the specified values. 161 | * @param width (int) Information about how wide the view wants to be. This should generally be WRAP_CONTENT or a fixed value. 162 | * @param height (int) Information about how tall the view wants to be. This should generally be WRAP_CONTENT or a fixed value. 163 | * @param left (int) Sets the absolute x value of the view's position in pixels 164 | * @param top (int) Sets the absolute y value of the view's position in pixels 165 | */ 166 | public LayoutParams( int width, int height, int left, int top ) { 167 | super( width, height ); 168 | x = left; 169 | y = top; 170 | } 171 | 172 | /** 173 | * Creates a new set of layout parameters with the specified values. 174 | * @param width (int) Information about how wide the view wants to be. This should generally be WRAP_CONTENT or a fixed value. 175 | * @param height (int) Information about how tall the view wants to be. This should generally be WRAP_CONTENT or a fixed value. 176 | * @param left (int) Sets the absolute x value of the view's position in pixels 177 | * @param top (int) Sets the absolute y value of the view's position in pixels 178 | * @param aX (float) Sets the relative horizontal offset of the view (multiplied by the view's width) 179 | * @param aY (float) Sets the relative vertical offset of the view (multiplied by the view's height) 180 | */ 181 | public LayoutParams( int width, int height, int left, int top, float aX, float aY ) { 182 | super( width, height ); 183 | x = left; 184 | y = top; 185 | anchorX = aX; 186 | anchorY = aY; 187 | } 188 | 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/com/qozix/layouts/FixedLayout.java: -------------------------------------------------------------------------------- 1 | package com.qozix.layouts; 2 | 3 | import android.content.Context; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | 7 | public class FixedLayout extends ViewGroup { 8 | 9 | public FixedLayout(Context context) { 10 | super(context); 11 | } 12 | 13 | @Override 14 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 15 | 16 | measureChildren(widthMeasureSpec, heightMeasureSpec); 17 | 18 | int w = 0; 19 | int h = 0; 20 | 21 | int count = getChildCount(); 22 | for (int i = 0; i < count; i++) { 23 | View child = getChildAt(i); 24 | if (child.getVisibility() != GONE) { 25 | FixedLayout.LayoutParams lp = (FixedLayout.LayoutParams) child.getLayoutParams(); 26 | int right = lp.x + child.getMeasuredWidth(); 27 | int bottom = lp.y + child.getMeasuredHeight(); 28 | w = Math.max(w, right); 29 | h = Math.max(h, bottom); 30 | } 31 | } 32 | 33 | h = Math.max(h, getSuggestedMinimumHeight()); 34 | w = Math.max(w, getSuggestedMinimumWidth()); 35 | 36 | w = resolveSize(w, widthMeasureSpec); 37 | h = resolveSize(h, heightMeasureSpec); 38 | 39 | setMeasuredDimension(w, h); 40 | 41 | } 42 | 43 | @Override 44 | protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 45 | return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0); 46 | } 47 | 48 | @Override 49 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 50 | int count = getChildCount(); 51 | for (int i = 0; i < count; i++) { 52 | View child = getChildAt(i); 53 | if (child.getVisibility() != GONE) { 54 | FixedLayout.LayoutParams lp = (FixedLayout.LayoutParams) child.getLayoutParams(); 55 | child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y + child.getMeasuredHeight()); 56 | } 57 | } 58 | } 59 | 60 | @Override 61 | protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 62 | return p instanceof FixedLayout.LayoutParams; 63 | } 64 | 65 | @Override 66 | protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 67 | return new LayoutParams(p); 68 | } 69 | 70 | public static class LayoutParams extends ViewGroup.LayoutParams { 71 | 72 | public int x = 0; 73 | public int y = 0; 74 | 75 | public LayoutParams(int width, int height, int left, int top) { 76 | super(width, height); 77 | x = left; 78 | y = top; 79 | } 80 | 81 | public LayoutParams(int width, int height){ 82 | super(width, height); 83 | } 84 | 85 | public LayoutParams(ViewGroup.LayoutParams source) { 86 | super(source); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/com/qozix/layouts/ScalingLayout.java: -------------------------------------------------------------------------------- 1 | package com.qozix.layouts; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | 6 | public class ScalingLayout extends FixedLayout { 7 | 8 | private double scale = 1; 9 | 10 | public ScalingLayout( Context context ) { 11 | super( context ); 12 | setWillNotDraw( false ); 13 | } 14 | 15 | public void setScale( double factor ) { 16 | scale = factor; 17 | invalidate(); 18 | } 19 | 20 | public double getScale() { 21 | return scale; 22 | } 23 | 24 | @Override 25 | public void onDraw( Canvas canvas ) { 26 | canvas.scale( (float) scale, (float) scale ); 27 | super.onDraw( canvas ); 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /src/com/qozix/layouts/StaticLayout.java: -------------------------------------------------------------------------------- 1 | package com.qozix.layouts; 2 | 3 | import android.content.Context; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | 7 | public class StaticLayout extends ViewGroup { 8 | 9 | public StaticLayout(Context context) { 10 | super(context); 11 | } 12 | 13 | @Override 14 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 15 | 16 | measureChildren(widthMeasureSpec, heightMeasureSpec); 17 | 18 | int w = 0; 19 | int h = 0; 20 | 21 | int count = getChildCount(); 22 | for (int i = 0; i < count; i++) { 23 | View child = getChildAt(i); 24 | if (child.getVisibility() != GONE) { 25 | w = Math.max(w, child.getMeasuredWidth()); 26 | h = Math.max(h, child.getMeasuredHeight()); 27 | } 28 | } 29 | 30 | h = Math.max(h, getSuggestedMinimumHeight()); 31 | w = Math.max(w, getSuggestedMinimumWidth()); 32 | 33 | w = resolveSize(w, widthMeasureSpec); 34 | h = resolveSize(h, heightMeasureSpec); 35 | 36 | setMeasuredDimension(w, h); 37 | 38 | } 39 | 40 | @Override 41 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 42 | int count = getChildCount(); 43 | for (int i = 0; i < count; i++) { 44 | View child = getChildAt(i); 45 | if (child.getVisibility() != GONE) { 46 | child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight()); 47 | } 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/com/qozix/layouts/TranslationLayout.java: -------------------------------------------------------------------------------- 1 | package com.qozix.layouts; 2 | 3 | import android.content.Context; 4 | import android.view.View; 5 | 6 | /** 7 | * The TranslationLayout extends {@link AnhorLayout}, but additionally supports 8 | * a scale value. The views of this layout will not be scaled along width or height, 9 | * but their positions will be multiplied by the TranslationLayout's scale value. 10 | * This allows the contained views to maintain their visual appearance and distance 11 | * relative to each other, while the total area of the group can be managed by the 12 | * scale value. 13 | * 14 | * This is useful for positioning groups of markers, tooltips, or indicator views 15 | * without scaling, while the reference element(s) are scaled. 16 | */ 17 | 18 | public class TranslationLayout extends AnchorLayout { 19 | 20 | protected double scale = 1; 21 | 22 | public TranslationLayout(Context context){ 23 | super(context); 24 | } 25 | 26 | /** 27 | * Sets the scale (0-1) of the ZoomPanLayout 28 | * @param scale (double) The new value of the ZoomPanLayout scale 29 | */ 30 | public void setScale(double d){ 31 | scale = d; 32 | requestLayout(); 33 | } 34 | 35 | /** 36 | * Retrieves the current scale of the ZoomPanLayout 37 | * @return (double) the current scale of the ZoomPanLayout 38 | */ 39 | public double getScale() { 40 | return scale; 41 | } 42 | 43 | @Override 44 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 45 | 46 | measureChildren(widthMeasureSpec, heightMeasureSpec); 47 | 48 | int width = 0; 49 | int height = 0; 50 | 51 | int count = getChildCount(); 52 | for (int i = 0; i < count; i++) { 53 | View child = getChildAt(i); 54 | if (child.getVisibility() != GONE) { 55 | TranslationLayout.LayoutParams lp = (TranslationLayout.LayoutParams) child.getLayoutParams(); 56 | // get anchor offsets 57 | float aX = (lp.anchorX == null) ? anchorX : lp.anchorX; 58 | float aY = (lp.anchorY == null) ? anchorY : lp.anchorY; 59 | // offset dimensions by anchor values 60 | int computedWidth = (int) (child.getMeasuredWidth() * aX); 61 | int computedHeight = (int) (child.getMeasuredHeight() * aY); 62 | // get offset position 63 | int scaledX = (int) (0.5 + (lp.x * scale)); 64 | int scaledY = (int) (0.5 + (lp.y * scale)); 65 | // add computed dimensions to actual position 66 | int right = scaledX + computedWidth; 67 | int bottom = scaledY + computedHeight; 68 | // if it's larger, use that 69 | width = Math.max(width, right); 70 | height = Math.max(height, bottom); 71 | } 72 | } 73 | 74 | height = Math.max(height, getSuggestedMinimumHeight()); 75 | width = Math.max(width, getSuggestedMinimumWidth()); 76 | width = resolveSize(width, widthMeasureSpec); 77 | height = resolveSize(height, heightMeasureSpec); 78 | setMeasuredDimension(width, height); 79 | 80 | } 81 | 82 | @Override 83 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 84 | int count = getChildCount(); 85 | for (int i = 0; i < count; i++) { 86 | View child = getChildAt(i); 87 | if (child.getVisibility() != GONE) { 88 | LayoutParams lp = (LayoutParams) child.getLayoutParams(); 89 | // get sizes 90 | int w = child.getMeasuredWidth(); 91 | int h = child.getMeasuredHeight(); 92 | // get offset position 93 | int scaledX = (int) (0.5 + (lp.x * scale)); 94 | int scaledY = (int) (0.5 + (lp.y * scale)); 95 | // user child's layout params anchor position if set, otherwise default to anchor position of layout 96 | float aX = (lp.anchorX == null) ? anchorX : lp.anchorX; 97 | float aY = (lp.anchorY == null) ? anchorY : lp.anchorY; 98 | // apply anchor offset to position 99 | int x = scaledX + (int) (w * aX); 100 | int y = scaledY + (int) (h * aY); 101 | // set it 102 | child.layout(x, y, x + w, y + h); 103 | } 104 | } 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/com/qozix/layouts/ZoomPanLayout.java: -------------------------------------------------------------------------------- 1 | package com.qozix.layouts; 2 | 3 | import java.lang.ref.WeakReference; 4 | import java.util.HashSet; 5 | 6 | import android.content.Context; 7 | import android.graphics.Point; 8 | import android.os.Handler; 9 | import android.os.Message; 10 | import android.view.MotionEvent; 11 | import android.view.VelocityTracker; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | 15 | import com.qozix.animation.Tween; 16 | import com.qozix.animation.TweenListener; 17 | import com.qozix.animation.easing.Strong; 18 | import com.qozix.widgets.Scroller; 19 | 20 | /** 21 | * ZoomPanLayout extends ViewGroup to provide support for scrolling and zooming. Fling, drag, pinch and 22 | * double-tap events are supported natively. 23 | * 24 | * ZoomPanLayout does not support direct insertion of child Views, and manages positioning through an intermediary View. 25 | * the addChild method provides an interface to add layouts to that intermediary view. Each of these children are provided 26 | * with LayoutParams of MATCH_PARENT for both axes, and will always be positioned at 0,0, so should generally be ViewGroups 27 | * themselves (RelativeLayouts or FrameLayouts are generally appropriate). 28 | */ 29 | 30 | public class ZoomPanLayout extends ViewGroup { 31 | 32 | private static final int MINIMUM_VELOCITY = 50; 33 | private static final int ZOOM_ANIMATION_DURATION = 500; 34 | private static final int SLIDE_DURATION = 500; 35 | private static final int VELOCITY_UNITS = 1000; 36 | private static final int DOUBLE_TAP_TIME_THRESHOLD = 250; 37 | private static final int SINGLE_TAP_DISTANCE_THRESHOLD = 50; 38 | private static final double MINIMUM_PINCH_SCALE = 0.5; 39 | private static final float FRICTION = 0.99f; 40 | 41 | private int baseWidth; 42 | private int baseHeight; 43 | 44 | private int scaledWidth; 45 | private int scaledHeight; 46 | 47 | private double scale = 1; 48 | private double historicalScale = 1; 49 | 50 | private double minScale = 0; 51 | private double maxScale = 1; 52 | 53 | private boolean scaleToFit = true; 54 | 55 | private Point pinchStartScroll = new Point(); 56 | private Point pinchStartOffset = new Point(); 57 | private double pinchStartDistance; 58 | 59 | private Point doubleTapStartScroll = new Point(); 60 | private Point doubleTapStartOffset = new Point(); 61 | private double doubleTapDestinationScale; 62 | 63 | private Point firstFinger = new Point(); 64 | private Point secondFinger = new Point(); 65 | private Point lastFirstFinger = new Point(); 66 | private Point lastSecondFinger = new Point(); 67 | 68 | private Point scrollPosition = new Point(); 69 | 70 | private Point singleTapHistory = new Point(); 71 | private Point doubleTapHistory = new Point(); 72 | 73 | private Point actualPoint = new Point(); 74 | private Point destinationScroll = new Point(); 75 | 76 | private boolean secondFingerIsDown = false; 77 | private boolean firstFingerIsDown = false; 78 | 79 | private boolean isTapInterrupted = false; 80 | private boolean isBeingFlung = false; 81 | 82 | private long lastTouchedAt; 83 | 84 | private boolean shouldIntercept = false; 85 | 86 | private ScrollActionHandler scrollActionHandler; 87 | 88 | private Scroller scroller; 89 | private VelocityTracker velocity; 90 | 91 | private HashSet gestureListeners = new HashSet(); 92 | private HashSet zoomPanListeners = new HashSet(); 93 | 94 | private StaticLayout clip; 95 | 96 | private TweenListener tweenListener = new TweenListener() { 97 | @Override 98 | public void onTweenComplete() { 99 | isTweening = false; 100 | for ( ZoomPanListener listener : zoomPanListeners ) { 101 | listener.onZoomComplete( scale ); 102 | listener.onZoomPanEvent(); 103 | } 104 | } 105 | @Override 106 | public void onTweenProgress( double progress, double eased ) { 107 | double originalChange = doubleTapDestinationScale - historicalScale; 108 | double updatedChange = originalChange * eased; 109 | double currentScale = historicalScale + updatedChange; 110 | setScale( currentScale ); 111 | maintainScrollDuringScaleTween(); 112 | } 113 | @Override 114 | public void onTweenStart() { 115 | isTweening = true; 116 | for ( ZoomPanListener listener : zoomPanListeners ) { 117 | listener.onZoomStart( scale ); 118 | listener.onZoomPanEvent(); 119 | } 120 | } 121 | }; 122 | 123 | private boolean isTweening; 124 | private Tween tween = new Tween(); 125 | { 126 | tween.setAnimationEase( Strong.EaseOut ); 127 | tween.addTweenListener( tweenListener ); 128 | } 129 | 130 | /** 131 | * Constructor to use when creating a ZoomPanLayout from code. Inflating from XML is not currently supported. 132 | * @param context (Context) The Context the ZoomPanLayout is running in, through which it can access the current theme, resources, etc. 133 | */ 134 | public ZoomPanLayout( Context context ) { 135 | 136 | super( context ); 137 | setWillNotDraw( false ); 138 | 139 | scrollActionHandler = new ScrollActionHandler( this ); 140 | 141 | scroller = new Scroller( context ); 142 | scroller.setFriction( FRICTION ); 143 | 144 | clip = new StaticLayout( context ); 145 | super.addView( clip ); 146 | 147 | updateClip(); 148 | } 149 | 150 | //------------------------------------------------------------------------------------ 151 | // PUBLIC API 152 | //------------------------------------------------------------------------------------ 153 | 154 | /** 155 | * Determines whether the ZoomPanLayout should limit it's minimum scale to no less than what would be required to fill it's container 156 | * @param shouldScaleToFit (boolean) True to limit minimum scale, false to allow arbitrary minimum scale (see {@link setScaleLimits}) 157 | */ 158 | public void setScaleToFit( boolean shouldScaleToFit ) { 159 | scaleToFit = shouldScaleToFit; 160 | calculateMinimumScaleToFit(); 161 | } 162 | 163 | /** 164 | * Set minimum and maximum scale values for this ZoomPanLayout. 165 | * Note that if {@link shouldScaleToFit} is set to true, the minimum value set here will be ignored 166 | * Default values are 0 and 1. 167 | * @param min 168 | * @param max 169 | */ 170 | public void setScaleLimits( double min, double max ) { 171 | // if scaleToFit is set, don't allow overwrite 172 | if ( !scaleToFit ) { 173 | minScale = min; 174 | } 175 | maxScale = max; 176 | setScale( scale ); 177 | } 178 | 179 | /** 180 | * Sets whether the ZoomPanLayout should intercept touch events on it's child views. 181 | * If true, the ZoomPanLayout will intercept touch events, so that touch events on child views 182 | * will not consume the event, so gestures (drag, fling) on the ZoomPanLayout will not be interrupted. 183 | * If false, child views will consume touch events normally, and will interrupt gesture events on the 184 | * containing ZoomPanLayout 185 | * @param intercept (boolean) Boolean value indicating whether the ZoomPanLayout should intercept touch events 186 | */ 187 | public void setShouldIntercept( boolean intercept ){ 188 | shouldIntercept = intercept; 189 | } 190 | 191 | /** 192 | * Sets the size (width and height) of the ZoomPanLayout as it should be rendered at a scale of 1f (100%) 193 | * @param wide width 194 | * @param tall height 195 | */ 196 | public void setSize( int wide, int tall ) { 197 | baseWidth = wide; 198 | baseHeight = tall; 199 | scaledWidth = (int) ( baseWidth * scale ); 200 | scaledHeight = (int) ( baseHeight * scale ); 201 | updateClip(); 202 | } 203 | 204 | /** 205 | * Returns the base (un-scaled) width 206 | * @return (int) base width 207 | */ 208 | public int getBaseWidth() { 209 | return baseWidth; 210 | } 211 | 212 | /** 213 | * Returns the base (un-scaled) height 214 | * @return (int) base height 215 | */ 216 | public int getBaseHeight() { 217 | return baseHeight; 218 | } 219 | 220 | /** 221 | * Returns the scaled width 222 | * @return (int) scaled width 223 | */ 224 | public int getScaledWidth() { 225 | return scaledWidth; 226 | } 227 | 228 | /** 229 | * Returns the scaled height 230 | * @return (int) scaled height 231 | */ 232 | public int getScaledHeight() { 233 | return scaledHeight; 234 | } 235 | 236 | /** 237 | * Sets the scale (0-1) of the ZoomPanLayout 238 | * @param scale (double) The new value of the ZoomPanLayout scale 239 | */ 240 | public void setScale( double d ) { 241 | d = Math.max( d, minScale ); 242 | d = Math.min( d, maxScale ); 243 | if ( scale != d ) { 244 | scale = d; 245 | scaledWidth = (int) ( baseWidth * scale ); 246 | scaledHeight = (int) ( baseHeight * scale ); 247 | updateClip(); 248 | invalidate(); 249 | for ( ZoomPanListener listener : zoomPanListeners ) { 250 | listener.onScaleChanged( scale ); 251 | listener.onZoomPanEvent(); 252 | } 253 | } 254 | } 255 | 256 | /** 257 | * Retrieves the current scale of the ZoomPanLayout 258 | * @return (double) the current scale of the ZoomPanLayout 259 | */ 260 | public double getScale() { 261 | return scale; 262 | } 263 | 264 | /** 265 | * Returns whether the ZoomPanLayout is currently being flung 266 | * @return (boolean) true if the ZoomPanLayout is currently flinging, false otherwise 267 | */ 268 | public boolean isFlinging(){ 269 | return isBeingFlung; 270 | } 271 | 272 | /** 273 | * Returns the single child of the ZoomPanLayout, a ViewGroup that serves as an intermediary container 274 | * @return (View) The child view of the ZoomPanLayout that manages all contained views 275 | */ 276 | protected View getClip() { 277 | return clip; 278 | } 279 | 280 | /** 281 | * Adds a GestureListener to the ZoomPanLayout, which will receive gesture events 282 | * @param listener (GestureListener) Listener to add 283 | * @return (boolean) true when the listener set did not already contain the Listener, false otherwise 284 | */ 285 | public boolean addGestureListener( GestureListener listener ) { 286 | return gestureListeners.add( listener ); 287 | } 288 | 289 | /** 290 | * Removes a GestureListener from the ZoomPanLayout 291 | * @param listener (GestureListener) Listener to remove 292 | * @return (boolean) if the Listener was removed, false otherwise 293 | */ 294 | public boolean removeGestureListener( GestureListener listener ) { 295 | return gestureListeners.remove( listener ); 296 | } 297 | 298 | /** 299 | * Adds a ZoomPanListener to the ZoomPanLayout, which will receive events relating to zoom and pan actions 300 | * @param listener (ZoomPanListener) Listener to add 301 | * @return (boolean) true when the listener set did not already contain the Listener, false otherwise 302 | */ 303 | public boolean addZoomPanListener( ZoomPanListener listener ) { 304 | return zoomPanListeners.add( listener ); 305 | } 306 | 307 | /** 308 | * Removes a ZoomPanListener from the ZoomPanLayout 309 | * @param listener (ZoomPanListener) Listener to remove 310 | * @return (boolean) if the Listener was removed, false otherwise 311 | */ 312 | public boolean removeZoomPanListener( ZoomPanListener listener ) { 313 | return zoomPanListeners.remove( listener ); 314 | } 315 | 316 | /** 317 | * Scrolls the ZoomPanLayout to the x and y values specified by {@param point} Point 318 | * @param point (Point) Point instance containing the destination x and y values 319 | */ 320 | public void scrollToPoint( Point point ) { 321 | constrainPoint( point ); 322 | int ox = getScrollX(); 323 | int oy = getScrollY(); 324 | int nx = (int) point.x; 325 | int ny = (int) point.y; 326 | scrollTo( nx, ny ); 327 | if ( ox != nx || oy != ny ) { 328 | for ( ZoomPanListener listener : zoomPanListeners ) { 329 | listener.onScrollChanged( nx, ny ); 330 | listener.onZoomPanEvent(); 331 | } 332 | } 333 | } 334 | 335 | /** 336 | * Scrolls and centers the ZoomPanLayout to the x and y values specified by {@param point} Point 337 | * @param point (Point) Point instance containing the destination x and y values 338 | */ 339 | public void scrollToAndCenter( Point point ) { // TODO: 340 | int x = (int) -(getWidth() * 0.5); 341 | int y = (int) -(getHeight() * 0.5); 342 | point.offset( x , y ); 343 | scrollToPoint( point ); 344 | } 345 | 346 | /** 347 | * Scrolls the ZoomPanLayout to the x and y values specified by {@param point} Point using scrolling animation 348 | * @param point (Point) Point instance containing the destination x and y values 349 | */ 350 | public void slideToPoint( Point point ) { // TODO: 351 | constrainPoint( point ); 352 | int startX = getScrollX(); 353 | int startY = getScrollY(); 354 | int dx = point.x - startX; 355 | int dy = point.y - startY; 356 | scroller.startScroll( startX, startY, dx, dy, SLIDE_DURATION ); 357 | } 358 | 359 | /** 360 | * Scrolls and centers the ZoomPanLayout to the x and y values specified by {@param point} Point using scrolling animation 361 | * @param point (Point) Point instance containing the destination x and y values 362 | */ 363 | public void slideToAndCenter( Point point ) { // TODO: 364 | int x = (int) -(getWidth() * 0.5); 365 | int y = (int) -(getHeight() * 0.5); 366 | point.offset( x , y ); 367 | slideToPoint( point ); 368 | } 369 | 370 | /** 371 | * Adds a View to the intermediary ViewGroup that manages layout for the ZoomPanLayout. 372 | * This View will be laid out at the width and height specified by {@setSize} at 0, 0 373 | * @param child (View) The View to be added to the ZoomPanLayout view tree 374 | */ 375 | public void addChild( View child ) { 376 | LayoutParams lp = new LayoutParams( scaledWidth, scaledHeight ); 377 | clip.addView( child, lp ); 378 | } 379 | 380 | /** 381 | * Removes a View from the intermediary ViewGroup that manages layout for this ZoomPanLayout 382 | * @param child (View) The View to be removed 383 | */ 384 | public void removeChild( View child ) { 385 | if ( clip.indexOfChild( child ) > -1 ) { 386 | clip.removeView( child ); 387 | } 388 | } 389 | 390 | /** 391 | * Scales the ZoomPanLayout with animated progress 392 | * @param destination (double) The final scale to animate to 393 | * @param duration (int) The duration (in milliseconds) of the animation 394 | */ 395 | public void smoothScaleTo( double destination, int duration ) { 396 | if ( isTweening ) { 397 | return; 398 | } 399 | doubleTapDestinationScale = destination; 400 | tween.setDuration( duration ); 401 | tween.start(); 402 | } 403 | 404 | 405 | 406 | //------------------------------------------------------------------------------------ 407 | // PRIVATE/PROTECTED 408 | //------------------------------------------------------------------------------------ 409 | 410 | @Override 411 | protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec ) { 412 | measureChildren( widthMeasureSpec, heightMeasureSpec ); 413 | int w = clip.getMeasuredWidth(); 414 | int h = clip.getMeasuredHeight(); 415 | w = Math.max( w, getSuggestedMinimumWidth() ); 416 | h = Math.max( h, getSuggestedMinimumHeight() ); 417 | w = resolveSize( w, widthMeasureSpec ); 418 | h = resolveSize( h, heightMeasureSpec ); 419 | setMeasuredDimension( w, h ); 420 | } 421 | 422 | @Override 423 | protected void onLayout( boolean changed, int l, int t, int r, int b ) { 424 | clip.layout( 0, 0, clip.getMeasuredWidth(), clip.getMeasuredHeight() ); 425 | if ( changed ) { 426 | calculateMinimumScaleToFit(); 427 | } 428 | } 429 | 430 | 431 | 432 | private void calculateMinimumScaleToFit() { 433 | if ( scaleToFit ) { 434 | double minimumScaleX = getWidth() / (double) baseWidth; 435 | double minimumScaleY = getHeight() / (double) baseHeight; 436 | double recalculatedMinScale = Math.max( minimumScaleX, minimumScaleY ); 437 | if ( recalculatedMinScale != minScale ) { 438 | minScale = recalculatedMinScale; 439 | setScale( scale ); 440 | } 441 | } 442 | } 443 | 444 | 445 | private void updateClip() { 446 | updateViewClip( clip ); 447 | for ( int i = 0; i < clip.getChildCount(); i++ ) { 448 | View child = clip.getChildAt( i ); 449 | updateViewClip( child ); 450 | } 451 | constrainScroll(); 452 | } 453 | 454 | private void updateViewClip( View v ) { 455 | LayoutParams lp = v.getLayoutParams(); 456 | lp.width = scaledWidth; 457 | lp.height = scaledHeight; 458 | v.setLayoutParams( lp ); 459 | } 460 | 461 | 462 | @Override 463 | public void computeScroll() { 464 | if ( scroller.computeScrollOffset() ) { 465 | Point destination = new Point( scroller.getCurrX(), scroller.getCurrY() ); 466 | scrollToPoint( destination ); 467 | postInvalidate(); // should not be necessary but is... 468 | dispatchScrollActionNotification(); 469 | } 470 | } 471 | 472 | private void dispatchScrollActionNotification(){ 473 | if ( scrollActionHandler.hasMessages( 0 )) { 474 | scrollActionHandler.removeMessages( 0 ); 475 | } 476 | scrollActionHandler.sendEmptyMessageDelayed( 0, 100 ); 477 | } 478 | 479 | private void handleScrollerAction() { 480 | Point point = new Point(); 481 | point.x = getScrollX(); 482 | point.y = getScrollY(); 483 | for( GestureListener listener : gestureListeners ) { 484 | listener.onScrollComplete( point ); 485 | } 486 | if ( isBeingFlung ) { 487 | isBeingFlung = false; 488 | for( GestureListener listener : gestureListeners ) { 489 | listener.onFlingComplete( point ); 490 | } 491 | } 492 | } 493 | 494 | private void constrainPoint( Point point ) { 495 | int x = point.x; 496 | int y = point.y; 497 | int mx = Math.max( 0, Math.min( x, getLimitX() ) ); 498 | int my = Math.max( 0, Math.min( y, getLimitY() ) ); 499 | if ( x != mx || y != my ) { 500 | point.set( mx, my ); 501 | } 502 | } 503 | 504 | 505 | 506 | private void constrainScroll() { // TODO: 507 | Point currentScroll = new Point( getScrollX(), getScrollY() ); 508 | Point limitScroll = new Point( currentScroll ); 509 | constrainPoint( limitScroll ); 510 | if ( !currentScroll.equals( limitScroll ) ) { 511 | scrollToPoint( currentScroll ); 512 | } 513 | } 514 | 515 | private int getLimitX() { 516 | return scaledWidth - getWidth(); 517 | } 518 | 519 | private int getLimitY() { 520 | return scaledHeight - getHeight(); 521 | } 522 | 523 | 524 | @Override 525 | public void addView( View child ) { 526 | throw new UnsupportedOperationException( "ZoomPanLayout does not allow direct addition of child views. Use addChild() instead." ); 527 | } 528 | 529 | @Override 530 | public void removeView( View child ) { 531 | throw new UnsupportedOperationException( "ZoomPanLayout does not allow direct removal of child views. Use removeChild() instead." ); 532 | } 533 | 534 | 535 | private void saveHistoricalScale() { 536 | historicalScale = scale; 537 | } 538 | 539 | private void savePinchHistory() { 540 | int x = (int) ( ( firstFinger.x + secondFinger.x ) * 0.5 ); 541 | int y = (int) ( ( firstFinger.y + secondFinger.y ) * 0.5 ); 542 | pinchStartOffset.set( x , y ); 543 | pinchStartScroll.set( getScrollX(), getScrollY() ); 544 | pinchStartScroll.offset( x, y ); 545 | } 546 | 547 | private void maintainScrollDuringPinchOperation() { 548 | double deltaScale = scale / historicalScale; 549 | int x = (int) ( pinchStartScroll.x * deltaScale ) - pinchStartOffset.x; 550 | int y = (int) ( pinchStartScroll.y * deltaScale ) - pinchStartOffset.y; 551 | destinationScroll.set( x, y ); 552 | scrollToPoint( destinationScroll ); 553 | } 554 | 555 | private void saveDoubleTapHistory() { 556 | doubleTapStartOffset.set( firstFinger.x, firstFinger.y ); 557 | doubleTapStartScroll.set( getScrollX(), getScrollY() ); 558 | doubleTapStartScroll.offset( doubleTapStartOffset.x, doubleTapStartOffset.y ); 559 | } 560 | 561 | private void maintainScrollDuringScaleTween() { 562 | double deltaScale = scale / historicalScale; 563 | int x = (int) ( doubleTapStartScroll.x * deltaScale ) - doubleTapStartOffset.x; 564 | int y = (int) ( doubleTapStartScroll.y * deltaScale ) - doubleTapStartOffset.y; 565 | destinationScroll.set( x, y ); 566 | scrollToPoint( destinationScroll ); 567 | } 568 | 569 | private void saveHistoricalPinchDistance() { 570 | int dx = firstFinger.x - secondFinger.x; 571 | int dy = firstFinger.y - secondFinger.y; 572 | pinchStartDistance = Math.sqrt( dx * dx + dy * dy ); 573 | } 574 | 575 | private void setScaleFromPinch() { 576 | int dx = firstFinger.x - secondFinger.x; 577 | int dy = firstFinger.y - secondFinger.y; 578 | double pinchCurrentDistance = Math.sqrt( dx * dx + dy * dy ); 579 | double currentScale = pinchCurrentDistance / pinchStartDistance; 580 | currentScale = Math.max( currentScale, MINIMUM_PINCH_SCALE ); 581 | currentScale = historicalScale * currentScale; 582 | setScale( currentScale ); 583 | } 584 | 585 | private void performDrag() { 586 | Point delta = new Point(); 587 | if ( secondFingerIsDown && !firstFingerIsDown ) { 588 | delta.set( lastSecondFinger.x, lastSecondFinger.y ); 589 | delta.offset( -secondFinger.x, -secondFinger.y ); 590 | } else { 591 | delta.set( lastFirstFinger.x, lastFirstFinger.y ); 592 | delta.offset( -firstFinger.x, -firstFinger.y ); 593 | } 594 | scrollPosition.offset( delta.x, delta.y ); 595 | scrollToPoint( scrollPosition ); 596 | } 597 | 598 | private boolean performFling() { 599 | if ( secondFingerIsDown ) { 600 | return false; 601 | } 602 | velocity.computeCurrentVelocity( VELOCITY_UNITS ); 603 | double xv = velocity.getXVelocity(); 604 | double yv = velocity.getYVelocity(); 605 | double totalVelocity = Math.abs( xv ) + Math.abs( yv ); 606 | if ( totalVelocity > MINIMUM_VELOCITY ) { 607 | scroller.fling( getScrollX(), getScrollY(), (int) -xv, (int) -yv, 0, getLimitX(), 0, getLimitY() ); 608 | invalidate(); 609 | return true; 610 | } 611 | return false; 612 | } 613 | 614 | // if the taps occurred within threshold, it's a double tap 615 | private boolean determineIfQualifiedDoubleTap(){ 616 | long now = System.currentTimeMillis(); 617 | long ellapsed = now - lastTouchedAt; 618 | lastTouchedAt = now; 619 | return ( ellapsed <= DOUBLE_TAP_TIME_THRESHOLD ) 620 | && ( Math.abs( firstFinger.x - doubleTapHistory.x ) <= SINGLE_TAP_DISTANCE_THRESHOLD ) 621 | && ( Math.abs( firstFinger.y - doubleTapHistory.y ) <= SINGLE_TAP_DISTANCE_THRESHOLD ); 622 | 623 | } 624 | 625 | private void saveTapActionOrigination(){ 626 | singleTapHistory.set( firstFinger.x, firstFinger.y ); 627 | } 628 | 629 | private void saveDoubleTapOrigination(){ 630 | doubleTapHistory.set( firstFinger.x, firstFinger.y ); 631 | } 632 | 633 | private void setTapInterrupted( boolean v ){ 634 | isTapInterrupted = v; 635 | } 636 | 637 | // if the touch event has traveled past threshold since the finger first when down, it's not a tap 638 | private boolean determineIfQualifiedSingleTap(){ 639 | return !isTapInterrupted 640 | && ( Math.abs( firstFinger.x - singleTapHistory.x ) <= SINGLE_TAP_DISTANCE_THRESHOLD ) 641 | && ( Math.abs( firstFinger.y - singleTapHistory.y ) <= SINGLE_TAP_DISTANCE_THRESHOLD ); 642 | } 643 | 644 | private void processEvent( MotionEvent event ) { 645 | 646 | // copy for history 647 | lastFirstFinger.set( firstFinger.x, firstFinger.y ); 648 | lastSecondFinger.set( secondFinger.x, secondFinger.y ); 649 | 650 | // set false for now 651 | firstFingerIsDown = false; 652 | secondFingerIsDown = false; 653 | 654 | // determine which finger is down and populate the appropriate points 655 | for ( int i = 0; i < event.getPointerCount(); i++ ) { 656 | int id = event.getPointerId( i ); 657 | int x = (int) event.getX( i ); 658 | int y = (int) event.getY( i ); 659 | switch ( id ) { 660 | case 0 : 661 | firstFingerIsDown = true; 662 | firstFinger.set( x, y ); 663 | actualPoint.set( x, y ); 664 | break; 665 | case 1 : 666 | secondFingerIsDown = true; 667 | secondFinger.set( x, y ); 668 | actualPoint.set( x, y ); 669 | break; 670 | } 671 | } 672 | // record scroll position and adjust finger point to account for scroll offset 673 | scrollPosition.set( getScrollX(), getScrollY() ); 674 | actualPoint.offset( scrollPosition.x, scrollPosition.y ); 675 | 676 | // update velocity for flinging 677 | // TODO: this can probably be moved to the ACTION_MOVE switch 678 | if ( velocity == null ) { 679 | velocity = VelocityTracker.obtain(); 680 | } 681 | velocity.addMovement( event ); 682 | } 683 | 684 | @Override 685 | public boolean onInterceptTouchEvent (MotionEvent event) { 686 | // update positions 687 | processEvent( event); 688 | // if we wan't to intercept events (and allow drag on children)... 689 | if ( shouldIntercept ) { 690 | // get the type of action 691 | final int action = event.getAction() & MotionEvent.ACTION_MASK; 692 | // if it's a move event... 693 | if ( action == MotionEvent.ACTION_MOVE ) { 694 | // and capture it (so touch listeners on the children don't consume it and prevent scrolling) 695 | return true; 696 | } 697 | // otherwise, let the child handle it 698 | } 699 | return false; 700 | } 701 | 702 | @Override 703 | public boolean onTouchEvent( MotionEvent event ) { 704 | // update positions 705 | processEvent( event ); 706 | // get the type of action 707 | final int action = event.getAction() & MotionEvent.ACTION_MASK; 708 | // react based on nature of touch event 709 | switch ( action ) { 710 | // first finger goes down 711 | case MotionEvent.ACTION_DOWN : 712 | if ( !scroller.isFinished() ) { 713 | scroller.abortAnimation(); 714 | } 715 | isBeingFlung = false; 716 | setTapInterrupted( false ); 717 | saveTapActionOrigination(); 718 | for ( GestureListener listener : gestureListeners ) { 719 | listener.onFingerDown( actualPoint ); 720 | } 721 | break; 722 | // second finger goes down 723 | case MotionEvent.ACTION_POINTER_DOWN : 724 | setTapInterrupted( true ); 725 | saveHistoricalPinchDistance(); 726 | saveHistoricalScale(); 727 | savePinchHistory(); 728 | for ( GestureListener listener : gestureListeners ) { 729 | listener.onFingerDown( actualPoint ); 730 | } 731 | for ( GestureListener listener : gestureListeners ) { 732 | listener.onPinchStart( pinchStartOffset ); 733 | } 734 | for ( ZoomPanListener listener : zoomPanListeners ) { 735 | listener.onZoomStart( scale ); 736 | listener.onZoomPanEvent(); 737 | } 738 | break; 739 | // either finger moves 740 | case MotionEvent.ACTION_MOVE : 741 | // if both fingers are down, that means it's a pinch 742 | if ( firstFingerIsDown && secondFingerIsDown ) { 743 | setScaleFromPinch(); 744 | maintainScrollDuringPinchOperation(); 745 | for ( GestureListener listener : gestureListeners ) { 746 | listener.onPinch( pinchStartOffset ); 747 | } 748 | // otherwise it's a drag 749 | } else { 750 | performDrag(); 751 | for ( GestureListener listener : gestureListeners ) { 752 | listener.onDrag( actualPoint ); 753 | } 754 | } 755 | break; 756 | // first finger goes up 757 | case MotionEvent.ACTION_UP : 758 | if ( performFling() ) { 759 | isBeingFlung = true; 760 | Point startPoint = new Point( getScrollX(), getScrollY() ); 761 | Point finalPoint = new Point( scroller.getFinalX(), scroller.getFinalY() ); 762 | for ( GestureListener listener : gestureListeners ) { 763 | listener.onFling( startPoint, finalPoint ); 764 | } 765 | } 766 | if ( velocity != null ) { 767 | velocity.recycle(); 768 | velocity = null; 769 | } 770 | // could be a single tap... 771 | if ( determineIfQualifiedSingleTap() ){ 772 | for ( GestureListener listener : gestureListeners ) { 773 | listener.onTap( actualPoint ); 774 | } 775 | } 776 | // or a double tap 777 | if ( determineIfQualifiedDoubleTap() ) { 778 | saveHistoricalScale(); 779 | saveDoubleTapHistory(); 780 | double destination = Math.min( 1, scale * 2 ); 781 | smoothScaleTo( destination, ZOOM_ANIMATION_DURATION ); 782 | for ( GestureListener listener : gestureListeners ) { 783 | listener.onDoubleTap( actualPoint ); 784 | } 785 | } 786 | // either way it's a finger up event 787 | for ( GestureListener listener : gestureListeners ) { 788 | listener.onFingerUp( actualPoint ); 789 | } 790 | // save coordinates to measure against the next double tap 791 | saveDoubleTapOrigination(); 792 | break; 793 | // second finger goes up 794 | case MotionEvent.ACTION_POINTER_UP : 795 | setTapInterrupted( true ); 796 | for ( GestureListener listener : gestureListeners ) { 797 | listener.onFingerUp( actualPoint ); 798 | } 799 | for ( GestureListener listener : gestureListeners ) { 800 | listener.onPinchComplete( pinchStartOffset ); 801 | } 802 | for ( ZoomPanListener listener : zoomPanListeners ) { 803 | listener.onZoomComplete( scale ); 804 | listener.onZoomPanEvent(); 805 | } 806 | break; 807 | 808 | } 809 | 810 | return true; 811 | 812 | } 813 | 814 | private static class ScrollActionHandler extends Handler { 815 | private final WeakReference reference; 816 | public ScrollActionHandler( ZoomPanLayout zoomPanLayout ) { 817 | super(); 818 | reference = new WeakReference( zoomPanLayout ); 819 | } 820 | @Override 821 | public void handleMessage( Message msg ) { 822 | ZoomPanLayout zoomPanLayout = reference.get(); 823 | if ( zoomPanLayout != null ) { 824 | zoomPanLayout.handleScrollerAction(); 825 | } 826 | } 827 | } 828 | 829 | //------------------------------------------------------------------------------------ 830 | // Public static interfaces and classes 831 | //------------------------------------------------------------------------------------ 832 | 833 | public static interface ZoomPanListener { 834 | public void onScaleChanged( double scale ); 835 | public void onScrollChanged( int x, int y ); 836 | public void onZoomStart( double scale ); 837 | public void onZoomComplete( double scale ); 838 | public void onZoomPanEvent(); 839 | } 840 | 841 | public static interface GestureListener { 842 | public void onFingerDown( Point point ); 843 | public void onScrollComplete( Point point ); 844 | public void onFingerUp( Point point ); 845 | public void onDrag( Point point ); 846 | public void onDoubleTap( Point point ); 847 | public void onTap( Point point ); 848 | public void onPinch( Point point ); 849 | public void onPinchStart( Point point ); 850 | public void onPinchComplete( Point point ); 851 | public void onFling( Point startPoint, Point finalPoint ); 852 | public void onFlingComplete( Point point ); 853 | } 854 | 855 | } 856 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/geom/ManagedGeolocator.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.geom; 2 | 3 | import android.util.Log; 4 | 5 | import com.qozix.geom.Geolocator; 6 | import com.qozix.mapview.zoom.ZoomListener; 7 | import com.qozix.mapview.zoom.ZoomManager; 8 | 9 | public class ManagedGeolocator extends Geolocator implements ZoomListener { 10 | 11 | private ZoomManager zoomManager; 12 | 13 | public ManagedGeolocator( ZoomManager zm ){ 14 | zoomManager = zm; 15 | zoomManager.addZoomListener( this ); 16 | update(); 17 | } 18 | 19 | private void update(){ 20 | int w = zoomManager.getComputedCurrentWidth(); 21 | int h = zoomManager.getComputedCurrentHeight(); 22 | setSize( w, h ); 23 | } 24 | 25 | @Override 26 | public void onZoomLevelChanged( int oldZoom, int newZoom ) { 27 | update(); 28 | } 29 | 30 | @Override 31 | public void onZoomScaleChanged( double scale ) { 32 | 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/hotspots/HotSpot.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.hotspots; 2 | 3 | import android.graphics.Rect; 4 | import android.view.View; 5 | 6 | public class HotSpot { 7 | public Rect area; 8 | public View.OnClickListener listener; 9 | public HotSpot( Rect r, View.OnClickListener l ){ 10 | area = r; 11 | listener = l; 12 | } 13 | @Override 14 | public boolean equals( Object o ){ 15 | if(o instanceof HotSpot ){ 16 | HotSpot h = (HotSpot) o; 17 | return listener == h.listener && area.equals( h.area ); 18 | } 19 | return false; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/hotspots/HotSpotManager.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.hotspots; 2 | 3 | import java.util.Iterator; 4 | import java.util.LinkedList; 5 | 6 | import android.graphics.Point; 7 | import android.graphics.Rect; 8 | import android.view.View; 9 | 10 | public class HotSpotManager { 11 | 12 | public LinkedList spots = new LinkedList(); 13 | 14 | public void addHotSpot( Rect r, View.OnClickListener l ){ 15 | HotSpot hotSpot = new HotSpot( r, l ); 16 | spots.add( hotSpot ); 17 | } 18 | 19 | public void removeHotSpot( Rect r, View.OnClickListener l ){ 20 | HotSpot comparison = new HotSpot( r, l ); 21 | Iterator iterator = spots.iterator(); 22 | while(iterator.hasNext()){ 23 | HotSpot hotSpot = iterator.next(); 24 | if(comparison.equals( hotSpot )){ 25 | iterator.remove(); 26 | } 27 | } 28 | } 29 | 30 | public void clear(){ 31 | spots.clear(); 32 | } 33 | 34 | // work from end of list - match the last one added (equivalant to z-index) 35 | private HotSpot getMatch( Point point ){ 36 | for(int i = spots.size(); i > 0; i--){ 37 | HotSpot hotSpot = spots.get( i - 1); 38 | if(hotSpot.area.contains( point.x, point.y )){ 39 | return hotSpot; 40 | } 41 | } 42 | return null; 43 | } 44 | 45 | public void processHit( Point point ){ 46 | HotSpot match = getMatch( point ); 47 | if( match != null){ 48 | match.listener.onClick( null ); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/markers/CalloutManager.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.markers; 2 | 3 | import android.content.Context; 4 | import android.view.MotionEvent; 5 | import android.view.View; 6 | 7 | import com.qozix.mapview.zoom.ZoomManager; 8 | 9 | public class CalloutManager extends MarkerManager { 10 | 11 | public CalloutManager( Context context, ZoomManager zoomManager ) { 12 | super( context, zoomManager ); 13 | } 14 | 15 | private void clear(){ 16 | while( getChildCount() > 0 ) { 17 | View child = getChildAt( 0 ); 18 | removeView( child ); 19 | } 20 | } 21 | 22 | @Override 23 | public boolean onTouchEvent( MotionEvent event ) { 24 | clear(); 25 | return false; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/markers/MarkerManager.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.markers; 2 | 3 | import android.content.Context; 4 | import android.view.View; 5 | 6 | import com.qozix.layouts.TranslationLayout; 7 | import com.qozix.mapview.viewmanagers.ViewSetManager; 8 | import com.qozix.mapview.zoom.ZoomListener; 9 | import com.qozix.mapview.zoom.ZoomManager; 10 | 11 | /* 12 | * TODO: need to consolidate positioning logic - works as is, but does too many unnecessary and possibly messy calculations 13 | * should work with adding at runtime, in response to user event, sliding, etc. 14 | */ 15 | 16 | 17 | public class MarkerManager extends TranslationLayout implements ZoomListener { 18 | 19 | private ZoomManager zoomManager; 20 | private ViewSetManager viewSetManager = new ViewSetManager(); 21 | 22 | public MarkerManager( Context context, ZoomManager zm ) { 23 | super( context ); 24 | zoomManager = zm; 25 | zoomManager.addZoomListener( this ); 26 | } 27 | 28 | public View addMarker( View v, int x, int y ){ 29 | LayoutParams lp = new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, x, y ); 30 | addView( v, lp ); 31 | return v; 32 | } 33 | 34 | public View addMarker( View v, int x, int y, float aX, float aY ) { 35 | LayoutParams lp = new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, x, y, aX, aY ); 36 | addView( v, lp ); 37 | return v; 38 | } 39 | 40 | public View addMarkerAtZoom( View v, int x, int y, int z ){ 41 | addMarker( v, x, y ); 42 | viewSetManager.addViewAtLevel( v, z ); 43 | filterMarkers(); 44 | return v; 45 | } 46 | 47 | public View addMarkerAtZoom( View v, int x, int y, float aX, float aY, int z ) { 48 | addMarker( v, x, y, aX, aY ); 49 | viewSetManager.addViewAtLevel( v, z ); 50 | filterMarkers(); 51 | return v; 52 | } 53 | 54 | public void filterMarkers(){ 55 | int zoom = zoomManager.getZoom(); 56 | viewSetManager.purgeViewSets(); 57 | viewSetManager.updateDisplay( zoom ); 58 | } 59 | 60 | @Override 61 | public void onZoomLevelChanged( int oldZoom, int newZoom ) { 62 | filterMarkers(); 63 | } 64 | 65 | @Override 66 | public void onZoomScaleChanged( double scale ) { 67 | setScale( scale ); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/paths/PathManager.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.paths; 2 | 3 | import java.util.List; 4 | 5 | import android.content.Context; 6 | import android.graphics.Point; 7 | import android.view.View; 8 | 9 | import com.qozix.layouts.StaticLayout; 10 | import com.qozix.mapview.viewmanagers.ViewSetManager; 11 | import com.qozix.mapview.zoom.ZoomListener; 12 | import com.qozix.mapview.zoom.ZoomManager; 13 | 14 | public class PathManager extends StaticLayout implements ZoomListener { 15 | 16 | private double scale = 1; 17 | private ZoomManager zoomManager; 18 | private ViewSetManager viewSetManager = new ViewSetManager(); 19 | 20 | public PathManager( Context context, ZoomManager zm ) { 21 | super( context ); 22 | zoomManager = zm; 23 | zoomManager.addZoomListener( this ); 24 | } 25 | 26 | public void setScale( double s ){ 27 | scale = s; 28 | for(int i = 0; i < getChildCount(); i++){ 29 | View child = getChildAt( i ); 30 | if(child instanceof PathView){ 31 | PathView pathView = (PathView) child; 32 | pathView.setScale( scale ); 33 | } 34 | } 35 | } 36 | 37 | public View drawPath( List points ) { 38 | PathView pathView = new PathView( getContext() ); 39 | pathView.setScale( scale ); 40 | pathView.drawPath( points ); 41 | addView( pathView ); 42 | return pathView; 43 | } 44 | 45 | public View drawPathAtZoom( List points, int zoom ){ 46 | View pathView = drawPath( points ); 47 | viewSetManager.addViewAtLevel( pathView, zoom ); 48 | filterPathViews(); 49 | return pathView; 50 | } 51 | 52 | public void filterPathViews(){ 53 | int zoom = zoomManager.getZoom(); 54 | viewSetManager.purgeViewSets(); 55 | viewSetManager.updateDisplay( zoom ); 56 | } 57 | 58 | @Override 59 | public void onZoomLevelChanged( int oldZoom, int newZoom ) { 60 | filterPathViews(); 61 | } 62 | 63 | @Override 64 | public void onZoomScaleChanged( double scale ) { 65 | setScale( scale ); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/paths/PathView.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.paths; 2 | 3 | import java.util.List; 4 | 5 | import android.content.Context; 6 | import android.graphics.Canvas; 7 | import android.graphics.CornerPathEffect; 8 | import android.graphics.Matrix; 9 | import android.graphics.Paint; 10 | import android.graphics.Path; 11 | import android.graphics.Point; 12 | import android.view.View; 13 | 14 | 15 | public class PathView extends View { 16 | 17 | private static final int DEFAULT_COLOR = 0xBB489FFF; 18 | 19 | private Paint paint = new Paint(); 20 | private Path originalPath = new Path(); 21 | private Path drawingPath = new Path(); 22 | 23 | private double scale = 1; 24 | 25 | public PathView( Context context ) { 26 | super( context ); 27 | setWillNotDraw( false ); 28 | paint.setStyle( Paint.Style.STROKE ); 29 | paint.setAntiAlias( true ); 30 | paint.setColor( DEFAULT_COLOR ); 31 | paint.setStrokeWidth( 7 ); 32 | paint.setShadowLayer( 4, 2, 2, 0x66000000 ); 33 | paint.setPathEffect( new CornerPathEffect( 5 ) ); 34 | } 35 | 36 | public void setColor( int c ) { 37 | paint.setColor( c ); 38 | invalidate(); 39 | } 40 | 41 | public void setCornerRadii( float r ) { 42 | paint.setPathEffect( new CornerPathEffect( r ) ); 43 | invalidate(); 44 | } 45 | 46 | public void setShadowLayer(float radius, float dx, float dy, int color){ 47 | paint.setShadowLayer( radius, dx, dy, color ); 48 | } 49 | 50 | public void setStrokeWidth( float w ){ 51 | paint.setStrokeWidth( w ); 52 | } 53 | 54 | public double getScale() { 55 | return scale; 56 | } 57 | 58 | public Paint getPaint(){ 59 | return paint; 60 | } 61 | 62 | public void setScale( double s ) { 63 | float factor = (float) s; 64 | Matrix matrix = new Matrix(); 65 | drawingPath.set( originalPath ); 66 | matrix.setScale( factor, factor ); 67 | originalPath.transform( matrix, drawingPath ); 68 | scale = s; 69 | invalidate(); 70 | } 71 | 72 | public void drawPath( List points ) { 73 | Point start = points.get( 0 ); 74 | originalPath.reset(); 75 | originalPath.moveTo( (float) start.x, (float) start.y ); 76 | int l = points.size(); 77 | for ( int i = 1; i < l; i++ ) { 78 | Point p = points.get( i ); 79 | originalPath.lineTo( (float) p.x, (float) p.y ); 80 | } 81 | drawingPath.set( originalPath ); 82 | invalidate(); 83 | } 84 | 85 | @Override 86 | public void onDraw( Canvas canvas ) { 87 | canvas.drawPath( drawingPath, paint ); 88 | super.onDraw( canvas ); 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /src/com/qozix/mapview/tiles/MapTile.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.tiles; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | 6 | import android.content.Context; 7 | import android.content.res.AssetManager; 8 | import android.graphics.Bitmap; 9 | import android.graphics.BitmapFactory; 10 | import android.util.Log; 11 | import android.view.ViewGroup; 12 | import android.view.ViewParent; 13 | import android.widget.ImageView; 14 | 15 | public class MapTile { 16 | 17 | private static final String TAG = MapTile.class.getSimpleName(); 18 | 19 | private static final BitmapFactory.Options OPTIONS = new BitmapFactory.Options(); 20 | static { 21 | OPTIONS.inPreferredConfig = Bitmap.Config.RGB_565; 22 | } 23 | 24 | private int zoom; 25 | 26 | private int row; 27 | private int column; 28 | private int left; 29 | private int top; 30 | private int width; 31 | private int height; 32 | private int right; 33 | private int bottom; 34 | 35 | private String pattern; 36 | 37 | private ImageView imageView; 38 | private Bitmap bitmap; 39 | 40 | private boolean hasBitmap; 41 | 42 | public MapTile() { 43 | 44 | } 45 | 46 | public MapTile( int z, int r, int c, int w, int h, String p ) { 47 | set( z, r, c, w, h, p ); 48 | } 49 | 50 | public void set( int z, int r, int c, int w, int h, String p ) { 51 | zoom = z; 52 | row = r; 53 | column = c; 54 | width = w; 55 | height = h; 56 | top = r * h; 57 | left = c * w; 58 | right = left + w; 59 | bottom = top + h; 60 | pattern = p; 61 | } 62 | 63 | public int getRow() { 64 | return row; 65 | } 66 | 67 | public int getColumn() { 68 | return column; 69 | } 70 | 71 | public int getLeft() { 72 | return left; 73 | } 74 | 75 | public int getTop() { 76 | return top; 77 | } 78 | 79 | public int getWidth() { 80 | return width; 81 | } 82 | 83 | public int getHeight() { 84 | return height; 85 | } 86 | 87 | public int getBottom() { 88 | return bottom; 89 | } 90 | 91 | public int getRight() { 92 | return right; 93 | } 94 | 95 | public int getZoom() { 96 | return zoom; 97 | } 98 | 99 | public ImageView getImageView() { 100 | return imageView; 101 | } 102 | 103 | public String getFileName() { 104 | return pattern.replace( "%col%", Integer.toString( column ) ).replace( "%row%", Integer.toString( row ) ); 105 | } 106 | 107 | public void decode( Context context, MapTileCache cache, MapTileDecoder decoder ) { 108 | if ( hasBitmap ) { 109 | return; 110 | } 111 | String fileName = getFileName(); 112 | if ( cache != null ) { 113 | Bitmap cached = cache.getBitmap( fileName ); 114 | if ( cached != null ) { 115 | bitmap = cached; 116 | return; 117 | } 118 | } 119 | bitmap = decoder.decode( fileName, context ); 120 | hasBitmap = ( bitmap != null ); 121 | if ( cache != null ) { 122 | cache.addBitmap( fileName, bitmap ); 123 | } 124 | } 125 | 126 | public boolean render( Context context ) { 127 | if ( imageView == null ) { 128 | imageView = new ImageView( context ); 129 | imageView.setAdjustViewBounds( false ); 130 | imageView.setScaleType( ImageView.ScaleType.MATRIX ); 131 | } 132 | imageView.setImageBitmap( bitmap ); 133 | return true; 134 | } 135 | 136 | public void destroy() { 137 | if ( imageView != null ) { 138 | imageView.setImageBitmap( null ); 139 | ViewParent parent = imageView.getParent(); 140 | if ( parent != null && parent instanceof ViewGroup ) { 141 | ViewGroup group = (ViewGroup) parent; 142 | group.removeView( imageView ); 143 | } 144 | imageView = null; 145 | } 146 | hasBitmap = false; 147 | bitmap = null; 148 | } 149 | 150 | @Override 151 | public boolean equals( Object o ) { 152 | if ( o instanceof MapTile ) { 153 | MapTile m = (MapTile) o; 154 | return ( m.getRow() == getRow() ) 155 | && ( m.getColumn() == getColumn() ) 156 | && ( m.getZoom() == getZoom() ); 157 | } 158 | return false; 159 | } 160 | 161 | @Override 162 | public String toString() { 163 | return "(left=" + left + ", top=" + top + ", right=" + right + ", bottom=" + bottom + ")"; 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/tiles/MapTileCache.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.tiles; 2 | 3 | import java.io.BufferedInputStream; 4 | import java.io.BufferedOutputStream; 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.OutputStream; 9 | import java.math.BigInteger; 10 | import java.security.MessageDigest; 11 | import java.security.NoSuchAlgorithmException; 12 | 13 | import android.content.Context; 14 | import android.graphics.Bitmap; 15 | import android.graphics.BitmapFactory; 16 | import android.graphics.Bitmap.CompressFormat; 17 | import android.support.v4.util.LruCache; 18 | 19 | import com.jakewharton.DiskLruCache; 20 | 21 | public class MapTileCache { 22 | 23 | private static final int DISK_CACHE_CAPACITY = 8 * 1024; 24 | private static final int IO_BUFFER_SIZE = 8 * 1024; 25 | 26 | private static final int COMPRESSION_QUALITY = 40; 27 | 28 | private static final BitmapFactory.Options BITMAPFACTORY_OPTIONS = new BitmapFactory.Options(); 29 | static { 30 | BITMAPFACTORY_OPTIONS.inPreferredConfig = Bitmap.Config.RGB_565; 31 | } 32 | 33 | private MessageDigest digest; 34 | 35 | private LruCache memoryCache; 36 | private DiskLruCache diskCache; 37 | 38 | // TODO: register local broadcast receiver to destroy the cache during onDestroy of containing Activity 39 | public MapTileCache( final Context context ) { 40 | try { 41 | digest = MessageDigest.getInstance( "MD5" ); 42 | } catch ( NoSuchAlgorithmException e1 ) { 43 | 44 | } 45 | // in memory cache 46 | final int memory = (int) ( Runtime.getRuntime().maxMemory() / 1024 ); 47 | final int size = memory / 8; 48 | memoryCache = new LruCache( size ) { 49 | @Override 50 | protected int sizeOf( String key, Bitmap bitmap ) { 51 | // The cache size will be measured in kilobytes rather than number of items. 52 | // emulate bitmap.getByteCount for APIs less than 12 53 | int byteCount = bitmap.getRowBytes() * bitmap.getHeight(); 54 | return byteCount / 1024; 55 | } 56 | }; 57 | // disk cache 58 | new Thread(new Runnable(){ 59 | @Override 60 | public void run(){ 61 | File cacheDirectory = new File( context.getCacheDir().getPath() + File.separator + "com/qozix/mapview" ); 62 | try { 63 | diskCache = DiskLruCache.open( cacheDirectory, 1, 1, DISK_CACHE_CAPACITY ); 64 | } catch ( IOException e ) { 65 | 66 | } 67 | } 68 | }); 69 | } 70 | 71 | public void addBitmap( String key, Bitmap bitmap ) { 72 | addBitmapToMemoryCache( key, bitmap ); 73 | addBitmapToDiskCache( key, bitmap ); 74 | } 75 | 76 | public Bitmap getBitmap( String key ) { 77 | Bitmap bitmap = getBitmapFromMemoryCache( key ); 78 | if ( bitmap == null ) { 79 | bitmap = getBitmapFromDiskCache( key ); 80 | } 81 | return bitmap; 82 | } 83 | 84 | public void destroy(){ 85 | memoryCache.evictAll(); 86 | memoryCache = null; 87 | new Thread(new Runnable(){ 88 | @Override 89 | public void run(){ 90 | try { 91 | diskCache.delete(); 92 | diskCache = null; 93 | } catch ( IOException e ) { 94 | 95 | } 96 | } 97 | }); 98 | } 99 | 100 | public void clear() { 101 | memoryCache.evictAll(); 102 | new Thread(new Runnable(){ 103 | @Override 104 | public void run(){ 105 | try { 106 | diskCache.delete(); 107 | } catch ( IOException e ) { 108 | 109 | } 110 | } 111 | }); 112 | } 113 | 114 | private void addBitmapToMemoryCache( String key, Bitmap bitmap ) { 115 | if ( getBitmapFromMemoryCache( key ) == null ) { 116 | memoryCache.put( key, bitmap ); 117 | } 118 | } 119 | 120 | private Bitmap getBitmapFromMemoryCache( String key ) { 121 | return memoryCache.get( key ); 122 | } 123 | 124 | private void addBitmapToDiskCache( String key, Bitmap bitmap ) { 125 | if ( diskCache == null ) { 126 | return; 127 | } 128 | key = getMD5( key ); 129 | DiskLruCache.Editor editor = null; 130 | try { 131 | editor = diskCache.edit( key ); 132 | if ( editor == null ) { 133 | return; 134 | } 135 | OutputStream output = null; 136 | try { 137 | output = new BufferedOutputStream( editor.newOutputStream( 0 ), IO_BUFFER_SIZE ); 138 | boolean compressed = bitmap.compress( CompressFormat.JPEG, COMPRESSION_QUALITY, output ); 139 | if ( compressed ) { 140 | diskCache.flush(); 141 | editor.commit(); 142 | } else { 143 | editor.abort(); 144 | } 145 | } finally { 146 | if ( output != null ) { 147 | output.close(); 148 | } 149 | } 150 | } catch ( IOException e ) { 151 | try { 152 | if ( editor != null ) { 153 | editor.abort(); 154 | } 155 | } catch ( IOException io ) { 156 | 157 | } 158 | } 159 | } 160 | 161 | private Bitmap getBitmapFromDiskCache( String key ) { 162 | if ( diskCache == null ) { 163 | return null; 164 | } 165 | key = getMD5( key ); 166 | Bitmap bitmap = null; 167 | DiskLruCache.Snapshot snapshot = null; 168 | try { 169 | snapshot = diskCache.get( key ); 170 | if ( snapshot == null ) { 171 | return null; 172 | } 173 | final InputStream input = snapshot.getInputStream( 0 ); 174 | if ( input != null ) { 175 | BufferedInputStream buffered = new BufferedInputStream( input, IO_BUFFER_SIZE ); 176 | bitmap = BitmapFactory.decodeStream( buffered, null, BITMAPFACTORY_OPTIONS ); 177 | } 178 | } catch ( IOException e ) { 179 | 180 | } finally { 181 | if ( snapshot != null ) { 182 | snapshot.close(); 183 | } 184 | } 185 | return bitmap; 186 | } 187 | 188 | private String getMD5( String fileName ) { 189 | if ( digest != null ) { 190 | digest.update( fileName.getBytes(), 0, fileName.length() ); 191 | return new BigInteger( 1, digest.digest() ).toString( 16 ); 192 | } 193 | // if digest is unavailable, at least make some attempt at an acceptable filename 194 | return fileName.replace("[^a-z0-9_-]", "_"); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/tiles/MapTileDecoder.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.tiles; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | 6 | public interface MapTileDecoder { 7 | 8 | public Bitmap decode( String fileName, Context context ); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/tiles/MapTileDecoderAssets.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.tiles; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | 6 | import android.content.Context; 7 | import android.content.res.AssetManager; 8 | import android.graphics.Bitmap; 9 | import android.graphics.BitmapFactory; 10 | 11 | public class MapTileDecoderAssets implements MapTileDecoder { 12 | 13 | private static final BitmapFactory.Options OPTIONS = new BitmapFactory.Options(); 14 | static { 15 | OPTIONS.inPreferredConfig = Bitmap.Config.RGB_565; 16 | } 17 | 18 | @Override 19 | public Bitmap decode( String fileName, Context context ) { 20 | AssetManager assets = context.getAssets(); 21 | try { 22 | InputStream input = assets.open( fileName ); 23 | if ( input != null ) { 24 | try { 25 | return BitmapFactory.decodeStream( input, null, OPTIONS ); 26 | } catch ( OutOfMemoryError oom ) { 27 | // oom - you can try sleeping (this method won't be called in the UI thread) or try again (or give up) 28 | } catch ( Exception e ) { 29 | // unknown error decoding bitmap 30 | } 31 | } 32 | } catch ( IOException io ) { 33 | // io error - probably can't find the file 34 | } catch ( Exception e ) { 35 | // unknown error opening the asset 36 | } 37 | return null; 38 | } 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/tiles/MapTileDecoderHttp.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.tiles; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.net.HttpURLConnection; 6 | import java.net.MalformedURLException; 7 | import java.net.URL; 8 | 9 | import android.content.Context; 10 | import android.graphics.Bitmap; 11 | import android.graphics.BitmapFactory; 12 | import android.util.Log; 13 | 14 | /** 15 | * Implementation of MapTileDecoder that loads bitmaps from a HTTP server 16 | * thanks to https://github.com/mohlendo for pointing out the need and a solution 17 | */ 18 | public class MapTileDecoderHttp implements MapTileDecoder { 19 | 20 | private static final String TAG = MapTileDecoderHttp.class.getSimpleName(); 21 | 22 | private static final BitmapFactory.Options OPTIONS = new BitmapFactory.Options(); 23 | static { 24 | OPTIONS.inPreferredConfig = Bitmap.Config.RGB_565; 25 | } 26 | 27 | @Override 28 | public Bitmap decode( String fileName, Context context ) { 29 | URL url; 30 | try { 31 | url = new URL(fileName); 32 | } catch (MalformedURLException e) { 33 | Log.e(TAG, "Your URL '" + fileName + "' is not an URL"); 34 | return null; 35 | } 36 | 37 | HttpURLConnection connection = null; 38 | InputStream input = null; 39 | try { 40 | connection = (HttpURLConnection) url.openConnection(); 41 | input = connection.getInputStream(); 42 | if (input != null) { 43 | try { 44 | return BitmapFactory.decodeStream( input, null, OPTIONS ); 45 | } catch ( OutOfMemoryError oom ) { 46 | // oom - you can try sleeping (this method won't be called in the UI thread) or try again (or give up) 47 | } catch ( Exception e ) { 48 | // unknown error decoding bitmap 49 | } 50 | } 51 | } catch ( IOException e ) { 52 | Log.e(TAG, "Cannot download tile for URL: " + fileName, e); 53 | } finally { 54 | //close the stream and url connection 55 | if (input != null) { 56 | try { 57 | input.close(); 58 | } catch (IOException e) { 59 | // ignore 60 | } 61 | } 62 | if (connection != null) { 63 | connection.disconnect(); 64 | } 65 | } 66 | return null; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/tiles/MapTilePool.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.tiles; 2 | 3 | import java.util.LinkedList; 4 | 5 | public class MapTilePool { 6 | 7 | private LinkedList employed = new LinkedList(); 8 | private LinkedList retired = new LinkedList(); 9 | 10 | public MapTile employ(){ 11 | MapTile m = retired.poll(); 12 | if ( m == null ) { 13 | m = new MapTile(); 14 | } 15 | employed.add( m ); 16 | return m; 17 | } 18 | 19 | public void retire( MapTile m ) { 20 | employed.remove( m ); 21 | retired.add( m ); 22 | } 23 | 24 | public void retireAll() { 25 | retired.addAll( employed ); 26 | employed.clear(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/tiles/TileManager.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.tiles; 2 | 3 | import java.util.HashMap; 4 | import java.util.LinkedList; 5 | 6 | import android.content.Context; 7 | import android.os.Handler; 8 | import android.os.Message; 9 | import android.view.View; 10 | import android.widget.ImageView; 11 | 12 | import com.qozix.layouts.FixedLayout; 13 | import com.qozix.layouts.ScalingLayout; 14 | import com.qozix.mapview.zoom.ZoomLevel; 15 | import com.qozix.mapview.zoom.ZoomListener; 16 | import com.qozix.mapview.zoom.ZoomManager; 17 | import com.qozix.widgets.AsyncTask; 18 | 19 | public class TileManager extends ScalingLayout implements ZoomListener { 20 | 21 | private static final String TAG = TileManager.class.getSimpleName(); 22 | 23 | private static final int RENDER_FLAG = 1; 24 | private static final int RENDER_BUFFER = 250; 25 | 26 | private LinkedList scheduledToRender = new LinkedList(); 27 | private LinkedList alreadyRendered = new LinkedList(); 28 | 29 | private MapTileDecoder decoder = new MapTileDecoderAssets(); 30 | private HashMap tileGroups = new HashMap(); 31 | 32 | private TileRenderListener renderListener; 33 | 34 | private MapTileCache cache; 35 | private ZoomLevel zoomLevelToRender; 36 | private TileRenderTask lastRunRenderTask; 37 | private ScalingLayout currentTileGroup; 38 | private ZoomManager zoomManager; 39 | 40 | private int lastRenderedZoom = -1; 41 | 42 | private boolean renderIsCancelled = false; 43 | private boolean renderIsSuppressed = false; 44 | private boolean isRendering = false; 45 | 46 | private TileRenderHandler handler; 47 | 48 | public TileManager( Context context, ZoomManager zm ) { 49 | super( context ); 50 | zoomManager = zm; 51 | zoomManager.addZoomListener( this ); 52 | handler = new TileRenderHandler( this ); 53 | } 54 | 55 | public void setDecoder( MapTileDecoder d ){ 56 | decoder = d; 57 | } 58 | 59 | public void setCacheEnabled( boolean shouldCache ) { 60 | if ( shouldCache ){ 61 | if ( cache == null ){ 62 | cache = new MapTileCache( getContext() ); 63 | } 64 | } else { 65 | if ( cache != null ) { 66 | cache.destroy(); 67 | } 68 | cache = null; 69 | } 70 | } 71 | 72 | public void setTileRenderListener( TileRenderListener listener ){ 73 | renderListener = listener; 74 | } 75 | 76 | public void requestRender() { 77 | // if we're requesting it, we must really want one 78 | renderIsCancelled = false; 79 | renderIsSuppressed = false; 80 | // if there's no data about the current zoom level, don't bother 81 | if ( zoomLevelToRender == null ) { 82 | return; 83 | } 84 | // throttle requests 85 | if ( handler.hasMessages( RENDER_FLAG ) ) { 86 | handler.removeMessages( RENDER_FLAG ); 87 | } 88 | // give it enough buffer that (generally) successive calls will be captured 89 | handler.sendEmptyMessageDelayed( RENDER_FLAG, RENDER_BUFFER ); 90 | } 91 | 92 | public void cancelRender() { 93 | // hard cancel - this applies to *all* tasks, not just the currently executing task 94 | renderIsCancelled = true; 95 | // if the currently executing task isn't null... 96 | if ( lastRunRenderTask != null ) { 97 | // ... and it's in a cancellable state 98 | if ( lastRunRenderTask.getStatus() != AsyncTask.Status.FINISHED ) { 99 | // ... then squash it 100 | lastRunRenderTask.cancel( true ); 101 | } 102 | } 103 | // give it to gc 104 | lastRunRenderTask = null; 105 | } 106 | 107 | public void suppressRender() { 108 | // this will prevent new tasks from starting, but won't actually cancel the currently executing task 109 | renderIsSuppressed = true; 110 | } 111 | 112 | public void updateTileSet() { 113 | // fast fail if there aren't any zoom levels registered 114 | int numZoomLevels = zoomManager.getNumZoomLevels(); 115 | if ( numZoomLevels == 0 ) { 116 | return; 117 | } 118 | // what zoom level should we be showing? 119 | int zoom = zoomManager.getZoom(); 120 | // fast-fail if there's no change 121 | if ( zoom == lastRenderedZoom ) { 122 | return; 123 | } 124 | // save it so we can detect change next time 125 | lastRenderedZoom = zoom; 126 | // grab reference to this zoom level, so we can get it's tile set for comparison to viewport 127 | zoomLevelToRender = zoomManager.getCurrentZoomLevel(); 128 | // fetch appropriate child 129 | currentTileGroup = getCurrentTileGroup(); 130 | // made it this far, so currentTileGroup should be valid, so update clipping 131 | updateViewClip( currentTileGroup ); 132 | // get the appropriate zoom 133 | double scale = zoomManager.getInvertedScale(); 134 | // scale the group 135 | currentTileGroup.setScale( scale ); 136 | // show it 137 | currentTileGroup.setVisibility( View.VISIBLE ); 138 | // bring it to top of stack 139 | currentTileGroup.bringToFront(); 140 | } 141 | 142 | public boolean getIsRendering() { 143 | return isRendering; 144 | } 145 | 146 | public void clear() { 147 | // suppress and cancel renders 148 | suppressRender(); 149 | cancelRender(); 150 | // destroy all tiles 151 | for ( MapTile m : scheduledToRender ) { 152 | m.destroy(); 153 | } 154 | scheduledToRender.clear(); 155 | for ( MapTile m : alreadyRendered ) { 156 | m.destroy(); 157 | } 158 | alreadyRendered.clear(); 159 | // the above should clear everything, but let's be redundant 160 | for ( ScalingLayout tileGroup : tileGroups.values() ) { 161 | int totalChildren = tileGroup.getChildCount(); 162 | for ( int i = 0; i < totalChildren; i++ ) { 163 | View child = tileGroup.getChildAt( i ); 164 | if ( child instanceof ImageView ) { 165 | ImageView imageView = (ImageView) child; 166 | imageView.setImageBitmap( null ); 167 | } 168 | } 169 | tileGroup.removeAllViews(); 170 | } 171 | // clear the cache 172 | if ( cache != null ) { 173 | cache.clear(); 174 | } 175 | } 176 | 177 | private ScalingLayout getCurrentTileGroup() { 178 | int zoom = zoomManager.getZoom(); 179 | // if a tile group has already been created and registered, return it 180 | if ( tileGroups.containsKey( zoom ) ) { 181 | return tileGroups.get( zoom ); 182 | } 183 | // otherwise create one, register it, and add it to the view tree 184 | ScalingLayout tileGroup = new ScalingLayout( getContext() ); 185 | tileGroups.put( zoom, tileGroup ); 186 | addView( tileGroup ); 187 | return tileGroup; 188 | } 189 | 190 | 191 | 192 | // access omitted deliberately - need package level access for the TileRenderHandler 193 | void renderTiles() { 194 | // has it been canceled since it was requested? 195 | if ( renderIsCancelled ) { 196 | return; 197 | } 198 | // can we keep rending existing tasks, but not start new ones? 199 | if ( renderIsSuppressed ) { 200 | return; 201 | } 202 | // fast-fail if there's no available data 203 | if ( zoomLevelToRender == null ) { 204 | return; 205 | } 206 | // decode and render the bitmaps asynchronously 207 | beginRenderTask(); 208 | } 209 | 210 | private void updateViewClip( View view ) { 211 | LayoutParams lp = (LayoutParams) view.getLayoutParams(); 212 | lp.width = zoomManager.getComputedCurrentWidth(); 213 | lp.height = zoomManager.getComputedCurrentHeight(); 214 | view.setLayoutParams( lp ); 215 | } 216 | 217 | private void beginRenderTask() { 218 | // find all matching tiles 219 | LinkedList intersections = zoomLevelToRender.getIntersections(); 220 | // if it's the same list, don't bother 221 | if ( scheduledToRender.equals( intersections ) ) { 222 | return; 223 | } 224 | // if we made it here, then replace the old list with the new list 225 | scheduledToRender = intersections; 226 | // cancel task if it's already running 227 | if ( lastRunRenderTask != null ) { 228 | if ( lastRunRenderTask.getStatus() != AsyncTask.Status.FINISHED ) { 229 | lastRunRenderTask.cancel( true ); 230 | } 231 | } 232 | // start a new one 233 | lastRunRenderTask = new TileRenderTask( this ); 234 | lastRunRenderTask.execute(); 235 | } 236 | 237 | private FixedLayout.LayoutParams getLayoutFromTile( MapTile m ) { 238 | int w = m.getWidth(); 239 | int h = m.getHeight(); 240 | int x = m.getLeft(); 241 | int y = m.getTop(); 242 | return new FixedLayout.LayoutParams( w, h, x, y ); 243 | } 244 | 245 | private void cleanup() { 246 | // start with all rendered tiles... 247 | LinkedList condemned = new LinkedList( alreadyRendered ); 248 | // now remove all those that were just qualified 249 | condemned.removeAll( scheduledToRender ); 250 | // for whatever's left, destroy and remove from list 251 | for ( MapTile m : condemned ) { 252 | m.destroy(); 253 | alreadyRendered.remove( m ); 254 | } 255 | // hide all other groups 256 | for ( ScalingLayout tileGroup : tileGroups.values() ) { 257 | if ( currentTileGroup == tileGroup ) { 258 | continue; 259 | } 260 | tileGroup.setVisibility( View.GONE ); 261 | } 262 | } 263 | 264 | /* 265 | * render tasks (invoked in asynctask's thread) 266 | */ 267 | 268 | void onRenderTaskPreExecute(){ 269 | // set a flag that we're working 270 | isRendering = true; 271 | // notify anybody interested 272 | if ( renderListener != null ) { 273 | renderListener.onRenderStart(); 274 | } 275 | } 276 | 277 | void onRenderTaskCancelled() { 278 | if ( renderListener != null ) { 279 | renderListener.onRenderCancelled(); 280 | } 281 | isRendering = false; 282 | } 283 | 284 | void onRenderTaskPostExecute() { 285 | // set flag that we're done 286 | isRendering = false; 287 | // everything's been rendered, so get rid of the old tiles 288 | cleanup(); 289 | // recurse - request another round of render - if the same intersections are discovered, recursion will end anyways 290 | requestRender(); 291 | // notify anybody interested 292 | if ( renderListener != null ) { 293 | renderListener.onRenderComplete(); 294 | } 295 | } 296 | 297 | LinkedList getRenderList(){ 298 | return new LinkedList( scheduledToRender ); 299 | } 300 | 301 | void decodeIndividualTile( MapTile m ) { 302 | m.decode( getContext(), cache, decoder ); 303 | } 304 | 305 | void renderIndividualTile( MapTile m ) { 306 | if ( alreadyRendered.contains( m ) ) { 307 | return; 308 | } 309 | m.render( getContext() ); 310 | alreadyRendered.add( m ); 311 | ImageView i = m.getImageView(); 312 | LayoutParams l = getLayoutFromTile( m ); 313 | currentTileGroup.addView( i, l ); 314 | } 315 | 316 | boolean getRenderIsCancelled() { 317 | return renderIsCancelled; 318 | } 319 | 320 | @Override 321 | public void onZoomLevelChanged( int oldZoom, int newZoom ) { 322 | updateTileSet(); 323 | } 324 | 325 | @Override 326 | public void onZoomScaleChanged( double scale ) { 327 | setScale( scale ); 328 | } 329 | 330 | } 331 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/tiles/TileRenderHandler.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.tiles; 2 | 3 | import java.lang.ref.WeakReference; 4 | 5 | import android.os.Handler; 6 | import android.os.Message; 7 | 8 | public class TileRenderHandler extends Handler { 9 | 10 | private final WeakReference reference; 11 | public TileRenderHandler( TileManager tm ) { 12 | super(); 13 | reference = new WeakReference( tm ); 14 | } 15 | @Override 16 | public final void handleMessage( Message message ) { 17 | final TileManager tileManager = reference.get(); 18 | if ( tileManager != null ) { 19 | tileManager.renderTiles(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/tiles/TileRenderListener.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.tiles; 2 | 3 | public interface TileRenderListener { 4 | public void onRenderStart(); 5 | public void onRenderCancelled(); 6 | public void onRenderComplete(); 7 | } 8 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/tiles/TileRenderTask.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.tiles; 2 | 3 | import java.lang.ref.WeakReference; 4 | import java.util.LinkedList; 5 | 6 | import com.qozix.widgets.AsyncTask; 7 | 8 | 9 | class TileRenderTask extends AsyncTask { 10 | 11 | private final WeakReference reference; 12 | 13 | // package level access 14 | TileRenderTask( TileManager tm ) { 15 | super(); 16 | reference = new WeakReference( tm ); 17 | } 18 | 19 | @Override 20 | protected void onPreExecute() { 21 | final TileManager tileManager = reference.get(); 22 | if ( tileManager != null ) { 23 | tileManager.onRenderTaskPreExecute(); 24 | } 25 | } 26 | 27 | @Override 28 | protected Void doInBackground( Void... params ) { 29 | // have we been stopped or dereffed? 30 | TileManager tileManager = reference.get(); 31 | // if not go ahead, but check again in each iteration 32 | if ( tileManager != null ) { 33 | // avoid concurrent modification exceptions by duplicating 34 | LinkedList renderList = tileManager.getRenderList(); 35 | // start rendering, checking each iteration if we need to break out 36 | for ( MapTile m : renderList ) { 37 | // check again if we've been stopped or gc'ed 38 | tileManager = reference.get(); 39 | if ( tileManager == null ) { 40 | return null; 41 | } 42 | // quit if we've been forcibly stopped 43 | if ( tileManager.getRenderIsCancelled() ) { 44 | return null; 45 | } 46 | // quit if task has been cancelled or replaced 47 | if ( isCancelled() ) { 48 | return null; 49 | } 50 | // once the bitmap is decoded, the heavy lift is done 51 | tileManager.decodeIndividualTile( m ); 52 | // pass it to the UI thread for insertion into the view tree 53 | publishProgress( m ); 54 | } 55 | 56 | } 57 | return null; 58 | } 59 | 60 | @Override 61 | protected void onProgressUpdate( MapTile... params ) { 62 | // have we been stopped or dereffed? 63 | TileManager tileManager = reference.get(); 64 | // if not go ahead but check other cancel states 65 | if ( tileManager != null ) { 66 | // quit if it's been force-stopped 67 | if ( tileManager.getRenderIsCancelled() ) { 68 | return; 69 | } 70 | // quit if it's been stopped or replaced by a new task 71 | if ( isCancelled() ) { 72 | return; 73 | } 74 | // tile should already have bitmap decoded 75 | MapTile m = params[0]; 76 | // add the bitmap to it's view, add the view to the current zoom layout 77 | tileManager.renderIndividualTile( m ); 78 | } 79 | 80 | } 81 | 82 | @Override 83 | protected void onPostExecute( Void param ) { 84 | // have we been stopped or dereffed? 85 | TileManager tileManager = reference.get(); 86 | // if not go ahead but check other cancel states 87 | if ( tileManager != null ) { 88 | tileManager.onRenderTaskPostExecute(); 89 | } 90 | } 91 | 92 | @Override 93 | protected void onCancelled() { 94 | // have we been stopped or dereffed? 95 | TileManager tileManager = reference.get(); 96 | // if not go ahead but check other cancel states 97 | if ( tileManager != null ) { 98 | tileManager.onRenderTaskCancelled(); 99 | } 100 | } 101 | 102 | } -------------------------------------------------------------------------------- /src/com/qozix/mapview/viewmanagers/DownsampleManager.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.viewmanagers; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | import android.graphics.drawable.BitmapDrawable; 7 | import android.graphics.drawable.Drawable; 8 | import android.os.Build; 9 | import android.view.View; 10 | 11 | import com.qozix.mapview.tiles.MapTileDecoder; 12 | import com.qozix.mapview.tiles.MapTileDecoderAssets; 13 | 14 | public class DownsampleManager { 15 | 16 | private MapTileDecoder decoder = new MapTileDecoderAssets(); 17 | 18 | private String lastFileName; 19 | 20 | public void setDecoder( MapTileDecoder d ){ 21 | decoder = d; 22 | } 23 | 24 | public void setDownsample( View view, String fileName ) { 25 | if ( fileName == null ) { 26 | setDownsampleBackground( view, null ); 27 | lastFileName = null; 28 | return; 29 | } 30 | if ( fileName.equals( lastFileName )) { 31 | return; 32 | } 33 | lastFileName = fileName; 34 | Context context = view.getContext(); 35 | Bitmap bitmap = decoder.decode( fileName, context ); 36 | BitmapDrawable bitmapDrawable = new BitmapDrawable( context.getResources(), bitmap ); 37 | setDownsampleBackground( view, bitmapDrawable ); 38 | } 39 | 40 | // suppress deprecation because we're doing the only thing we can do with Android breaking API 41 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 42 | @SuppressWarnings("deprecation") 43 | private void setDownsampleBackground( View view, Drawable drawable ){ 44 | if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) { 45 | view.setBackgroundDrawable( drawable ); 46 | } else { 47 | view.setBackground( drawable ); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/viewmanagers/ViewCurator.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.viewmanagers; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | import android.widget.ImageView; 6 | 7 | public class ViewCurator { 8 | 9 | private ViewCurator() { 10 | 11 | } 12 | 13 | public static void clear( View view ) { 14 | if ( view instanceof ImageView ) { 15 | ImageView imageView = (ImageView) view; 16 | imageView.setImageBitmap( null ); 17 | } else if ( view instanceof ViewGroup ) { 18 | ViewGroup viewGroup = (ViewGroup) view; 19 | int childCount = viewGroup.getChildCount(); 20 | for ( int i = 0; i < childCount; i++ ) { 21 | View child = viewGroup.getChildAt( i ); 22 | clear( child ); 23 | } 24 | viewGroup.removeAllViews(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/viewmanagers/ViewFactory.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.viewmanagers; 2 | 3 | import android.content.Context; 4 | import android.view.View; 5 | 6 | public abstract class ViewFactory { 7 | public abstract E fetch( Context c ); 8 | } -------------------------------------------------------------------------------- /src/com/qozix/mapview/viewmanagers/ViewPool.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.viewmanagers; 2 | 3 | import java.util.LinkedList; 4 | 5 | import android.content.Context; 6 | import android.util.Log; 7 | import android.view.View; 8 | 9 | public class ViewPool { 10 | 11 | private ViewFactory factory; 12 | private LinkedList employed = new LinkedList(); 13 | private LinkedList unemployed = new LinkedList(); 14 | 15 | public ViewPool( ViewFactory f ) { 16 | factory = f; 17 | } 18 | 19 | public View employView( Context context ) { 20 | View v; 21 | if ( unemployed.size() > 0 ) { 22 | v = unemployed.get( 0 ); 23 | unemployed.remove( v ); 24 | } else { 25 | v = factory.fetch( context ); 26 | } 27 | employed.add( v ); 28 | Log.d( "ViewPool", "employed.size=" + employed.size() + ", unemployed.size=" + unemployed.size()); 29 | return v; 30 | } 31 | 32 | public void retireView( View v ) { 33 | if ( employed.contains( v ) ) { 34 | employed.remove( v ); 35 | unemployed.add( v ); 36 | } 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /src/com/qozix/mapview/viewmanagers/ViewSetManager.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.viewmanagers; 2 | 3 | import java.util.HashMap; 4 | import java.util.HashSet; 5 | import java.util.Iterator; 6 | import java.util.Map; 7 | import java.util.Map.Entry; 8 | 9 | import android.view.View; 10 | import android.view.ViewParent; 11 | 12 | public class ViewSetManager { 13 | 14 | private HashMap> map = new HashMap>(); 15 | 16 | public HashSet getSetAtLevel( int level ) { 17 | if(!map.containsKey( level )){ 18 | HashSet viewSet = new HashSet(); 19 | map.put( level, viewSet ); 20 | } 21 | return map.get( level ); 22 | } 23 | 24 | public void addViewAtLevel( View view, int level ){ 25 | HashSet viewSet = getSetAtLevel( level ); 26 | viewSet.add( view ); 27 | } 28 | 29 | public void removeViewAtLevel( View view, int level ) { 30 | HashSet viewSet = map.get( level ); 31 | viewSet.remove( view ); 32 | } 33 | 34 | public void removeAllViewsAtLevel( int level ) { 35 | map.remove( level ); 36 | } 37 | 38 | public boolean removeView( View view ){ 39 | for(Entry> e : map.entrySet()){ 40 | HashSet viewSet = e.getValue(); 41 | Iterator iterator = viewSet.iterator(); 42 | while(iterator.hasNext()) { 43 | View comparison = iterator.next(); 44 | if( comparison == view ) { 45 | iterator.remove(); 46 | return true; 47 | } 48 | } 49 | } 50 | return false; 51 | } 52 | 53 | // remove all views from all sets that aren't in the view tree 54 | public void purgeViewSets() { 55 | for(Entry> e : map.entrySet()){ 56 | HashSet viewSet = e.getValue(); 57 | Iterator iterator = viewSet.iterator(); 58 | while(iterator.hasNext()) { 59 | View view = iterator.next(); 60 | ViewParent parent = view.getParent(); 61 | if( parent == null ) { 62 | iterator.remove(); 63 | } 64 | } 65 | } 66 | } 67 | 68 | public void updateDisplay( int level ){ 69 | for(Map.Entry> e : map.entrySet()) { 70 | Integer intendedLevel = e.getKey(); 71 | HashSet viewSet = e.getValue(); 72 | int visibility = intendedLevel.equals( level ) ? View.VISIBLE : View.GONE; 73 | for(View v : viewSet) { 74 | v.setVisibility( visibility ); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/zoom/ZoomLevel.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.zoom; 2 | 3 | import java.util.LinkedList; 4 | 5 | import android.graphics.Rect; 6 | 7 | import com.qozix.mapview.tiles.MapTile; 8 | 9 | public class ZoomLevel implements Comparable { 10 | 11 | private static final int DEFAULT_TILE_SIZE = 256; 12 | 13 | private int tileWidth; 14 | private int tileHeight; 15 | private int mapWidth; 16 | private int mapHeight; 17 | 18 | private long area; 19 | 20 | private int rowCount; 21 | private int columnCount; 22 | 23 | private String pattern; 24 | private String downsample; 25 | 26 | private ZoomManager zoomManager; 27 | private Rect viewport = new Rect(); 28 | 29 | public ZoomLevel( ZoomManager zm, int mw, int mh, String p ) { 30 | this( zm, mw, mh, p, null, DEFAULT_TILE_SIZE, DEFAULT_TILE_SIZE ); 31 | } 32 | 33 | public ZoomLevel( ZoomManager zm, int mw, int mh, String p, String d ) { 34 | this( zm, mw, mh, p, d, DEFAULT_TILE_SIZE, DEFAULT_TILE_SIZE ); 35 | } 36 | 37 | public ZoomLevel( ZoomManager zm, int mw, int mh, String p, int tw, int th ) { 38 | this( zm, mw, mh, p, null, tw, th ); 39 | } 40 | 41 | public ZoomLevel( ZoomManager zm, int mw, int mh, String p, String d, int tw, int th ) { 42 | zoomManager = zm; 43 | mapWidth = mw; 44 | mapHeight = mh; 45 | pattern = p; 46 | downsample = d; 47 | tileWidth = tw; 48 | tileHeight = th; 49 | rowCount = (int) ( mapHeight / tileHeight ); 50 | columnCount = (int) ( mapWidth / tileWidth ); 51 | area = (long) ( mapWidth * mapHeight ); 52 | } 53 | 54 | public LinkedList getIntersections() { 55 | int zoom = zoomManager.getZoom(); 56 | double scale = zoomManager.getRelativeScale(); 57 | double offsetWidth = tileWidth * scale; 58 | double offsetHeight = tileHeight * scale; 59 | LinkedList intersections = new LinkedList(); 60 | viewport.set( zoomManager.getComputedViewport() ); 61 | viewport.top = Math.max( viewport.top, 0 ); 62 | viewport.left = Math.max( viewport.left, 0 ); 63 | viewport.right = Math.min( viewport.right, (int) ( mapWidth * scale ) ); 64 | viewport.bottom = Math.min( viewport.bottom, (int) ( mapHeight * scale ) ); 65 | int sr = (int) Math.floor( viewport.top / offsetHeight ); 66 | int er = (int) Math.ceil( viewport.bottom / offsetHeight ); 67 | int sc = (int) Math.floor( viewport.left / offsetWidth ); 68 | int ec = (int) Math.ceil( viewport.right / offsetWidth ); 69 | for ( int r = sr; r < er; r++ ) { 70 | for ( int c = sc; c < ec; c++ ) { 71 | MapTile m = new MapTile( zoom, r, c, tileWidth, tileHeight, pattern ); 72 | intersections.add( m ); 73 | } 74 | } 75 | return intersections; 76 | } 77 | 78 | public int getTileWidth() { 79 | return tileWidth; 80 | } 81 | 82 | public int getTileHeight() { 83 | return tileHeight; 84 | } 85 | 86 | public int getMapWidth() { 87 | return mapWidth; 88 | } 89 | 90 | public int getMapHeight() { 91 | return mapHeight; 92 | } 93 | 94 | public String getPattern() { 95 | return pattern; 96 | } 97 | 98 | public String getDownsample() { 99 | return downsample; 100 | } 101 | 102 | public int getRowCount() { 103 | return rowCount; 104 | } 105 | 106 | public int getColumnCount() { 107 | return columnCount; 108 | } 109 | 110 | public String getTilePath( int col, int row ) { 111 | String path = pattern; 112 | path = path.replace( "%col%", Integer.toString( col ) ); 113 | path = path.replace( "%row%", Integer.toString( row ) ); 114 | return path; 115 | } 116 | 117 | public long getArea() { 118 | return area; 119 | } 120 | 121 | @Override 122 | public int compareTo( ZoomLevel o ) { 123 | return (int) Math.signum( getArea() - o.getArea() ); 124 | } 125 | 126 | @Override 127 | public boolean equals( Object o ) { 128 | if ( o instanceof ZoomLevel ) { 129 | ZoomLevel zl = (ZoomLevel) o; 130 | return ( zl.getMapWidth() == getMapWidth() ) && ( zl.getMapHeight() == getMapHeight() ); 131 | } 132 | return false; 133 | } 134 | 135 | @Override 136 | public int hashCode() { 137 | long bits = ( Double.doubleToLongBits( mapWidth ) * 43 ) + ( Double.doubleToLongBits( mapHeight ) * 47 ); 138 | return ( ( (int) bits ) ^ ( (int) ( bits >> 32 ) ) ); 139 | } 140 | 141 | @Override 142 | public String toString() { 143 | return "(w=" + mapWidth + ", h=" + mapHeight + ", p=" + pattern + ")"; 144 | } 145 | 146 | } -------------------------------------------------------------------------------- /src/com/qozix/mapview/zoom/ZoomLevelSet.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.zoom; 2 | 3 | import java.util.Collections; 4 | import java.util.LinkedList; 5 | 6 | /* 7 | * this collection should be: 8 | * 1. Unique 9 | * 2. Sorted 10 | * 3. Indexed 11 | * So it's either a TreeSet with a get(int index) method 12 | * or a LinkedList with uniqueness and sorting implemented 13 | * Since adding ZoomLevels is likely to be infrequent, and 14 | * done outside of heavy rendering work on the UI thread, 15 | * and fetching might happen rapidly and repeatedly in 16 | * response to user or touch events, we're opting for 17 | * the sorted List (for now) 18 | */ 19 | 20 | 21 | public class ZoomLevelSet extends LinkedList { 22 | 23 | private static final long serialVersionUID = -1742428277010988084L; 24 | 25 | public void addZoomLevel( ZoomLevel zoomLevel ) { 26 | // ensure uniqueness 27 | if( contains( zoomLevel ) ) { 28 | return; 29 | } 30 | add( zoomLevel ); 31 | // sort it 32 | Collections.sort( this ); 33 | } 34 | 35 | } 36 | 37 | /* 38 | public class ZoomLevelSet extends TreeSet { 39 | 40 | private static final long serialVersionUID = -4028578060932067224L; 41 | 42 | public ZoomLevel get( int index ) { 43 | if ( index >= ( size() - 1) ) { 44 | return null; 45 | } 46 | if ( index < 0 ) { 47 | return null; 48 | } 49 | Iterator i = iterator(); 50 | while( i.hasNext() ) { 51 | ZoomLevel e = i.next(); 52 | if ( index-- == 0 ) { 53 | return e; 54 | } 55 | } 56 | return null; 57 | } 58 | } 59 | */ 60 | 61 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/zoom/ZoomListener.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.zoom; 2 | 3 | public interface ZoomListener { 4 | public void onZoomLevelChanged(int oldZoom, int currentZoom); 5 | public void onZoomScaleChanged(double scale); 6 | } 7 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/zoom/ZoomManager.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.zoom; 2 | 3 | import java.util.HashSet; 4 | 5 | import android.graphics.Rect; 6 | 7 | public class ZoomManager { 8 | 9 | private static final double BASE_2 = Math.log( 2 ); 10 | private static final double PRECISION = 6; 11 | private static final double DECIMAL = Math.pow( 10, PRECISION ); 12 | private static final double OFFSET = 1 / DECIMAL; 13 | 14 | private ZoomLevelSet zoomLevels = new ZoomLevelSet(); 15 | private HashSet zoomListeners = new HashSet(); 16 | private HashSet zoomSetupListeners = new HashSet(); 17 | 18 | private double scale = 1; 19 | private double minScale = 0; 20 | private double maxScale = 1; 21 | private double historicalScale; 22 | 23 | private double relativeScale; 24 | private double invertedScale; 25 | private double computedScale; 26 | 27 | private int zoom; 28 | private int lastZoom = -1; 29 | 30 | private int numZoomLevels; 31 | 32 | private ZoomLevel currentZoomLevel; 33 | private ZoomLevel highestZoomLevel; 34 | private ZoomLevel lowestZoomLevel; 35 | 36 | private int computedCurrentWidth; 37 | private int computedCurrentHeight; 38 | private int currentScaledWidth; 39 | private int currentScaledHeight; 40 | 41 | private int baseMapWidth; 42 | private int baseMapHeight; 43 | 44 | private boolean zoomLocked = false; 45 | 46 | private int padding = 0; 47 | private Rect viewport = new Rect(); 48 | private Rect computedViewport = new Rect(); 49 | 50 | private static double getAtPrecision( double s ) { 51 | return Math.round( s * DECIMAL ) / DECIMAL; 52 | } 53 | 54 | public static int computeZoom( double scale, int numZoomLevels ){ 55 | int zoom = (int) ( numZoomLevels + Math.floor( Math.log( scale - OFFSET ) / BASE_2 ) ); 56 | zoom = Math.max( zoom, 0 ); 57 | zoom = Math.min( zoom, numZoomLevels - 1); 58 | return zoom; 59 | } 60 | 61 | public static double computeRelativeScale( double scale, int numZoomLevels, int zoom ){ 62 | double relativeScale = scale * ( 1 << ( ( numZoomLevels - 1 ) - zoom ) ); 63 | return getAtPrecision( relativeScale ); 64 | } 65 | 66 | public static double computeInvertedScale( int numZoomLevels, int zoom ){ 67 | return 1 << ( ( numZoomLevels - zoom ) - 1 ); 68 | } 69 | 70 | public static double computeOffsetScale( double scale, int numZoomLevels, int zoom ) { 71 | return computeInvertedScale( numZoomLevels, zoom ) + computeRelativeScale( scale, numZoomLevels, zoom ) - 1; 72 | } 73 | 74 | public static double computeScaleForZoom( double scale, int zoom ) { 75 | double computedScale = ( scale / Math.pow( 2, -zoom ) ) - 1; 76 | return getAtPrecision( computedScale ); 77 | } 78 | 79 | public ZoomManager(){ 80 | update( true ); 81 | } 82 | 83 | public double getScale() { 84 | return scale; 85 | } 86 | 87 | public void setScale( double s ) { 88 | // constrain between minimum and maximum allowed values 89 | s = Math.min( s, maxScale ); 90 | s = Math.max( s, minScale ); 91 | // round to PRECISION decimal places 92 | s = getAtPrecision( s ); 93 | // is it changed? 94 | boolean changed = ( scale != s ); 95 | // set it 96 | scale = s; 97 | // update computed values 98 | update( changed ); 99 | } 100 | 101 | /** 102 | * "pads" the viewport by the number of pixels passed. e.g., setPadding( 100 ) instructs the 103 | * ZoomManager to interpret it's actual viewport offset by 100 pixels in each direction (top, left, 104 | * right, bottom), so more tiles will qualify for "visible" status when intersections are calculated. 105 | * @param pixels (int) the number of pixels to pad the viewport by 106 | */ 107 | public void setPadding( int pixels ) { 108 | padding = pixels; 109 | updateComputedViewport(); 110 | } 111 | 112 | public void updateViewport( int left, int top, int right, int bottom ) { 113 | viewport.set( left, top, right, bottom ); 114 | updateComputedViewport(); 115 | } 116 | 117 | private void updateComputedViewport() { 118 | computedViewport.set( viewport ); 119 | computedViewport.top -= padding; 120 | computedViewport.left -= padding; 121 | computedViewport.bottom += padding; 122 | computedViewport.right += padding; 123 | } 124 | 125 | public Rect getViewport() { 126 | return viewport; 127 | } 128 | 129 | public Rect getComputedViewport() { 130 | return computedViewport; 131 | } 132 | 133 | private void update( boolean changed ){ 134 | 135 | // if no levels, fast-fail 136 | if(numZoomLevels == 0){ 137 | zoom = 0; 138 | relativeScale = invertedScale = scale; 139 | currentZoomLevel = highestZoomLevel = lowestZoomLevel = null; 140 | computedCurrentWidth = computedCurrentHeight = 0; 141 | return; 142 | } 143 | 144 | // get references to top and bottom levels 145 | highestZoomLevel = zoomLevels.getLast(); 146 | lowestZoomLevel = zoomLevels.getFirst(); 147 | 148 | // update zoom if unlocked 149 | if(!zoomLocked){ 150 | zoom = computeZoom( scale, numZoomLevels ); 151 | } 152 | 153 | // update current zoom level 154 | currentZoomLevel = zoomLevels.get( zoom ); 155 | 156 | // update computed scales 157 | relativeScale = computeRelativeScale( scale, numZoomLevels, zoom ); 158 | computedScale = computeOffsetScale( scale, numZoomLevels, zoom ); 159 | invertedScale = computeInvertedScale( numZoomLevels, zoom ); 160 | 161 | // update current dimensions 162 | baseMapWidth = currentZoomLevel.getMapWidth(); 163 | baseMapHeight = currentZoomLevel.getMapHeight(); 164 | computedCurrentWidth = (int) ( baseMapWidth * invertedScale ); 165 | computedCurrentHeight = (int) ( baseMapHeight * invertedScale ); 166 | currentScaledWidth = (int) ( computedCurrentWidth * scale ); 167 | currentScaledHeight = (int) ( computedCurrentHeight * scale ); 168 | 169 | // broadcast scale change 170 | if( changed ) { 171 | for ( ZoomListener listener : zoomListeners ) { 172 | listener.onZoomScaleChanged( scale ); 173 | } 174 | } 175 | 176 | // if there's a change in zoom, update appropriate values 177 | if ( zoom != lastZoom ) { 178 | // notify all interested parties 179 | for ( ZoomListener listener : zoomListeners ) { 180 | listener.onZoomLevelChanged( lastZoom, zoom ); 181 | } 182 | lastZoom = zoom; 183 | } 184 | 185 | } 186 | 187 | public void lockZoom(){ 188 | zoomLocked = true; 189 | } 190 | 191 | public void unlockZoom(){ 192 | zoomLocked = false; 193 | } 194 | 195 | public void setZoom( int z ) { 196 | int maxZoom = numZoomLevels - 1; 197 | z = Math.max(z, 0); 198 | z = Math.min(z, maxZoom); 199 | double s = 1 / (double) ( 1 << ( maxZoom - z ) ); 200 | setScale( s ); 201 | } 202 | 203 | public void addZoomListener( ZoomListener l ) { 204 | zoomListeners.add( l ); 205 | } 206 | 207 | public void removeZoomListener( ZoomListener l ) { 208 | zoomListeners.remove( l ); 209 | } 210 | 211 | public void addzoomSetupListener( ZoomSetupListener l ) { 212 | zoomSetupListeners.add( l ); 213 | } 214 | 215 | public void removezoomSetupListener( ZoomSetupListener l ) { 216 | zoomSetupListeners.remove( l ); 217 | } 218 | 219 | public void addZoomLevel( int wide, int tall, String pattern ) { 220 | ZoomLevel zoomLevel = new ZoomLevel( this, wide, tall, pattern ); 221 | registerZoomLevel( zoomLevel ); 222 | } 223 | 224 | public void addZoomLevel( int wide, int tall, String pattern, String downsample ) { 225 | ZoomLevel zoomLevel = new ZoomLevel( this, wide, tall, pattern, downsample ); 226 | registerZoomLevel( zoomLevel ); 227 | } 228 | 229 | public void addZoomLevel( int wide, int tall, String pattern, int tileWidth, int tileHeight ) { 230 | ZoomLevel zoomLevel = new ZoomLevel( this, wide, tall, pattern, tileWidth, tileHeight ); 231 | registerZoomLevel( zoomLevel ); 232 | } 233 | 234 | public void addZoomLevel( int wide, int tall, String pattern, String downsample, int tileWidth, int tileHeight ) { 235 | ZoomLevel zoomLevel = new ZoomLevel( this, wide, tall, pattern, downsample, tileWidth, tileHeight ); 236 | registerZoomLevel( zoomLevel ); 237 | } 238 | 239 | private void registerZoomLevel( ZoomLevel zoomLevel ) { 240 | zoomLevels.addZoomLevel( zoomLevel ); 241 | numZoomLevels = zoomLevels.size(); 242 | update( true ); 243 | for ( ZoomSetupListener listener : zoomSetupListeners ) { 244 | listener.onZoomLevelAdded(); 245 | } 246 | } 247 | 248 | public void resetZoomLevels(){ 249 | zoomLevels.clear(); 250 | numZoomLevels = 0; 251 | update( true ); 252 | } 253 | 254 | public ZoomLevel getCurrentZoomLevel() { 255 | return currentZoomLevel; 256 | } 257 | 258 | public ZoomLevel getHighestZoomLevel(){ 259 | return highestZoomLevel; 260 | } 261 | 262 | public ZoomLevel getLowestZoomLevel(){ 263 | return lowestZoomLevel; 264 | } 265 | 266 | public int getComputedCurrentWidth(){ 267 | return computedCurrentWidth; 268 | } 269 | 270 | public int getComputedCurrentHeight(){ 271 | return computedCurrentHeight; 272 | } 273 | 274 | public int getCurrentScaledWidth(){ 275 | return currentScaledWidth; 276 | } 277 | 278 | public int getCurrentScaledHeight(){ 279 | return currentScaledHeight; 280 | } 281 | 282 | public int getZoom() { 283 | return zoom; 284 | } 285 | 286 | public int getNumZoomLevels() { 287 | return numZoomLevels; 288 | } 289 | 290 | public int getMaxZoom() { 291 | return numZoomLevels - 1; 292 | } 293 | 294 | public double getRelativeScale() { 295 | return relativeScale; 296 | } 297 | 298 | public double getInvertedScale(){ 299 | return invertedScale; 300 | } 301 | 302 | public double getComputedScale(){ 303 | return computedScale; 304 | } 305 | 306 | public int getBaseMapWidth() { 307 | return baseMapWidth; 308 | } 309 | 310 | public int getBaseMapHeight() { 311 | return baseMapHeight; 312 | } 313 | 314 | public double getHistoricalScale(){ 315 | return historicalScale; 316 | } 317 | 318 | public void saveHistoricalScale(){ 319 | historicalScale = scale; 320 | } 321 | 322 | } 323 | -------------------------------------------------------------------------------- /src/com/qozix/mapview/zoom/ZoomSetupListener.java: -------------------------------------------------------------------------------- 1 | package com.qozix.mapview.zoom; 2 | 3 | public interface ZoomSetupListener { 4 | public void onZoomLevelAdded(); 5 | } 6 | -------------------------------------------------------------------------------- /src/com/qozix/widgets/AsyncTask.java: -------------------------------------------------------------------------------- 1 | package com.qozix.widgets; 2 | 3 | 4 | /* 5 | * Copyright (C) 2008 The Android Open Source Project 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | import java.util.concurrent.BlockingQueue; 21 | import java.util.concurrent.Callable; 22 | import java.util.concurrent.CancellationException; 23 | import java.util.concurrent.Executor; 24 | import java.util.concurrent.ExecutionException; 25 | import java.util.concurrent.FutureTask; 26 | import java.util.concurrent.LinkedBlockingQueue; 27 | import java.util.concurrent.ThreadFactory; 28 | import java.util.concurrent.ThreadPoolExecutor; 29 | import java.util.concurrent.TimeUnit; 30 | import java.util.concurrent.TimeoutException; 31 | import java.util.concurrent.atomic.AtomicBoolean; 32 | import java.util.concurrent.atomic.AtomicInteger; 33 | 34 | import android.os.Handler; 35 | import android.os.Message; 36 | import android.os.Process; 37 | 38 | /** 39 | *

AsyncTask enables proper and easy use of the UI thread. This class allows to 40 | * perform background operations and publish results on the UI thread without 41 | * having to manipulate threads and/or handlers.

42 | * 43 | *

AsyncTask is designed to be a helper class around {@link Thread} and {@link Handler} 44 | * and does not constitute a generic threading framework. AsyncTasks should ideally be 45 | * used for short operations (a few seconds at the most.) If you need to keep threads 46 | * running for long periods of time, it is highly recommended you use the various APIs 47 | * provided by the java.util.concurrent pacakge such as {@link Executor}, 48 | * {@link ThreadPoolExecutor} and {@link FutureTask}.

49 | * 50 | *

An asynchronous task is defined by a computation that runs on a background thread and 51 | * whose result is published on the UI thread. An asynchronous task is defined by 3 generic 52 | * types, called Params, Progress and Result, 53 | * and 4 steps, called onPreExecute, doInBackground, 54 | * onProgressUpdate and onPostExecute.

55 | * 56 | *
57 | *

Developer Guides

58 | *

For more information about using tasks and threads, read the 59 | * Processes and 60 | * Threads developer guide.

61 | *
62 | * 63 | *

Usage

64 | *

AsyncTask must be subclassed to be used. The subclass will override at least 65 | * one method ({@link #doInBackground}), and most often will override a 66 | * second one ({@link #onPostExecute}.)

67 | * 68 | *

Here is an example of subclassing:

69 | *
 70 |  * private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
 71 |  *     protected Long doInBackground(URL... urls) {
 72 |  *         int count = urls.length;
 73 |  *         long totalSize = 0;
 74 |  *         for (int i = 0; i < count; i++) {
 75 |  *             totalSize += Downloader.downloadFile(urls[i]);
 76 |  *             publishProgress((int) ((i / (float) count) * 100));
 77 |  *             // Escape early if cancel() is called
 78 |  *             if (isCancelled()) break;
 79 |  *         }
 80 |  *         return totalSize;
 81 |  *     }
 82 |  *
 83 |  *     protected void onProgressUpdate(Integer... progress) {
 84 |  *         setProgressPercent(progress[0]);
 85 |  *     }
 86 |  *
 87 |  *     protected void onPostExecute(Long result) {
 88 |  *         showDialog("Downloaded " + result + " bytes");
 89 |  *     }
 90 |  * }
 91 |  * 
92 | * 93 | *

Once created, a task is executed very simply:

94 | *
 95 |  * new DownloadFilesTask().execute(url1, url2, url3);
 96 |  * 
97 | * 98 | *

AsyncTask's generic types

99 | *

The three types used by an asynchronous task are the following:

100 | *
    101 | *
  1. Params, the type of the parameters sent to the task upon 102 | * execution.
  2. 103 | *
  3. Progress, the type of the progress units published during 104 | * the background computation.
  4. 105 | *
  5. Result, the type of the result of the background 106 | * computation.
  6. 107 | *
108 | *

Not all types are always used by an asynchronous task. To mark a type as unused, 109 | * simply use the type {@link Void}:

110 | *
111 |  * private class MyTask extends AsyncTask<Void, Void, Void> { ... }
112 |  * 
113 | * 114 | *

The 4 steps

115 | *

When an asynchronous task is executed, the task goes through 4 steps:

116 | *
    117 | *
  1. {@link #onPreExecute()}, invoked on the UI thread before the task 118 | * is executed. This step is normally used to setup the task, for instance by 119 | * showing a progress bar in the user interface.
  2. 120 | *
  3. {@link #doInBackground}, invoked on the background thread 121 | * immediately after {@link #onPreExecute()} finishes executing. This step is used 122 | * to perform background computation that can take a long time. The parameters 123 | * of the asynchronous task are passed to this step. The result of the computation must 124 | * be returned by this step and will be passed back to the last step. This step 125 | * can also use {@link #publishProgress} to publish one or more units 126 | * of progress. These values are published on the UI thread, in the 127 | * {@link #onProgressUpdate} step.
  4. 128 | *
  5. {@link #onProgressUpdate}, invoked on the UI thread after a 129 | * call to {@link #publishProgress}. The timing of the execution is 130 | * undefined. This method is used to display any form of progress in the user 131 | * interface while the background computation is still executing. For instance, 132 | * it can be used to animate a progress bar or show logs in a text field.
  6. 133 | *
  7. {@link #onPostExecute}, invoked on the UI thread after the background 134 | * computation finishes. The result of the background computation is passed to 135 | * this step as a parameter.
  8. 136 | *
137 | * 138 | *

Cancelling a task

139 | *

A task can be cancelled at any time by invoking {@link #cancel(boolean)}. Invoking 140 | * this method will cause subsequent calls to {@link #isCancelled()} to return true. 141 | * After invoking this method, {@link #onCancelled(Object)}, instead of 142 | * {@link #onPostExecute(Object)} will be invoked after {@link #doInBackground(Object[])} 143 | * returns. To ensure that a task is cancelled as quickly as possible, you should always 144 | * check the return value of {@link #isCancelled()} periodically from 145 | * {@link #doInBackground(Object[])}, if possible (inside a loop for instance.)

146 | * 147 | *

Threading rules

148 | *

There are a few threading rules that must be followed for this class to 149 | * work properly:

150 | *
    151 | *
  • The AsyncTask class must be loaded on the UI thread. This is done 152 | * automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.
  • 153 | *
  • The task instance must be created on the UI thread.
  • 154 | *
  • {@link #execute} must be invoked on the UI thread.
  • 155 | *
  • Do not call {@link #onPreExecute()}, {@link #onPostExecute}, 156 | * {@link #doInBackground}, {@link #onProgressUpdate} manually.
  • 157 | *
  • The task can be executed only once (an exception will be thrown if 158 | * a second execution is attempted.)
  • 159 | *
160 | * 161 | *

Memory observability

162 | *

AsyncTask guarantees that all callback calls are synchronized in such a way that the following 163 | * operations are safe without explicit synchronizations.

164 | *
    165 | *
  • Set member fields in the constructor or {@link #onPreExecute}, and refer to them 166 | * in {@link #doInBackground}. 167 | *
  • Set member fields in {@link #doInBackground}, and refer to them in 168 | * {@link #onProgressUpdate} and {@link #onPostExecute}. 169 | *
170 | * 171 | *

Order of execution

172 | *

When first introduced, AsyncTasks were executed serially on a single background 173 | * thread. Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed 174 | * to a pool of threads allowing multiple tasks to operate in parallel. Starting with 175 | * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are executed on a single 176 | * thread to avoid common application errors caused by parallel execution.

177 | *

If you truly want parallel execution, you can invoke 178 | * {@link #executeOnExecutor(java.util.concurrent.Executor, Object[])} with 179 | * {@link #THREAD_POOL_EXECUTOR}.

180 | * 181 | *

Implementation Note

182 | * This class is adapted from the Honeycomb (API 11) version of AsyncTask, so that we can leverage 183 | * parallel execution with ThreadPool. Due to previous API constraints, the SERIAL_EXECUTOR is removed 184 | * entirely (it's normally the default). The default executor now is a ThreadPoolExecutor. 185 | */ 186 | public abstract class AsyncTask { 187 | private static final String LOG_TAG = "AsyncTask"; 188 | 189 | private static final int CORE_POOL_SIZE = 5; 190 | private static final int MAXIMUM_POOL_SIZE = 128; 191 | private static final int KEEP_ALIVE = 1; 192 | 193 | private static final ThreadFactory sThreadFactory = new ThreadFactory() { 194 | private final AtomicInteger mCount = new AtomicInteger(1); 195 | 196 | public Thread newThread(Runnable r) { 197 | return new Thread(r, "AsyncTask #" + mCount.getAndIncrement()); 198 | } 199 | }; 200 | 201 | private static final BlockingQueue sPoolWorkQueue = 202 | new LinkedBlockingQueue(10); 203 | 204 | /** 205 | * An {@link Executor} that can be used to execute tasks in parallel. 206 | */ 207 | public static final Executor THREAD_POOL_EXECUTOR 208 | = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, 209 | TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); 210 | 211 | private static final int MESSAGE_POST_RESULT = 0x1; 212 | private static final int MESSAGE_POST_PROGRESS = 0x2; 213 | 214 | private static final InternalHandler sHandler = new InternalHandler(); 215 | 216 | private static volatile Executor sDefaultExecutor = THREAD_POOL_EXECUTOR; 217 | private final WorkerRunnable mWorker; 218 | private final FutureTask mFuture; 219 | 220 | private volatile Status mStatus = Status.PENDING; 221 | 222 | private final AtomicBoolean mTaskInvoked = new AtomicBoolean(); 223 | 224 | /** 225 | * Indicates the current status of the task. Each status will be set only once 226 | * during the lifetime of a task. 227 | */ 228 | public enum Status { 229 | /** 230 | * Indicates that the task has not been executed yet. 231 | */ 232 | PENDING, 233 | /** 234 | * Indicates that the task is running. 235 | */ 236 | RUNNING, 237 | /** 238 | * Indicates that {@link AsyncTask#onPostExecute} has finished. 239 | */ 240 | FINISHED, 241 | } 242 | 243 | /** @hide Used to force static handler to be created. */ 244 | public static void init() { 245 | sHandler.getLooper(); 246 | } 247 | 248 | /** @hide */ 249 | public static void setDefaultExecutor(Executor exec) { 250 | sDefaultExecutor = exec; 251 | } 252 | 253 | /** 254 | * Creates a new asynchronous task. This constructor must be invoked on the UI thread. 255 | */ 256 | public AsyncTask() { 257 | mWorker = new WorkerRunnable() { 258 | public Result call() throws Exception { 259 | mTaskInvoked.set(true); 260 | 261 | Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 262 | return postResult(doInBackground(mParams)); 263 | } 264 | }; 265 | 266 | mFuture = new FutureTask(mWorker) { 267 | @Override 268 | protected void done() { 269 | try { 270 | final Result result = get(); 271 | 272 | postResultIfNotInvoked(result); 273 | } catch (InterruptedException e) { 274 | android.util.Log.w(LOG_TAG, e); 275 | } catch (ExecutionException e) { 276 | throw new RuntimeException("An error occured while executing doInBackground()", 277 | e.getCause()); 278 | } catch (CancellationException e) { 279 | postResultIfNotInvoked(null); 280 | } catch (Throwable t) { 281 | throw new RuntimeException("An error occured while executing " 282 | + "doInBackground()", t); 283 | } 284 | } 285 | }; 286 | } 287 | 288 | private void postResultIfNotInvoked(Result result) { 289 | final boolean wasTaskInvoked = mTaskInvoked.get(); 290 | if (!wasTaskInvoked) { 291 | postResult(result); 292 | } 293 | } 294 | 295 | private Result postResult(Result result) { 296 | Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT, 297 | new AsyncTaskResult(this, result)); 298 | message.sendToTarget(); 299 | return result; 300 | } 301 | 302 | /** 303 | * Returns the current status of this task. 304 | * 305 | * @return The current status. 306 | */ 307 | public final Status getStatus() { 308 | return mStatus; 309 | } 310 | 311 | /** 312 | * Override this method to perform a computation on a background thread. The 313 | * specified parameters are the parameters passed to {@link #execute} 314 | * by the caller of this task. 315 | * 316 | * This method can call {@link #publishProgress} to publish updates 317 | * on the UI thread. 318 | * 319 | * @param params The parameters of the task. 320 | * 321 | * @return A result, defined by the subclass of this task. 322 | * 323 | * @see #onPreExecute() 324 | * @see #onPostExecute 325 | * @see #publishProgress 326 | */ 327 | protected abstract Result doInBackground(Params... params); 328 | 329 | /** 330 | * Runs on the UI thread before {@link #doInBackground}. 331 | * 332 | * @see #onPostExecute 333 | * @see #doInBackground 334 | */ 335 | protected void onPreExecute() { 336 | } 337 | 338 | /** 339 | *

Runs on the UI thread after {@link #doInBackground}. The 340 | * specified result is the value returned by {@link #doInBackground}.

341 | * 342 | *

This method won't be invoked if the task was cancelled.

343 | * 344 | * @param result The result of the operation computed by {@link #doInBackground}. 345 | * 346 | * @see #onPreExecute 347 | * @see #doInBackground 348 | * @see #onCancelled(Object) 349 | */ 350 | @SuppressWarnings({"UnusedDeclaration"}) 351 | protected void onPostExecute(Result result) { 352 | } 353 | 354 | /** 355 | * Runs on the UI thread after {@link #publishProgress} is invoked. 356 | * The specified values are the values passed to {@link #publishProgress}. 357 | * 358 | * @param values The values indicating progress. 359 | * 360 | * @see #publishProgress 361 | * @see #doInBackground 362 | */ 363 | @SuppressWarnings({"UnusedDeclaration"}) 364 | protected void onProgressUpdate(Progress... values) { 365 | } 366 | 367 | /** 368 | *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and 369 | * {@link #doInBackground(Object[])} has finished.

370 | * 371 | *

The default implementation simply invokes {@link #onCancelled()} and 372 | * ignores the result. If you write your own implementation, do not call 373 | * super.onCancelled(result).

374 | * 375 | * @param result The result, if any, computed in 376 | * {@link #doInBackground(Object[])}, can be null 377 | * 378 | * @see #cancel(boolean) 379 | * @see #isCancelled() 380 | */ 381 | @SuppressWarnings({"UnusedParameters"}) 382 | protected void onCancelled(Result result) { 383 | onCancelled(); 384 | } 385 | 386 | /** 387 | *

Applications should preferably override {@link #onCancelled(Object)}. 388 | * This method is invoked by the default implementation of 389 | * {@link #onCancelled(Object)}.

390 | * 391 | *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and 392 | * {@link #doInBackground(Object[])} has finished.

393 | * 394 | * @see #onCancelled(Object) 395 | * @see #cancel(boolean) 396 | * @see #isCancelled() 397 | */ 398 | protected void onCancelled() { 399 | } 400 | 401 | /** 402 | * Returns true if this task was cancelled before it completed 403 | * normally. If you are calling {@link #cancel(boolean)} on the task, 404 | * the value returned by this method should be checked periodically from 405 | * {@link #doInBackground(Object[])} to end the task as soon as possible. 406 | * 407 | * @return true if task was cancelled before it completed 408 | * 409 | * @see #cancel(boolean) 410 | */ 411 | public final boolean isCancelled() { 412 | return mFuture.isCancelled(); 413 | } 414 | 415 | /** 416 | *

Attempts to cancel execution of this task. This attempt will 417 | * fail if the task has already completed, already been cancelled, 418 | * or could not be cancelled for some other reason. If successful, 419 | * and this task has not started when cancel is called, 420 | * this task should never run. If the task has already started, 421 | * then the mayInterruptIfRunning parameter determines 422 | * whether the thread executing this task should be interrupted in 423 | * an attempt to stop the task.

424 | * 425 | *

Calling this method will result in {@link #onCancelled(Object)} being 426 | * invoked on the UI thread after {@link #doInBackground(Object[])} 427 | * returns. Calling this method guarantees that {@link #onPostExecute(Object)} 428 | * is never invoked. After invoking this method, you should check the 429 | * value returned by {@link #isCancelled()} periodically from 430 | * {@link #doInBackground(Object[])} to finish the task as early as 431 | * possible.

432 | * 433 | * @param mayInterruptIfRunning true if the thread executing this 434 | * task should be interrupted; otherwise, in-progress tasks are allowed 435 | * to complete. 436 | * 437 | * @return false if the task could not be cancelled, 438 | * typically because it has already completed normally; 439 | * true otherwise 440 | * 441 | * @see #isCancelled() 442 | * @see #onCancelled(Object) 443 | */ 444 | public final boolean cancel(boolean mayInterruptIfRunning) { 445 | return mFuture.cancel(mayInterruptIfRunning); 446 | } 447 | 448 | /** 449 | * Waits if necessary for the computation to complete, and then 450 | * retrieves its result. 451 | * 452 | * @return The computed result. 453 | * 454 | * @throws CancellationException If the computation was cancelled. 455 | * @throws ExecutionException If the computation threw an exception. 456 | * @throws InterruptedException If the current thread was interrupted 457 | * while waiting. 458 | */ 459 | public final Result get() throws InterruptedException, ExecutionException { 460 | return mFuture.get(); 461 | } 462 | 463 | /** 464 | * Waits if necessary for at most the given time for the computation 465 | * to complete, and then retrieves its result. 466 | * 467 | * @param timeout Time to wait before cancelling the operation. 468 | * @param unit The time unit for the timeout. 469 | * 470 | * @return The computed result. 471 | * 472 | * @throws CancellationException If the computation was cancelled. 473 | * @throws ExecutionException If the computation threw an exception. 474 | * @throws InterruptedException If the current thread was interrupted 475 | * while waiting. 476 | * @throws TimeoutException If the wait timed out. 477 | */ 478 | public final Result get(long timeout, TimeUnit unit) throws InterruptedException, 479 | ExecutionException, TimeoutException { 480 | return mFuture.get(timeout, unit); 481 | } 482 | 483 | /** 484 | * Executes the task with the specified parameters. The task returns 485 | * itself (this) so that the caller can keep a reference to it. 486 | * 487 | *

Note: this function schedules the task on a queue for a single background 488 | * thread or pool of threads depending on the platform version. When first 489 | * introduced, AsyncTasks were executed serially on a single background thread. 490 | * Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed 491 | * to a pool of threads allowing multiple tasks to operate in parallel. After 492 | * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, it is planned to change this 493 | * back to a single thread to avoid common application errors caused 494 | * by parallel execution. If you truly want parallel execution, you can use 495 | * the {@link #executeOnExecutor} version of this method 496 | * with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings on 497 | * its use. 498 | * 499 | *

This method must be invoked on the UI thread. 500 | * 501 | * @param params The parameters of the task. 502 | * 503 | * @return This instance of AsyncTask. 504 | * 505 | * @throws IllegalStateException If {@link #getStatus()} returns either 506 | * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. 507 | */ 508 | public final AsyncTask execute(Params... params) { 509 | return executeOnExecutor(sDefaultExecutor, params); 510 | } 511 | 512 | /** 513 | * Executes the task with the specified parameters. The task returns 514 | * itself (this) so that the caller can keep a reference to it. 515 | * 516 | *

This method is typically used with {@link #THREAD_POOL_EXECUTOR} to 517 | * allow multiple tasks to run in parallel on a pool of threads managed by 518 | * AsyncTask, however you can also use your own {@link Executor} for custom 519 | * behavior. 520 | * 521 | *

Warning: Allowing multiple tasks to run in parallel from 522 | * a thread pool is generally not what one wants, because the order 523 | * of their operation is not defined. For example, if these tasks are used 524 | * to modify any state in common (such as writing a file due to a button click), 525 | * there are no guarantees on the order of the modifications. 526 | * Without careful work it is possible in rare cases for the newer version 527 | * of the data to be over-written by an older one, leading to obscure data 528 | * loss and stability issues. Such changes are best 529 | * executed in serial; to guarantee such work is serialized regardless of 530 | * platform version you can use this function with {@link #SERIAL_EXECUTOR}. 531 | * 532 | *

This method must be invoked on the UI thread. 533 | * 534 | * @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a 535 | * convenient process-wide thread pool for tasks that are loosely coupled. 536 | * @param params The parameters of the task. 537 | * 538 | * @return This instance of AsyncTask. 539 | * 540 | * @throws IllegalStateException If {@link #getStatus()} returns either 541 | * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. 542 | */ 543 | public final AsyncTask executeOnExecutor(Executor exec, 544 | Params... params) { 545 | if (mStatus != Status.PENDING) { 546 | switch (mStatus) { 547 | case RUNNING: 548 | throw new IllegalStateException("Cannot execute task:" 549 | + " the task is already running."); 550 | case FINISHED: 551 | throw new IllegalStateException("Cannot execute task:" 552 | + " the task has already been executed " 553 | + "(a task can be executed only once)"); 554 | } 555 | } 556 | 557 | mStatus = Status.RUNNING; 558 | 559 | onPreExecute(); 560 | 561 | mWorker.mParams = params; 562 | exec.execute(mFuture); 563 | 564 | return this; 565 | } 566 | 567 | /** 568 | * Convenience version of {@link #execute(Object...)} for use with 569 | * a simple Runnable object. 570 | */ 571 | public static void execute(Runnable runnable) { 572 | sDefaultExecutor.execute(runnable); 573 | } 574 | 575 | /** 576 | * This method can be invoked from {@link #doInBackground} to 577 | * publish updates on the UI thread while the background computation is 578 | * still running. Each call to this method will trigger the execution of 579 | * {@link #onProgressUpdate} on the UI thread. 580 | * 581 | * {@link #onProgressUpdate} will note be called if the task has been 582 | * canceled. 583 | * 584 | * @param values The progress values to update the UI with. 585 | * 586 | * @see #onProgressUpdate 587 | * @see #doInBackground 588 | */ 589 | protected final void publishProgress(Progress... values) { 590 | if (!isCancelled()) { 591 | sHandler.obtainMessage(MESSAGE_POST_PROGRESS, 592 | new AsyncTaskResult(this, values)).sendToTarget(); 593 | } 594 | } 595 | 596 | private void finish(Result result) { 597 | if (isCancelled()) { 598 | onCancelled(result); 599 | } else { 600 | onPostExecute(result); 601 | } 602 | mStatus = Status.FINISHED; 603 | } 604 | 605 | private static class InternalHandler extends Handler { 606 | @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) 607 | @Override 608 | public void handleMessage(Message msg) { 609 | AsyncTaskResult result = (AsyncTaskResult) msg.obj; 610 | switch (msg.what) { 611 | case MESSAGE_POST_RESULT: 612 | // There is only one result 613 | result.mTask.finish(result.mData[0]); 614 | break; 615 | case MESSAGE_POST_PROGRESS: 616 | result.mTask.onProgressUpdate(result.mData); 617 | break; 618 | } 619 | } 620 | } 621 | 622 | private static abstract class WorkerRunnable implements Callable { 623 | Params[] mParams; 624 | } 625 | 626 | @SuppressWarnings({"RawUseOfParameterizedType"}) 627 | private static class AsyncTaskResult { 628 | final AsyncTask mTask; 629 | final Data[] mData; 630 | 631 | AsyncTaskResult(AsyncTask task, Data... data) { 632 | mTask = task; 633 | mData = data; 634 | } 635 | } 636 | } 637 | -------------------------------------------------------------------------------- /src/com/qozix/widgets/Scroller.java: -------------------------------------------------------------------------------- 1 | package com.qozix.widgets; 2 | 3 | import android.content.Context; 4 | import android.hardware.SensorManager; 5 | import android.util.FloatMath; 6 | import android.view.ViewConfiguration; 7 | import android.view.animation.AnimationUtils; 8 | import android.view.animation.Interpolator; 9 | 10 | 11 | /** 12 | * This class encapsulates scrolling. The duration of the scroll 13 | * can be passed in the constructor and specifies the maximum time that 14 | * the scrolling animation should take. Past this time, the scrolling is 15 | * automatically moved to its final stage and computeScrollOffset() 16 | * will always return false to indicate that scrolling is over. 17 | */ 18 | public class Scroller { 19 | private int mMode; 20 | 21 | private int mStartX; 22 | private int mStartY; 23 | private int mFinalX; 24 | private int mFinalY; 25 | 26 | private int mMinX; 27 | private int mMaxX; 28 | private int mMinY; 29 | private int mMaxY; 30 | 31 | private int mCurrX; 32 | private int mCurrY; 33 | private long mStartTime; 34 | private int mDuration; 35 | private float mDurationReciprocal; 36 | private float mDeltaX; 37 | private float mDeltaY; 38 | private boolean mFinished; 39 | private Interpolator mInterpolator; 40 | private boolean mFlywheel; 41 | 42 | private float mVelocity; 43 | 44 | private static final int DEFAULT_DURATION = 250; 45 | private static final int SCROLL_MODE = 0; 46 | private static final int FLING_MODE = 1; 47 | 48 | private static float DECELERATION_RATE = (float) (Math.log(0.75) / Math.log(0.9)); 49 | private static float ALPHA = 800; // pixels / seconds 50 | private static float START_TENSION = 0.4f; // Tension at start: (0.4 * total T, 1.0 * Distance) 51 | private static float END_TENSION = 1.0f - START_TENSION; 52 | private static final int NB_SAMPLES = 100; 53 | private static final float[] SPLINE = new float[NB_SAMPLES + 1]; 54 | 55 | private float mDeceleration; 56 | private final float mPpi; 57 | 58 | static { 59 | float x_min = 0.0f; 60 | for (int i = 0; i <= NB_SAMPLES; i++) { 61 | final float t = (float) i / NB_SAMPLES; 62 | float x_max = 1.0f; 63 | float x, tx, coef; 64 | while (true) { 65 | x = x_min + (x_max - x_min) / 2.0f; 66 | coef = 3.0f * x * (1.0f - x); 67 | tx = coef * ((1.0f - x) * START_TENSION + x * END_TENSION) + x * x * x; 68 | if (Math.abs(tx - t) < 1E-5) break; 69 | if (tx > t) x_max = x; 70 | else x_min = x; 71 | } 72 | final float d = coef + x * x * x; 73 | SPLINE[i] = d; 74 | } 75 | SPLINE[NB_SAMPLES] = 1.0f; 76 | 77 | // This controls the viscous fluid effect (how much of it) 78 | sViscousFluidScale = 8.0f; 79 | // must be set to 1.0 (used in viscousFluid()) 80 | sViscousFluidNormalize = 1.0f; 81 | sViscousFluidNormalize = 1.0f / viscousFluid(1.0f); 82 | } 83 | 84 | private static float sViscousFluidScale; 85 | private static float sViscousFluidNormalize; 86 | 87 | /** 88 | * Create a Scroller with the default duration and interpolator. 89 | */ 90 | public Scroller(Context context) { 91 | this(context, null); 92 | } 93 | 94 | public Scroller(Context context, Interpolator interpolator) { 95 | mFinished = true; 96 | mInterpolator = interpolator; 97 | mPpi = context.getResources().getDisplayMetrics().density * 160.0f; 98 | mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()); 99 | } 100 | 101 | /** 102 | * The amount of friction applied to flings. The default value 103 | * is {@link ViewConfiguration#getScrollFriction}. 104 | * 105 | * @param friction A scalar dimension-less value representing the coefficient of 106 | * friction. 107 | */ 108 | public final void setFriction(float friction) { 109 | mDeceleration = computeDeceleration(friction); 110 | } 111 | 112 | private float computeDeceleration(float friction) { 113 | return SensorManager.GRAVITY_EARTH // g (m/s^2) 114 | * 39.37f // inch/meter 115 | * mPpi // pixels per inch 116 | * friction; 117 | } 118 | 119 | /** 120 | * 121 | * Returns whether the scroller has finished scrolling. 122 | * 123 | * @return True if the scroller has finished scrolling, false otherwise. 124 | */ 125 | public final boolean isFinished() { 126 | return mFinished; 127 | } 128 | 129 | /** 130 | * Force the finished field to a particular value. 131 | * 132 | * @param finished The new finished value. 133 | */ 134 | public final void forceFinished(boolean finished) { 135 | mFinished = finished; 136 | } 137 | 138 | /** 139 | * Returns how long the scroll event will take, in milliseconds. 140 | * 141 | * @return The duration of the scroll in milliseconds. 142 | */ 143 | public final int getDuration() { 144 | return mDuration; 145 | } 146 | 147 | /** 148 | * Returns the current X offset in the scroll. 149 | * 150 | * @return The new X offset as an absolute distance from the origin. 151 | */ 152 | public final int getCurrX() { 153 | return mCurrX; 154 | } 155 | 156 | /** 157 | * Returns the current Y offset in the scroll. 158 | * 159 | * @return The new Y offset as an absolute distance from the origin. 160 | */ 161 | public final int getCurrY() { 162 | return mCurrY; 163 | } 164 | 165 | /** 166 | * Returns the current velocity. 167 | * 168 | * @return The original velocity less the deceleration. Result may be 169 | * negative. 170 | */ 171 | public float getCurrVelocity() { 172 | return mVelocity - mDeceleration * timePassed() / 2000.0f; 173 | } 174 | 175 | /** 176 | * Returns the start X offset in the scroll. 177 | * 178 | * @return The start X offset as an absolute distance from the origin. 179 | */ 180 | public final int getStartX() { 181 | return mStartX; 182 | } 183 | 184 | /** 185 | * Returns the start Y offset in the scroll. 186 | * 187 | * @return The start Y offset as an absolute distance from the origin. 188 | */ 189 | public final int getStartY() { 190 | return mStartY; 191 | } 192 | 193 | /** 194 | * Returns where the scroll will end. Valid only for "fling" scrolls. 195 | * 196 | * @return The final X offset as an absolute distance from the origin. 197 | */ 198 | public final int getFinalX() { 199 | return mFinalX; 200 | } 201 | 202 | /** 203 | * Returns where the scroll will end. Valid only for "fling" scrolls. 204 | * 205 | * @return The final Y offset as an absolute distance from the origin. 206 | */ 207 | public final int getFinalY() { 208 | return mFinalY; 209 | } 210 | 211 | /** 212 | * Call this when you want to know the new location. If it returns true, 213 | * the animation is not yet finished. loc will be altered to provide the 214 | * new location. 215 | */ 216 | public boolean computeScrollOffset() { 217 | if (mFinished) { 218 | return false; 219 | } 220 | 221 | int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); 222 | 223 | if (timePassed < mDuration) { 224 | switch (mMode) { 225 | case SCROLL_MODE: 226 | float x = timePassed * mDurationReciprocal; 227 | 228 | if (mInterpolator == null) 229 | x = viscousFluid(x); 230 | else 231 | x = mInterpolator.getInterpolation(x); 232 | 233 | mCurrX = mStartX + Math.round(x * mDeltaX); 234 | mCurrY = mStartY + Math.round(x * mDeltaY); 235 | break; 236 | case FLING_MODE: 237 | final float t = (float) timePassed / mDuration; 238 | final int index = (int) (NB_SAMPLES * t); 239 | final float t_inf = (float) index / NB_SAMPLES; 240 | final float t_sup = (float) (index + 1) / NB_SAMPLES; 241 | final float d_inf = SPLINE[index]; 242 | final float d_sup = SPLINE[index + 1]; 243 | final float distanceCoef = d_inf + (t - t_inf) / (t_sup - t_inf) * (d_sup - d_inf); 244 | 245 | mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX)); 246 | // Pin to mMinX <= mCurrX <= mMaxX 247 | mCurrX = Math.min(mCurrX, mMaxX); 248 | mCurrX = Math.max(mCurrX, mMinX); 249 | 250 | mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY)); 251 | // Pin to mMinY <= mCurrY <= mMaxY 252 | mCurrY = Math.min(mCurrY, mMaxY); 253 | mCurrY = Math.max(mCurrY, mMinY); 254 | 255 | if (mCurrX == mFinalX && mCurrY == mFinalY) { 256 | mFinished = true; 257 | } 258 | 259 | break; 260 | } 261 | } 262 | else { 263 | mCurrX = mFinalX; 264 | mCurrY = mFinalY; 265 | mFinished = true; 266 | } 267 | return true; 268 | } 269 | 270 | /** 271 | * Start scrolling by providing a starting point and the distance to travel. 272 | * The scroll will use the default value of 250 milliseconds for the 273 | * duration. 274 | * 275 | * @param startX Starting horizontal scroll offset in pixels. Positive 276 | * numbers will scroll the content to the left. 277 | * @param startY Starting vertical scroll offset in pixels. Positive numbers 278 | * will scroll the content up. 279 | * @param dx Horizontal distance to travel. Positive numbers will scroll the 280 | * content to the left. 281 | * @param dy Vertical distance to travel. Positive numbers will scroll the 282 | * content up. 283 | */ 284 | public void startScroll(int startX, int startY, int dx, int dy) { 285 | startScroll(startX, startY, dx, dy, DEFAULT_DURATION); 286 | } 287 | 288 | /** 289 | * Start scrolling by providing a starting point and the distance to travel. 290 | * 291 | * @param startX Starting horizontal scroll offset in pixels. Positive 292 | * numbers will scroll the content to the left. 293 | * @param startY Starting vertical scroll offset in pixels. Positive numbers 294 | * will scroll the content up. 295 | * @param dx Horizontal distance to travel. Positive numbers will scroll the 296 | * content to the left. 297 | * @param dy Vertical distance to travel. Positive numbers will scroll the 298 | * content up. 299 | * @param duration Duration of the scroll in milliseconds. 300 | */ 301 | public void startScroll(int startX, int startY, int dx, int dy, int duration) { 302 | mMode = SCROLL_MODE; 303 | mFinished = false; 304 | mDuration = duration; 305 | mStartTime = AnimationUtils.currentAnimationTimeMillis(); 306 | mStartX = startX; 307 | mStartY = startY; 308 | mFinalX = startX + dx; 309 | mFinalY = startY + dy; 310 | mDeltaX = dx; 311 | mDeltaY = dy; 312 | mDurationReciprocal = 1.0f / (float) mDuration; 313 | } 314 | 315 | /** 316 | * Start scrolling based on a fling gesture. The distance travelled will 317 | * depend on the initial velocity of the fling. 318 | * 319 | * @param startX Starting point of the scroll (X) 320 | * @param startY Starting point of the scroll (Y) 321 | * @param velocityX Initial velocity of the fling (X) measured in pixels per 322 | * second. 323 | * @param velocityY Initial velocity of the fling (Y) measured in pixels per 324 | * second 325 | * @param minX Minimum X value. The scroller will not scroll past this 326 | * point. 327 | * @param maxX Maximum X value. The scroller will not scroll past this 328 | * point. 329 | * @param minY Minimum Y value. The scroller will not scroll past this 330 | * point. 331 | * @param maxY Maximum Y value. The scroller will not scroll past this 332 | * point. 333 | */ 334 | public void fling(int startX, int startY, int velocityX, int velocityY, 335 | int minX, int maxX, int minY, int maxY) { 336 | // Continue a scroll or fling in progress 337 | if (mFlywheel && !mFinished) { 338 | float oldVel = getCurrVelocity(); 339 | 340 | float dx = (float) (mFinalX - mStartX); 341 | float dy = (float) (mFinalY - mStartY); 342 | float hyp = FloatMath.sqrt(dx * dx + dy * dy); 343 | 344 | float ndx = dx / hyp; 345 | float ndy = dy / hyp; 346 | 347 | float oldVelocityX = ndx * oldVel; 348 | float oldVelocityY = ndy * oldVel; 349 | if (Math.signum(velocityX) == Math.signum(oldVelocityX) && 350 | Math.signum(velocityY) == Math.signum(oldVelocityY)) { 351 | velocityX += oldVelocityX; 352 | velocityY += oldVelocityY; 353 | } 354 | } 355 | 356 | mMode = FLING_MODE; 357 | mFinished = false; 358 | 359 | float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY); 360 | 361 | mVelocity = velocity; 362 | final double l = Math.log(START_TENSION * velocity / ALPHA); 363 | mDuration = (int) (1000.0 * Math.exp(l / (DECELERATION_RATE - 1.0))); 364 | mStartTime = AnimationUtils.currentAnimationTimeMillis(); 365 | mStartX = startX; 366 | mStartY = startY; 367 | 368 | float coeffX = velocity == 0 ? 1.0f : velocityX / velocity; 369 | float coeffY = velocity == 0 ? 1.0f : velocityY / velocity; 370 | 371 | int totalDistance = 372 | (int) (ALPHA * Math.exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l)); 373 | 374 | mMinX = minX; 375 | mMaxX = maxX; 376 | mMinY = minY; 377 | mMaxY = maxY; 378 | 379 | mFinalX = startX + Math.round(totalDistance * coeffX); 380 | // Pin to mMinX <= mFinalX <= mMaxX 381 | mFinalX = Math.min(mFinalX, mMaxX); 382 | mFinalX = Math.max(mFinalX, mMinX); 383 | 384 | mFinalY = startY + Math.round(totalDistance * coeffY); 385 | // Pin to mMinY <= mFinalY <= mMaxY 386 | mFinalY = Math.min(mFinalY, mMaxY); 387 | mFinalY = Math.max(mFinalY, mMinY); 388 | } 389 | 390 | static float viscousFluid(float x) 391 | { 392 | x *= sViscousFluidScale; 393 | if (x < 1.0f) { 394 | x -= (1.0f - (float)Math.exp(-x)); 395 | } else { 396 | float start = 0.36787944117f; // 1/e == exp(-1) 397 | x = 1.0f - (float)Math.exp(1.0f - x); 398 | x = start + x * (1.0f - start); 399 | } 400 | x *= sViscousFluidNormalize; 401 | return x; 402 | } 403 | 404 | /** 405 | * Stops the animation. Contrary to {@link #forceFinished(boolean)}, 406 | * aborting the animating cause the scroller to move to the final x and y 407 | * position 408 | * 409 | * @see #forceFinished(boolean) 410 | */ 411 | public void abortAnimation() { 412 | mCurrX = mFinalX; 413 | mCurrY = mFinalY; 414 | mFinished = true; 415 | } 416 | 417 | /** 418 | * Extend the scroll animation. This allows a running animation to scroll 419 | * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}. 420 | * 421 | * @param extend Additional time to scroll in milliseconds. 422 | * @see #setFinalX(int) 423 | * @see #setFinalY(int) 424 | */ 425 | public void extendDuration(int extend) { 426 | int passed = timePassed(); 427 | mDuration = passed + extend; 428 | mDurationReciprocal = 1.0f / mDuration; 429 | mFinished = false; 430 | } 431 | 432 | /** 433 | * Returns the time elapsed since the beginning of the scrolling. 434 | * 435 | * @return The elapsed time in milliseconds. 436 | */ 437 | public int timePassed() { 438 | return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); 439 | } 440 | 441 | /** 442 | * Sets the final position (X) for this scroller. 443 | * 444 | * @param newX The new X offset as an absolute distance from the origin. 445 | * @see #extendDuration(int) 446 | * @see #setFinalY(int) 447 | */ 448 | public void setFinalX(int newX) { 449 | mFinalX = newX; 450 | mDeltaX = mFinalX - mStartX; 451 | mFinished = false; 452 | } 453 | 454 | /** 455 | * Sets the final position (Y) for this scroller. 456 | * 457 | * @param newY The new Y offset as an absolute distance from the origin. 458 | * @see #extendDuration(int) 459 | * @see #setFinalX(int) 460 | */ 461 | public void setFinalY(int newY) { 462 | mFinalY = newY; 463 | mDeltaY = mFinalY - mStartY; 464 | mFinished = false; 465 | } 466 | 467 | /** 468 | * @hide 469 | */ 470 | public boolean isScrollingInDirection(float xvel, float yvel) { 471 | return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) && 472 | Math.signum(yvel) == Math.signum(mFinalY - mStartY); 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /src/com/qozix/widgets/SealedHandler.java: -------------------------------------------------------------------------------- 1 | package com.qozix.widgets; 2 | 3 | import java.lang.ref.WeakReference; 4 | 5 | import android.os.Handler; 6 | import android.os.Message; 7 | 8 | public abstract class SealedHandler extends Handler { 9 | 10 | private final WeakReference reference; 11 | 12 | public SealedHandler( T entity ) { 13 | super(); 14 | reference = new WeakReference( entity ); 15 | } 16 | 17 | @Override 18 | public final void handleMessage( Message message ) { 19 | final T entity = reference.get(); 20 | if ( entity != null ) { 21 | handleMessage( message, entity ); 22 | } 23 | } 24 | 25 | public abstract void handleMessage( Message message, T entity ); 26 | 27 | } --------------------------------------------------------------------------------