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