1package com.androidplot.xy;
2
3import android.content.Context;
4import android.graphics.PointF;
5import android.util.AttributeSet;
6import android.view.MotionEvent;
7import android.view.View;
8import android.view.View.OnTouchListener;
9
10public class XYPlotZoomPan extends XYPlot implements OnTouchListener {
11    private static final float MIN_DIST_2_FING = 5f;
12
13    // Definition of the touch states
14    private enum State
15    {
16        NONE,
17        ONE_FINGER_DRAG,
18        TWO_FINGERS_DRAG
19    }
20
21    private State mode = State.NONE;
22    private float minXLimit = Float.MAX_VALUE;
23    private float maxXLimit = Float.MAX_VALUE;
24    private float minYLimit = Float.MAX_VALUE;
25    private float maxYLimit = Float.MAX_VALUE;
26    private float lastMinX = Float.MAX_VALUE;
27    private float lastMaxX = Float.MAX_VALUE;
28    private float lastMinY = Float.MAX_VALUE;
29    private float lastMaxY = Float.MAX_VALUE;
30    private PointF firstFingerPos;
31    private float mDistX;
32    private boolean mZoomEnabled; //default is enabled
33    private boolean mZoomVertically;
34    private boolean mZoomHorizontally;
35    private boolean mCalledBySelf;
36    private boolean mZoomEnabledInit;
37    private boolean mZoomVerticallyInit;
38    private boolean mZoomHorizontallyInit;
39
40    public XYPlotZoomPan(Context context, String title, RenderMode mode) {
41        super(context, title, mode);
42        setZoomEnabled(true); //Default is ZoomEnabled if instantiated programmatically
43    }
44
45    public XYPlotZoomPan(final Context context, final AttributeSet attrs) {
46        super(context, attrs);
47        if(mZoomEnabled || !mZoomEnabledInit) {
48            setZoomEnabled(true);
49        }
50        if(!mZoomHorizontallyInit) {
51            mZoomHorizontally = true;
52        }
53        if(!mZoomVerticallyInit) {
54            mZoomVertically = true;
55        }
56    }
57
58    public XYPlotZoomPan(final Context context, final AttributeSet attrs, final int defStyle) {
59        super(context, attrs, defStyle);
60        if(mZoomEnabled || !mZoomEnabledInit) {
61            setZoomEnabled(true);
62        }
63        if(!mZoomHorizontallyInit) {
64            mZoomHorizontally = true;
65        }
66        if(!mZoomVerticallyInit) {
67            mZoomVertically = true;
68        }
69    }
70
71    public XYPlotZoomPan(final Context context, final String title) {
72        super(context, title);
73    }
74
75    @Override
76    public void setOnTouchListener(OnTouchListener l) {
77        if(l != this) {
78            mZoomEnabled = false;
79        }
80        super.setOnTouchListener(l);
81    }
82
83    public boolean getZoomVertically() {
84        return mZoomVertically;
85    }
86
87    public void setZoomVertically(boolean zoomVertically) {
88        mZoomVertically = zoomVertically;
89        mZoomVerticallyInit = true;
90    }
91
92    public boolean getZoomHorizontally() {
93        return mZoomHorizontally;
94    }
95
96    public void setZoomHorizontally(boolean zoomHorizontally) {
97        mZoomHorizontally = zoomHorizontally;
98        mZoomHorizontallyInit = true;
99    }
100
101    public void setZoomEnabled(boolean enabled) {
102        if(enabled) {
103            setOnTouchListener(this);
104        } else {
105            setOnTouchListener(null);
106        }
107        mZoomEnabled = enabled;
108        mZoomEnabledInit = true;
109    }
110
111    public boolean getZoomEnabled() {
112        return mZoomEnabled;
113    }
114
115    private float getMinXLimit() {
116        if(minXLimit == Float.MAX_VALUE) {
117            minXLimit = getCalculatedMinX().floatValue();
118            lastMinX = minXLimit;
119        }
120        return minXLimit;
121    }
122
123    private float getMaxXLimit() {
124        if(maxXLimit == Float.MAX_VALUE) {
125            maxXLimit = getCalculatedMaxX().floatValue();
126            lastMaxX = maxXLimit;
127        }
128        return maxXLimit;
129    }
130
131    private float getMinYLimit() {
132        if(minYLimit == Float.MAX_VALUE) {
133            minYLimit = getCalculatedMinY().floatValue();
134            lastMinY = minYLimit;
135        }
136        return minYLimit;
137    }
138
139    private float getMaxYLimit() {
140        if(maxYLimit == Float.MAX_VALUE) {
141            maxYLimit = getCalculatedMaxY().floatValue();
142            lastMaxY = maxYLimit;
143        }
144        return maxYLimit;
145    }
146
147    private float getLastMinX() {
148        if(lastMinX == Float.MAX_VALUE) {
149            lastMinX = getCalculatedMinX().floatValue();
150        }
151        return lastMinX;
152    }
153
154    private float getLastMaxX() {
155        if(lastMaxX == Float.MAX_VALUE) {
156            lastMaxX = getCalculatedMaxX().floatValue();
157        }
158        return lastMaxX;
159    }
160
161    private float getLastMinY() {
162        if(lastMinY == Float.MAX_VALUE) {
163            lastMinY = getCalculatedMinY().floatValue();
164        }
165        return lastMinY;
166    }
167
168    private float getLastMaxY() {
169        if(lastMaxY == Float.MAX_VALUE) {
170            lastMaxY = getCalculatedMaxY().floatValue();
171        }
172        return lastMaxY;
173    }
174
175    @Override
176    public synchronized void setDomainBoundaries(final Number lowerBoundary, final BoundaryMode lowerBoundaryMode, final Number upperBoundary, final BoundaryMode upperBoundaryMode) {
177        super.setDomainBoundaries(lowerBoundary, lowerBoundaryMode, upperBoundary, upperBoundaryMode);
178        if(mCalledBySelf) {
179            mCalledBySelf = false;
180        } else {
181            minXLimit = lowerBoundaryMode == BoundaryMode.FIXED ? lowerBoundary.floatValue() : getCalculatedMinX().floatValue();
182            maxXLimit = upperBoundaryMode == BoundaryMode.FIXED ? upperBoundary.floatValue() : getCalculatedMaxX().floatValue();
183            lastMinX = minXLimit;
184            lastMaxX = maxXLimit;
185        }
186    }
187
188    @Override
189    public synchronized void setRangeBoundaries(final Number lowerBoundary, final BoundaryMode lowerBoundaryMode, final Number upperBoundary, final BoundaryMode upperBoundaryMode) {
190        super.setRangeBoundaries(lowerBoundary, lowerBoundaryMode, upperBoundary, upperBoundaryMode);
191        if(mCalledBySelf) {
192            mCalledBySelf = false;
193        } else {
194            minYLimit = lowerBoundaryMode == BoundaryMode.FIXED ? lowerBoundary.floatValue() : getCalculatedMinY().floatValue();
195            maxYLimit = upperBoundaryMode == BoundaryMode.FIXED ? upperBoundary.floatValue() : getCalculatedMaxY().floatValue();
196            lastMinY = minYLimit;
197            lastMaxY = maxYLimit;
198        }
199    }
200
201    @Override
202    public synchronized void setDomainBoundaries(final Number lowerBoundary, final Number upperBoundary, final BoundaryMode mode) {
203        super.setDomainBoundaries(lowerBoundary, upperBoundary, mode);
204        if(mCalledBySelf) {
205            mCalledBySelf = false;
206        } else {
207            minXLimit = mode == BoundaryMode.FIXED ? lowerBoundary.floatValue() : getCalculatedMinX().floatValue();
208            maxXLimit = mode == BoundaryMode.FIXED ? upperBoundary.floatValue() : getCalculatedMaxX().floatValue();
209            lastMinX = minXLimit;
210            lastMaxX = maxXLimit;
211        }
212    }
213
214    @Override
215    public synchronized void setRangeBoundaries(final Number lowerBoundary, final Number upperBoundary, final BoundaryMode mode) {
216        super.setRangeBoundaries(lowerBoundary, upperBoundary, mode);
217        if(mCalledBySelf) {
218            mCalledBySelf = false;
219        } else {
220            minYLimit = mode == BoundaryMode.FIXED ? lowerBoundary.floatValue() : getCalculatedMinY().floatValue();
221            maxYLimit = mode == BoundaryMode.FIXED ? upperBoundary.floatValue() : getCalculatedMaxY().floatValue();
222            lastMinY = minYLimit;
223            lastMaxY = maxYLimit;
224        }
225    }
226
227    @Override
228    public boolean onTouch(final View view, final MotionEvent event) {
229        switch (event.getAction() & MotionEvent.ACTION_MASK)
230        {
231            case MotionEvent.ACTION_DOWN: // start gesture
232                firstFingerPos = new PointF(event.getX(), event.getY());
233                mode = State.ONE_FINGER_DRAG;
234                break;
235            case MotionEvent.ACTION_POINTER_DOWN: // second finger
236            {
237                mDistX = getXDistance(event);
238                // the distance check is done to avoid false alarms
239                if(mDistX > MIN_DIST_2_FING || mDistX < -MIN_DIST_2_FING) {
240                    mode = State.TWO_FINGERS_DRAG;
241                }
242                break;
243            }
244            case MotionEvent.ACTION_POINTER_UP: // end zoom
245                mode = State.NONE;
246                break;
247            case MotionEvent.ACTION_MOVE:
248                if(mode == State.ONE_FINGER_DRAG) {
249                    pan(event);
250                } else if(mode == State.TWO_FINGERS_DRAG) {
251                    zoom(event);
252                }
253                break;
254        }
255        return true;
256    }
257
258    private float getXDistance(final MotionEvent event) {
259        return event.getX(0) - event.getX(1);
260    }
261
262    private void pan(final MotionEvent motionEvent) {
263        final PointF oldFirstFinger = firstFingerPos; //save old position of finger
264        firstFingerPos = new PointF(motionEvent.getX(), motionEvent.getY()); //update finger position
265        PointF newX = new PointF();
266        if(mZoomHorizontally) {
267            calculatePan(oldFirstFinger, newX, true);
268            mCalledBySelf = true;
269            super.setDomainBoundaries(newX.x, newX.y, BoundaryMode.FIXED);
270            lastMinX = newX.x;
271            lastMaxX = newX.y;
272        }
273        if(mZoomVertically) {
274            calculatePan(oldFirstFinger, newX, false);
275            mCalledBySelf = true;
276            super.setRangeBoundaries(newX.x, newX.y, BoundaryMode.FIXED);
277            lastMinY = newX.x;
278            lastMaxY = newX.y;
279        }
280        redraw();
281    }
282
283    private void calculatePan(final PointF oldFirstFinger, PointF newX, final boolean horizontal) {
284        final float offset;
285        // multiply the absolute finger movement for a factor.
286        // the factor is dependent on the calculated min and max
287        if(horizontal) {
288            newX.x = getLastMinX();
289            newX.y = getLastMaxX();
290            offset = (oldFirstFinger.x - firstFingerPos.x) * ((newX.y - newX.x) / getWidth());
291        } else {
292            newX.x = getLastMinY();
293            newX.y = getLastMaxY();
294            offset = -(oldFirstFinger.y - firstFingerPos.y) * ((newX.y - newX.x) / getHeight());
295        }
296        // move the calculated offset
297        newX.x = newX.x + offset;
298        newX.y = newX.y + offset;
299        //get the distance between max and min
300        final float diff = newX.y - newX.x;
301        //check if we reached the limit of panning
302        if(horizontal) {
303            if(newX.x < getMinXLimit()) {
304                newX.x = getMinXLimit();
305                newX.y = newX.x + diff;
306            }
307            if(newX.y > getMaxXLimit()) {
308                newX.y = getMaxXLimit();
309                newX.x = newX.y - diff;
310            }
311        } else {
312            if(newX.x < getMinYLimit()) {
313                newX.x = getMinYLimit();
314                newX.y = newX.x + diff;
315            }
316            if(newX.y > getMaxYLimit()) {
317                newX.y = getMaxYLimit();
318                newX.x = newX.y - diff;
319            }
320        }
321    }
322
323    private void zoom(final MotionEvent motionEvent) {
324        final float oldDist = mDistX;
325        final float newDist = getXDistance(motionEvent);
326        // sign change! Fingers have crossed ;-)
327        if(oldDist > 0 && newDist < 0 || oldDist < 0 && newDist > 0) {
328            return;
329        }
330        mDistX = newDist;
331        float scale = (oldDist / mDistX);
332        // sanity check
333        if(Float.isInfinite(scale) || Float.isNaN(scale) || scale > -0.001 && scale < 0.001) {
334            return;
335        }
336        PointF newX = new PointF();
337        if(mZoomHorizontally) {
338            calculateZoom(scale, newX, true);
339            mCalledBySelf = true;
340            super.setDomainBoundaries(newX.x, newX.y, BoundaryMode.FIXED);
341            lastMinX = newX.x;
342            lastMaxX = newX.y;
343        }
344        if(mZoomVertically) {
345            calculateZoom(scale, newX, false);
346            mCalledBySelf = true;
347            super.setRangeBoundaries(newX.x, newX.y, BoundaryMode.FIXED);
348            lastMinY = newX.x;
349            lastMaxY = newX.y;
350        }
351        redraw();
352    }
353
354    private void calculateZoom(float scale, PointF newX, final boolean horizontal) {
355        final float calcMax;
356        final float span;
357        if(horizontal) {
358            calcMax = getLastMaxX();
359            span = calcMax - getLastMinX();
360        } else {
361            calcMax = getLastMaxY();
362            span = calcMax - getLastMinY();
363        }
364        final float midPoint = calcMax - (span / 2.0f);
365        final float offset = span * scale / 2.0f;
366        newX.x = midPoint - offset;
367        newX.y = midPoint + offset;
368        if(horizontal) {
369            if(newX.x < getMinXLimit()) {
370                newX.x = getMinXLimit();
371            }
372            if(newX.y > getMaxXLimit()) {
373                newX.y = getMaxXLimit();
374            }
375        } else {
376            if(newX.x < getMinYLimit()) {
377                newX.x = getMinYLimit();
378            }
379            if(newX.y > getMaxYLimit()) {
380                newX.y = getMaxYLimit();
381            }
382        }
383    }
384}
385