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 |
Fixed bug in pixel-based positioning created in 1.0.1
13 |
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)
14 |
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)
15 |
Added concrete implementation of `MapEventListener` called `MapEventListenerImplementation`, that impelemts all signatures so you can just override the one's you're using
Updating to 1.0.1, fixing several bugs, reinstituting undocumented or previously removed features, and adding some experimental stuff.
21 |
22 |
Fixes:
23 |
24 |
Fixed bug in draw path where start y position was incorrectly reading x position
25 |
Fixed bug in setZoom where it was maxing to the minimum (this method should work properly now)
26 |
Fixed bug in geolocation math where the relative scale of the current zoom level was not being considered
27 |
Fixed bug in all slideTo methods where postInvalidate was not being called
28 |
Fixed bug where a render request was not issued at the conclusion of a slideTo animation
29 |
Fixed signature of removeHotSpot
30 |
31 |
32 |
Enhancements:
33 |
34 |
Reinstated support for downsamples images. See MapView.java for new signatures
35 |
No longer intercepts touch events by default. This can be enabled with `setShouldIntercept(boolean)`
36 |
No longer caches tile images by default. This can be enabled with `setCacheEnabled(boolean)`
37 |
added `clear` and `destroy` methods. The former is appropriate for `onPause`, the latter for `onDestroy` (incl. orientation changes)
38 |
added `resetZoomLevels` to allow different sets to be used during runtime
39 |
onFlingComplete now passes the x and y value of the final position
40 |
added onScrollComplete (fires when a slideTo method finishes)
41 |
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)
42 |
43 |
44 |
Known issues:
45 |
46 |
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.
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 |
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 |
72 |
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 |
76 |
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:
.
79 | Testing for permission is on the todo list - right now you'll just get a runtime failure.
80 |
81 |
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 |
86 |
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 |
89 |
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 |
93 |
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 |
97 |
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 |
100 |
101 | Drastic refactorization and optimization of the core classes.
102 |
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 |
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 |
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}.)
When an asynchronous task is executed, the task goes through 4 steps:
116 | *
117 | *
{@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.
120 | *
{@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.
128 | *
{@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.
133 | *
{@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.
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).
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