├── .classpath ├── .gitignore ├── .project ├── AndroidManifest.xml ├── README.md ├── build.gradle ├── build.xml ├── gradle.properties ├── licence.txt ├── proguard.cfg ├── project.properties └── src └── pl └── polidea └── view └── ZoomView.java /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ^.gradle$ 2 | ^log$ 3 | ^build$ 4 | ^bin$ 5 | ^gen$ 6 | ^local.properties$ 7 | ^ota$ 8 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android-zoom-view 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | __This repository is no longer maintained. Issue reports and pull requests will not be attended.__ 4 | 5 | --- 6 | 7 | Free and efficient widget for creating zoomable applications. 8 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | configurations.all { 3 | resolutionStrategy.cacheChangingModulesFor 0, 'seconds' 4 | } 5 | repositories { 6 | maven { 7 | url "https://oss.sonatype.org/content/repositories/snapshots/" 8 | } 9 | mavenLocal() 10 | mavenCentral() 11 | } 12 | dependencies { 13 | classpath ('com.apphance.ameba:Ameba:0.99.6') 14 | } 15 | } 16 | 17 | apply plugin: 'ameba-project-configuration' 18 | apply plugin: 'ameba-android-build' 19 | apply plugin: 'ameba-project-release' 20 | apply plugin: 'ameba-android-release' 21 | //apply plugin: 'ameba-android-apphance' 22 | apply plugin: 'ameba-android-analysis' 23 | //apply plugin: 'ameba-android-test' 24 | //apply plugin: 'ameba-android-jarlibrary' 25 | apply from: 'http://dev.polidea.pl/ext/Ameba/polidea_conventions.gradle' -------------------------------------------------------------------------------- /build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 29 | 30 | 31 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | 51 | 63 | 64 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ########################################################### 2 | # Generated by Ameba system by running 3 | # gradle prepareSetup 4 | # You can modify the file manually. 5 | # Or you can re-run the prepareSetup command 6 | # for guided re-configuration 7 | ########################################################### 8 | # Android properties 9 | ########################################################### 10 | android.mainVariant=Debug 11 | android.excluded.builds= 12 | android.minSdk.target=android-8 13 | ########################################################### 14 | # Release properties 15 | ########################################################### 16 | release.project.icon.file= 17 | release.project.url=http://dev.polidea.pl/ext/Zoom_View/ 18 | release.project.language=en 19 | release.project.country=US 20 | release.mail.from=Jenkins 21 | release.mail.to=lab@polidea.pl 22 | release.mail.flags=qrCode,imageMontage 23 | -------------------------------------------------------------------------------- /licence.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2016 Polidea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /proguard.cfg: -------------------------------------------------------------------------------- 1 | -optimizationpasses 5 2 | -dontusemixedcaseclassnames 3 | -dontskipnonpubliclibraryclasses 4 | -dontpreverify 5 | -verbose 6 | -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* 7 | 8 | -keep public class * extends android.app.Activity 9 | -keep public class * extends android.app.Application 10 | -keep public class * extends android.app.Service 11 | -keep public class * extends android.content.BroadcastReceiver 12 | -keep public class * extends android.content.ContentProvider 13 | -keep public class * extends android.app.backup.BackupAgentHelper 14 | -keep public class * extends android.preference.Preference 15 | -keep public class com.android.vending.licensing.ILicensingService 16 | 17 | -keepclasseswithmembernames class * { 18 | native ; 19 | } 20 | 21 | -keepclasseswithmembers class * { 22 | public (android.content.Context, android.util.AttributeSet); 23 | } 24 | 25 | -keepclasseswithmembers class * { 26 | public (android.content.Context, android.util.AttributeSet, int); 27 | } 28 | 29 | -keepclassmembers class * extends android.app.Activity { 30 | public void *(android.view.View); 31 | } 32 | 33 | -keepclassmembers enum * { 34 | public static **[] values(); 35 | public static ** valueOf(java.lang.String); 36 | } 37 | 38 | -keep class * implements android.os.Parcelable { 39 | public static final android.os.Parcelable$Creator *; 40 | } 41 | -------------------------------------------------------------------------------- /project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system use, 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | 10 | # Project target. 11 | target=android-7 12 | android.library=true 13 | -------------------------------------------------------------------------------- /src/pl/polidea/view/ZoomView.java: -------------------------------------------------------------------------------- 1 | package pl.polidea.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Matrix; 8 | import android.graphics.Paint; 9 | import android.view.MotionEvent; 10 | import android.view.View; 11 | import android.widget.FrameLayout; 12 | 13 | /** 14 | * Zooming view. 15 | */ 16 | public class ZoomView extends FrameLayout { 17 | 18 | /** 19 | * Zooming view listener interface. 20 | * 21 | * @author karooolek 22 | * 23 | */ 24 | public interface ZoomViewListener { 25 | 26 | void onZoomStarted(float zoom, float zoomx, float zoomy); 27 | 28 | void onZooming(float zoom, float zoomx, float zoomy); 29 | 30 | void onZoomEnded(float zoom, float zoomx, float zoomy); 31 | } 32 | 33 | // zooming 34 | float zoom = 1.0f; 35 | float maxZoom = 2.0f; 36 | float smoothZoom = 1.0f; 37 | float zoomX, zoomY; 38 | float smoothZoomX, smoothZoomY; 39 | private boolean scrolling; // NOPMD by karooolek on 29.06.11 11:45 40 | 41 | // minimap variables 42 | private boolean showMinimap = false; 43 | private int miniMapColor = Color.BLACK; 44 | private int miniMapHeight = -1; 45 | private String miniMapCaption; 46 | private float miniMapCaptionSize = 10.0f; 47 | private int miniMapCaptionColor = Color.WHITE; 48 | 49 | // touching variables 50 | private long lastTapTime; 51 | private float touchStartX, touchStartY; 52 | private float touchLastX, touchLastY; 53 | private float startd; 54 | private boolean pinching; 55 | private float lastd; 56 | private float lastdx1, lastdy1; 57 | private float lastdx2, lastdy2; 58 | 59 | // drawing 60 | private final Matrix m = new Matrix(); 61 | private final Paint p = new Paint(); 62 | 63 | // listener 64 | ZoomViewListener listener; 65 | 66 | private Bitmap ch; 67 | 68 | public ZoomView(final Context context) { 69 | super(context); 70 | } 71 | 72 | public float getZoom() { 73 | return zoom; 74 | } 75 | 76 | public float getMaxZoom() { 77 | return maxZoom; 78 | } 79 | 80 | public void setMaxZoom(final float maxZoom) { 81 | if (maxZoom < 1.0f) { 82 | return; 83 | } 84 | 85 | this.maxZoom = maxZoom; 86 | } 87 | 88 | public void setMiniMapEnabled(final boolean showMiniMap) { 89 | this.showMinimap = showMiniMap; 90 | } 91 | 92 | public boolean isMiniMapEnabled() { 93 | return showMinimap; 94 | } 95 | 96 | public void setMiniMapHeight(final int miniMapHeight) { 97 | if (miniMapHeight < 0) { 98 | return; 99 | } 100 | this.miniMapHeight = miniMapHeight; 101 | } 102 | 103 | public int getMiniMapHeight() { 104 | return miniMapHeight; 105 | } 106 | 107 | public void setMiniMapColor(final int color) { 108 | miniMapColor = color; 109 | } 110 | 111 | public int getMiniMapColor() { 112 | return miniMapColor; 113 | } 114 | 115 | public String getMiniMapCaption() { 116 | return miniMapCaption; 117 | } 118 | 119 | public void setMiniMapCaption(final String miniMapCaption) { 120 | this.miniMapCaption = miniMapCaption; 121 | } 122 | 123 | public float getMiniMapCaptionSize() { 124 | return miniMapCaptionSize; 125 | } 126 | 127 | public void setMiniMapCaptionSize(final float size) { 128 | miniMapCaptionSize = size; 129 | } 130 | 131 | public int getMiniMapCaptionColor() { 132 | return miniMapCaptionColor; 133 | } 134 | 135 | public void setMiniMapCaptionColor(final int color) { 136 | miniMapCaptionColor = color; 137 | } 138 | 139 | public void zoomTo(final float zoom, final float x, final float y) { 140 | this.zoom = Math.min(zoom, maxZoom); 141 | zoomX = x; 142 | zoomY = y; 143 | smoothZoomTo(this.zoom, x, y); 144 | } 145 | 146 | public void smoothZoomTo(final float zoom, final float x, final float y) { 147 | smoothZoom = clamp(1.0f, zoom, maxZoom); 148 | smoothZoomX = x; 149 | smoothZoomY = y; 150 | if (listener != null) { 151 | listener.onZoomStarted(smoothZoom, x, y); 152 | } 153 | } 154 | 155 | public ZoomViewListener getListener() { 156 | return listener; 157 | } 158 | 159 | public void setListner(final ZoomViewListener listener) { 160 | this.listener = listener; 161 | } 162 | 163 | public float getZoomFocusX() { 164 | return zoomX * zoom; 165 | } 166 | 167 | public float getZoomFocusY() { 168 | return zoomY * zoom; 169 | } 170 | 171 | @Override 172 | public boolean dispatchTouchEvent(final MotionEvent ev) { 173 | // single touch 174 | if (ev.getPointerCount() == 1) { 175 | processSingleTouchEvent(ev); 176 | } 177 | 178 | // // double touch 179 | if (ev.getPointerCount() == 2) { 180 | processDoubleTouchEvent(ev); 181 | } 182 | 183 | // redraw 184 | getRootView().invalidate(); 185 | invalidate(); 186 | 187 | return true; 188 | } 189 | 190 | private void processSingleTouchEvent(final MotionEvent ev) { 191 | 192 | final float x = ev.getX(); 193 | final float y = ev.getY(); 194 | 195 | final float w = miniMapHeight * (float) getWidth() / getHeight(); 196 | final float h = miniMapHeight; 197 | final boolean touchingMiniMap = x >= 10.0f && x <= 10.0f + w && y >= 10.0f && y <= 10.0f + h; 198 | 199 | if (showMinimap && smoothZoom > 1.0f && touchingMiniMap) { 200 | processSingleTouchOnMinimap(ev); 201 | } else { 202 | processSingleTouchOutsideMinimap(ev); 203 | } 204 | } 205 | 206 | private void processSingleTouchOnMinimap(final MotionEvent ev) { 207 | final float x = ev.getX(); 208 | final float y = ev.getY(); 209 | 210 | final float w = miniMapHeight * (float) getWidth() / getHeight(); 211 | final float h = miniMapHeight; 212 | final float zx = (x - 10.0f) / w * getWidth(); 213 | final float zy = (y - 10.0f) / h * getHeight(); 214 | smoothZoomTo(smoothZoom, zx, zy); 215 | } 216 | 217 | private void processSingleTouchOutsideMinimap(final MotionEvent ev) { 218 | final float x = ev.getX(); 219 | final float y = ev.getY(); 220 | float lx = x - touchStartX; 221 | float ly = y - touchStartY; 222 | final float l = (float) Math.hypot(lx, ly); 223 | float dx = x - touchLastX; 224 | float dy = y - touchLastY; 225 | touchLastX = x; 226 | touchLastY = y; 227 | 228 | switch (ev.getAction()) { 229 | case MotionEvent.ACTION_DOWN: 230 | touchStartX = x; 231 | touchStartY = y; 232 | touchLastX = x; 233 | touchLastY = y; 234 | dx = 0; 235 | dy = 0; 236 | lx = 0; 237 | ly = 0; 238 | scrolling = false; 239 | break; 240 | 241 | case MotionEvent.ACTION_MOVE: 242 | if (scrolling || (smoothZoom > 1.0f && l > 30.0f)) { 243 | if (!scrolling) { 244 | scrolling = true; 245 | ev.setAction(MotionEvent.ACTION_CANCEL); 246 | super.dispatchTouchEvent(ev); 247 | } 248 | smoothZoomX -= dx / zoom; 249 | smoothZoomY -= dy / zoom; 250 | return; 251 | } 252 | break; 253 | 254 | case MotionEvent.ACTION_OUTSIDE: 255 | case MotionEvent.ACTION_UP: 256 | 257 | // tap 258 | if (l < 30.0f) { 259 | // check double tap 260 | if (System.currentTimeMillis() - lastTapTime < 500) { 261 | if (smoothZoom == 1.0f) { 262 | smoothZoomTo(maxZoom, x, y); 263 | } else { 264 | smoothZoomTo(1.0f, getWidth() / 2.0f, getHeight() / 2.0f); 265 | } 266 | lastTapTime = 0; 267 | ev.setAction(MotionEvent.ACTION_CANCEL); 268 | super.dispatchTouchEvent(ev); 269 | return; 270 | } 271 | 272 | lastTapTime = System.currentTimeMillis(); 273 | 274 | performClick(); 275 | } 276 | break; 277 | 278 | default: 279 | break; 280 | } 281 | 282 | ev.setLocation(zoomX + (x - 0.5f * getWidth()) / zoom, zoomY + (y - 0.5f * getHeight()) / zoom); 283 | 284 | ev.getX(); 285 | ev.getY(); 286 | 287 | super.dispatchTouchEvent(ev); 288 | } 289 | 290 | private void processDoubleTouchEvent(final MotionEvent ev) { 291 | final float x1 = ev.getX(0); 292 | final float dx1 = x1 - lastdx1; 293 | lastdx1 = x1; 294 | final float y1 = ev.getY(0); 295 | final float dy1 = y1 - lastdy1; 296 | lastdy1 = y1; 297 | final float x2 = ev.getX(1); 298 | final float dx2 = x2 - lastdx2; 299 | lastdx2 = x2; 300 | final float y2 = ev.getY(1); 301 | final float dy2 = y2 - lastdy2; 302 | lastdy2 = y2; 303 | 304 | // pointers distance 305 | final float d = (float) Math.hypot(x2 - x1, y2 - y1); 306 | final float dd = d - lastd; 307 | lastd = d; 308 | final float ld = Math.abs(d - startd); 309 | 310 | Math.atan2(y2 - y1, x2 - x1); 311 | switch (ev.getAction()) { 312 | case MotionEvent.ACTION_DOWN: 313 | startd = d; 314 | pinching = false; 315 | break; 316 | 317 | case MotionEvent.ACTION_MOVE: 318 | if (pinching || ld > 30.0f) { 319 | pinching = true; 320 | final float dxk = 0.5f * (dx1 + dx2); 321 | final float dyk = 0.5f * (dy1 + dy2); 322 | smoothZoomTo(Math.max(1.0f, zoom * d / (d - dd)), zoomX - dxk / zoom, zoomY - dyk / zoom); 323 | } 324 | 325 | break; 326 | 327 | case MotionEvent.ACTION_UP: 328 | default: 329 | pinching = false; 330 | break; 331 | } 332 | 333 | ev.setAction(MotionEvent.ACTION_CANCEL); 334 | super.dispatchTouchEvent(ev); 335 | } 336 | 337 | private float clamp(final float min, final float value, final float max) { 338 | return Math.max(min, Math.min(value, max)); 339 | } 340 | 341 | private float lerp(final float a, final float b, final float k) { 342 | return a + (b - a) * k; 343 | } 344 | 345 | private float bias(final float a, final float b, final float k) { 346 | return Math.abs(b - a) >= k ? a + k * Math.signum(b - a) : b; 347 | } 348 | 349 | @Override 350 | protected void dispatchDraw(final Canvas canvas) { 351 | // do zoom 352 | zoom = lerp(bias(zoom, smoothZoom, 0.05f), smoothZoom, 0.2f); 353 | smoothZoomX = clamp(0.5f * getWidth() / smoothZoom, smoothZoomX, getWidth() - 0.5f * getWidth() / smoothZoom); 354 | smoothZoomY = clamp(0.5f * getHeight() / smoothZoom, smoothZoomY, getHeight() - 0.5f * getHeight() / smoothZoom); 355 | 356 | zoomX = lerp(bias(zoomX, smoothZoomX, 0.1f), smoothZoomX, 0.35f); 357 | zoomY = lerp(bias(zoomY, smoothZoomY, 0.1f), smoothZoomY, 0.35f); 358 | if (zoom != smoothZoom && listener != null) { 359 | listener.onZooming(zoom, zoomX, zoomY); 360 | } 361 | 362 | final boolean animating = Math.abs(zoom - smoothZoom) > 0.0000001f 363 | || Math.abs(zoomX - smoothZoomX) > 0.0000001f || Math.abs(zoomY - smoothZoomY) > 0.0000001f; 364 | 365 | // nothing to draw 366 | if (getChildCount() == 0) { 367 | return; 368 | } 369 | 370 | // prepare matrix 371 | m.setTranslate(0.5f * getWidth(), 0.5f * getHeight()); 372 | m.preScale(zoom, zoom); 373 | m.preTranslate(-clamp(0.5f * getWidth() / zoom, zoomX, getWidth() - 0.5f * getWidth() / zoom), 374 | -clamp(0.5f * getHeight() / zoom, zoomY, getHeight() - 0.5f * getHeight() / zoom)); 375 | 376 | // get view 377 | final View v = getChildAt(0); 378 | m.preTranslate(v.getLeft(), v.getTop()); 379 | 380 | // get drawing cache if available 381 | if (animating && ch == null && isAnimationCacheEnabled()) { 382 | v.setDrawingCacheEnabled(true); 383 | ch = v.getDrawingCache(); 384 | } 385 | 386 | // draw using cache while animating 387 | if (animating && isAnimationCacheEnabled() && ch != null) { 388 | p.setColor(0xffffffff); 389 | canvas.drawBitmap(ch, m, p); 390 | } else { // zoomed or cache unavailable 391 | ch = null; 392 | canvas.save(); 393 | canvas.concat(m); 394 | v.draw(canvas); 395 | canvas.restore(); 396 | } 397 | 398 | // draw minimap 399 | if (showMinimap) { 400 | if (miniMapHeight < 0) { 401 | miniMapHeight = getHeight() / 4; 402 | } 403 | 404 | canvas.translate(10.0f, 10.0f); 405 | 406 | p.setColor(0x80000000 | 0x00ffffff & miniMapColor); 407 | final float w = miniMapHeight * (float) getWidth() / getHeight(); 408 | final float h = miniMapHeight; 409 | canvas.drawRect(0.0f, 0.0f, w, h, p); 410 | 411 | if (miniMapCaption != null && miniMapCaption.length() > 0) { 412 | p.setTextSize(miniMapCaptionSize); 413 | p.setColor(miniMapCaptionColor); 414 | p.setAntiAlias(true); 415 | canvas.drawText(miniMapCaption, 10.0f, 10.0f + miniMapCaptionSize, p); 416 | p.setAntiAlias(false); 417 | } 418 | 419 | p.setColor(0x80000000 | 0x00ffffff & miniMapColor); 420 | final float dx = w * zoomX / getWidth(); 421 | final float dy = h * zoomY / getHeight(); 422 | canvas.drawRect(dx - 0.5f * w / zoom, dy - 0.5f * h / zoom, dx + 0.5f * w / zoom, dy + 0.5f * h / zoom, p); 423 | 424 | canvas.translate(-10.0f, -10.0f); 425 | } 426 | 427 | // redraw 428 | // if (animating) { 429 | getRootView().invalidate(); 430 | invalidate(); 431 | // } 432 | } 433 | } 434 | --------------------------------------------------------------------------------