1 package com.hwloc.lstopo.ZoomView; 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.util.AttributeSet; 10 import android.view.MotionEvent; 11 import android.view.View; 12 import android.widget.RelativeLayout; 13 14 /** 15 * Zooming view. 16 */ 17 public class ZoomView extends RelativeLayout { 18 19 /** 20 * Zooming view listener interface. 21 * 22 * @author karooolek 23 * 24 */ 25 ZoomView(Context context, AttributeSet attrs, int defStyle)26 public ZoomView(Context context, AttributeSet attrs, int defStyle) { 27 super(context, attrs, defStyle); 28 // TODO Auto-generated constructor stub 29 } 30 ZoomView(Context context, AttributeSet attrs)31 public ZoomView(Context context, AttributeSet attrs) { 32 super(context, attrs); 33 // TODO Auto-generated constructor stub 34 } 35 ZoomView(final Context context)36 public ZoomView(final Context context) { 37 super(context); 38 } 39 40 41 public interface ZoomViewListener { 42 onZoomStarted(float zoom, float zoomx, float zoomy)43 void onZoomStarted(float zoom, float zoomx, float zoomy); 44 onZooming(float zoom, float zoomx, float zoomy)45 void onZooming(float zoom, float zoomx, float zoomy); 46 } 47 48 // zooming 49 float zoom = 1.0f; 50 float maxZoom = 4.0f; 51 float smoothZoom = 1.0f; 52 float zoomX, zoomY; 53 float smoothZoomX, smoothZoomY; 54 private boolean scrolling; // NOPMD by karooolek on 29.06.11 11:45 55 56 // minimap variables 57 private boolean showMinimap = false; 58 private int miniMapColor = Color.WHITE; 59 private int miniMapHeight = -1; 60 private String miniMapCaption; 61 private float miniMapCaptionSize = 10.0f; 62 private int miniMapCaptionColor = Color.WHITE; 63 64 // touching variables 65 private long lastTapTime; 66 private float touchStartX, touchStartY; 67 private float touchLastX, touchLastY; 68 private float startd; 69 private boolean pinching; 70 private float lastd; 71 private float lastdx1, lastdy1; 72 private float lastdx2, lastdy2; 73 74 // drawing 75 private final Matrix m = new Matrix(); 76 private final Paint p = new Paint(); 77 78 // listener 79 ZoomViewListener listener; 80 81 private Bitmap ch; 82 getZoom()83 public float getZoom() { 84 return zoom; 85 } 86 getMaxZoom()87 public float getMaxZoom() { 88 return maxZoom; 89 } 90 setMaxZoom(final float maxZoom)91 public void setMaxZoom(final float maxZoom) { 92 if (maxZoom < 1.0f) { 93 return; 94 } 95 96 this.maxZoom = maxZoom; 97 } 98 setMiniMapEnabled(final boolean showMiniMap)99 public void setMiniMapEnabled(final boolean showMiniMap) { 100 this.showMinimap = showMiniMap; 101 } 102 isMiniMapEnabled()103 public boolean isMiniMapEnabled() { 104 return showMinimap; 105 } 106 setMiniMapHeight(final int miniMapHeight)107 public void setMiniMapHeight(final int miniMapHeight) { 108 if (miniMapHeight < 0) { 109 return; 110 } 111 this.miniMapHeight = miniMapHeight; 112 } 113 getMiniMapHeight()114 public int getMiniMapHeight() { 115 return miniMapHeight; 116 } 117 setMiniMapColor(final int color)118 public void setMiniMapColor(final int color) { 119 miniMapColor = color; 120 } 121 getMiniMapColor()122 public int getMiniMapColor() { 123 return miniMapColor; 124 } 125 getMiniMapCaption()126 public String getMiniMapCaption() { 127 return miniMapCaption; 128 } 129 setMiniMapCaption(final String miniMapCaption)130 public void setMiniMapCaption(final String miniMapCaption) { 131 this.miniMapCaption = miniMapCaption; 132 } 133 getMiniMapCaptionSize()134 public float getMiniMapCaptionSize() { 135 return miniMapCaptionSize; 136 } 137 setMiniMapCaptionSize(final float size)138 public void setMiniMapCaptionSize(final float size) { 139 miniMapCaptionSize = size; 140 } 141 getMiniMapCaptionColor()142 public int getMiniMapCaptionColor() { 143 return miniMapCaptionColor; 144 } 145 setMiniMapCaptionColor(final int color)146 public void setMiniMapCaptionColor(final int color) { 147 miniMapCaptionColor = color; 148 } 149 zoomTo(final float zoom, final float x, final float y)150 public void zoomTo(final float zoom, final float x, final float y) { 151 this.zoom = Math.min(zoom, maxZoom); 152 zoomX = x; 153 zoomY = y; 154 smoothZoomTo(this.zoom, x, y); 155 } 156 smoothZoomTo(final float zoom, final float x, final float y)157 public void smoothZoomTo(final float zoom, final float x, final float y) { 158 smoothZoom = clamp(1.0f, zoom, maxZoom); 159 smoothZoomX = x; 160 smoothZoomY = y; 161 if (listener != null) { 162 listener.onZoomStarted(smoothZoom, x, y); 163 } 164 } 165 getListener()166 public ZoomViewListener getListener() { 167 return listener; 168 } 169 setListner(final ZoomViewListener listener)170 public void setListner(final ZoomViewListener listener) { 171 this.listener = listener; 172 } 173 getZoomFocusX()174 public float getZoomFocusX() { 175 return zoomX * zoom; 176 } 177 getZoomFocusY()178 public float getZoomFocusY() { 179 return zoomY * zoom; 180 } 181 182 @Override dispatchTouchEvent(final MotionEvent ev)183 public boolean dispatchTouchEvent(final MotionEvent ev) { 184 // single touch 185 if (ev.getPointerCount() == 1) { 186 processSingleTouchEvent(ev); 187 } 188 189 // // double touch 190 if (ev.getPointerCount() == 2) { 191 processDoubleTouchEvent(ev); 192 } 193 194 // redraw 195 getRootView().invalidate(); 196 invalidate(); 197 198 return true; 199 } 200 processSingleTouchEvent(final MotionEvent ev)201 private void processSingleTouchEvent(final MotionEvent ev) { 202 203 final float x = ev.getX(); 204 final float y = ev.getY(); 205 206 final float w = miniMapHeight * (float) getWidth() / getHeight(); 207 final float h = miniMapHeight; 208 final boolean touchingMiniMap = x >= 10.0f && x <= 10.0f + w 209 && y >= 10.0f && y <= 10.0f + h; 210 211 if (showMinimap && smoothZoom > 1.0f && touchingMiniMap) { 212 processSingleTouchOnMinimap(ev); 213 } else { 214 processSingleTouchOutsideMinimap(ev); 215 } 216 } 217 processSingleTouchOnMinimap(final MotionEvent ev)218 private void processSingleTouchOnMinimap(final MotionEvent ev) { 219 final float x = ev.getX(); 220 final float y = ev.getY(); 221 222 final float w = miniMapHeight * (float) getWidth() / getHeight(); 223 final float h = miniMapHeight; 224 final float zx = (x - 10.0f) / w * getWidth(); 225 final float zy = (y - 10.0f) / h * getHeight(); 226 smoothZoomTo(smoothZoom, zx, zy); 227 } 228 processSingleTouchOutsideMinimap(final MotionEvent ev)229 private void processSingleTouchOutsideMinimap(final MotionEvent ev) { 230 final float x = ev.getX(); 231 final float y = ev.getY(); 232 float lx = x - touchStartX; 233 float ly = y - touchStartY; 234 final float l = (float) Math.hypot(lx, ly); 235 float dx = x - touchLastX; 236 float dy = y - touchLastY; 237 touchLastX = x; 238 touchLastY = y; 239 240 switch (ev.getAction()) { 241 case MotionEvent.ACTION_DOWN: 242 touchStartX = x; 243 touchStartY = y; 244 touchLastX = x; 245 touchLastY = y; 246 dx = 0; 247 dy = 0; 248 lx = 0; 249 ly = 0; 250 scrolling = false; 251 break; 252 253 case MotionEvent.ACTION_MOVE: 254 if (scrolling || (smoothZoom > 1.0f && l > 30.0f)) { 255 if (!scrolling) { 256 scrolling = true; 257 ev.setAction(MotionEvent.ACTION_CANCEL); 258 super.dispatchTouchEvent(ev); 259 } 260 smoothZoomX -= dx / zoom; 261 smoothZoomY -= dy / zoom; 262 return; 263 } 264 break; 265 266 case MotionEvent.ACTION_OUTSIDE: 267 case MotionEvent.ACTION_UP: 268 269 // tap 270 if (l < 30.0f) { 271 // check double tap 272 if (System.currentTimeMillis() - lastTapTime < 500) { 273 if (smoothZoom == 1.0f) { 274 smoothZoomTo(maxZoom, x, y); 275 } else { 276 smoothZoomTo(1.0f, getWidth() / 2.0f, 277 getHeight() / 2.0f); 278 } 279 lastTapTime = 0; 280 ev.setAction(MotionEvent.ACTION_CANCEL); 281 super.dispatchTouchEvent(ev); 282 return; 283 } 284 285 lastTapTime = System.currentTimeMillis(); 286 287 performClick(); 288 } 289 break; 290 291 default: 292 break; 293 } 294 295 ev.setLocation(zoomX + (x - 0.5f * getWidth()) / zoom, zoomY 296 + (y - 0.5f * getHeight()) / zoom); 297 298 ev.getX(); 299 ev.getY(); 300 301 super.dispatchTouchEvent(ev); 302 } 303 processDoubleTouchEvent(final MotionEvent ev)304 private void processDoubleTouchEvent(final MotionEvent ev) { 305 final float x1 = ev.getX(0); 306 final float dx1 = x1 - lastdx1; 307 lastdx1 = x1; 308 final float y1 = ev.getY(0); 309 final float dy1 = y1 - lastdy1; 310 lastdy1 = y1; 311 final float x2 = ev.getX(1); 312 final float dx2 = x2 - lastdx2; 313 lastdx2 = x2; 314 final float y2 = ev.getY(1); 315 final float dy2 = y2 - lastdy2; 316 lastdy2 = y2; 317 318 // pointers distance 319 final float d = (float) Math.hypot(x2 - x1, y2 - y1); 320 final float dd = d - lastd; 321 lastd = d; 322 final float ld = Math.abs(d - startd); 323 324 Math.atan2(y2 - y1, x2 - x1); 325 switch (ev.getAction()) { 326 case MotionEvent.ACTION_DOWN: 327 startd = d; 328 pinching = false; 329 break; 330 331 case MotionEvent.ACTION_MOVE: 332 if (pinching || ld > 30.0f) { 333 pinching = true; 334 final float dxk = 0.5f * (dx1 + dx2); 335 final float dyk = 0.5f * (dy1 + dy2); 336 smoothZoomTo(Math.max(1.0f, zoom * d / (d - dd)), zoomX - dxk 337 / zoom, zoomY - dyk / zoom); 338 } 339 340 break; 341 342 case MotionEvent.ACTION_UP: 343 default: 344 pinching = false; 345 break; 346 } 347 348 ev.setAction(MotionEvent.ACTION_CANCEL); 349 super.dispatchTouchEvent(ev); 350 } 351 clamp(final float min, final float value, final float max)352 private float clamp(final float min, final float value, final float max) { 353 return Math.max(min, Math.min(value, max)); 354 } 355 lerp(final float a, final float b, final float k)356 private float lerp(final float a, final float b, final float k) { 357 return a + (b - a) * k; 358 } 359 bias(final float a, final float b, final float k)360 private float bias(final float a, final float b, final float k) { 361 return Math.abs(b - a) >= k ? a + k * Math.signum(b - a) : b; 362 } 363 364 @Override dispatchDraw(final Canvas canvas)365 protected void dispatchDraw(final Canvas canvas) { 366 367 // do zoom 368 zoom = lerp(bias(zoom, smoothZoom, 0.05f), smoothZoom, 0.2f); 369 smoothZoomX = clamp(0.5f * getWidth() / smoothZoom, smoothZoomX, 370 getWidth() - 0.5f * getWidth() / smoothZoom); 371 smoothZoomY = clamp(0.5f * getHeight() / smoothZoom, smoothZoomY, 372 getHeight() - 0.5f * getHeight() / smoothZoom); 373 374 zoomX = lerp(bias(zoomX, smoothZoomX, 0.1f), smoothZoomX, 0.35f); 375 zoomY = lerp(bias(zoomY, smoothZoomY, 0.1f), smoothZoomY, 0.35f); 376 if (zoom != smoothZoom && listener != null) { 377 listener.onZooming(zoom, zoomX, zoomY); 378 } 379 380 final boolean animating = Math.abs(zoom - smoothZoom) > 0.0000001f 381 || Math.abs(zoomX - smoothZoomX) > 0.0000001f 382 || Math.abs(zoomY - smoothZoomY) > 0.0000001f; 383 384 // nothing to draw 385 if (getChildCount() == 0) { 386 return; 387 } 388 389 // prepare matrix 390 m.setTranslate(0.5f * getWidth(), 0.5f * getHeight()); 391 m.preScale(zoom, zoom); 392 m.preTranslate( 393 -clamp(0.5f * getWidth() / zoom, zoomX, getWidth() - 0.5f 394 * getWidth() / zoom), 395 -clamp(0.5f * getHeight() / zoom, zoomY, getHeight() - 0.5f 396 * getHeight() / zoom)); 397 398 // get view 399 final View v = getChildAt(0); 400 m.preTranslate(v.getLeft(), v.getTop()); 401 402 // get drawing cache if available 403 if (animating && ch == null && isAnimationCacheEnabled()) { 404 v.setDrawingCacheEnabled(true); 405 ch = v.getDrawingCache(); 406 } 407 408 // draw using cache while animating 409 if (animating && isAnimationCacheEnabled() && ch != null) { 410 p.setColor(0xffffffff); 411 canvas.drawBitmap(ch, m, p); 412 } else { // zoomed or cache unavailable 413 ch = null; 414 canvas.save(); 415 canvas.concat(m); 416 v.draw(canvas); 417 canvas.restore(); 418 } 419 420 // draw minimap 421 if (showMinimap) { 422 if (miniMapHeight < 0) { 423 miniMapHeight = getHeight() / 4; 424 } 425 426 canvas.translate(10.0f, 10.0f); 427 428 p.setColor(0x80000000 | 0x00ffffff & miniMapColor); 429 final float w = miniMapHeight * (float) getWidth() / getHeight(); 430 final float h = miniMapHeight; 431 canvas.drawRect(0.0f, 0.0f, w, h, p); 432 433 if (miniMapCaption != null && miniMapCaption.length() > 0) { 434 p.setTextSize(miniMapCaptionSize); 435 p.setColor(miniMapCaptionColor); 436 p.setAntiAlias(true); 437 canvas.drawText(miniMapCaption, 10.0f, 438 10.0f + miniMapCaptionSize, p); 439 p.setAntiAlias(false); 440 } 441 442 p.setColor(0x80000000 | 0x00ffffff & miniMapColor); 443 final float dx = w * zoomX / getWidth(); 444 final float dy = h * zoomY / getHeight(); 445 canvas.drawRect(dx - 0.5f * w / zoom, dy - 0.5f * h / zoom, dx 446 + 0.5f * w / zoom, dy + 0.5f * h / zoom, p); 447 448 canvas.translate(-10.0f, -10.0f); 449 } 450 451 // redraw 452 // if (animating) { 453 getRootView().invalidate(); 454 invalidate(); 455 // } 456 } 457 resetZoom()458 public void resetZoom() { 459 smoothZoomTo(1.0f, getWidth() / 2.0f, 460 getHeight() / 2.0f); 461 } 462 setZoom(float zoom)463 public void setZoom(float zoom) { 464 this.zoom = zoom; 465 } 466 } 467