1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
|
/*
* Copyright 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.interactivechart;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.os.ParcelableCompat;
import android.support.v4.os.ParcelableCompatCreatorCallbacks;
import android.support.v4.view.GestureDetectorCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.EdgeEffectCompat;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.OverScroller;
/**
* A view representing a simple yet interactive line chart for the function <code>x^3 - x/4</code>.
* <p>
* This view isn't all that useful on its own; rather it serves as an example of how to correctly
* implement these types of gestures to perform zooming and scrolling with interesting content
* types.
* <p>
* The view is interactive in that it can be zoomed and panned using
* typical <a href="http://developer.android.com/design/patterns/gestures.html">gestures</a> such
* as double-touch, drag, pinch-open, and pinch-close. This is done using the
* {@link ScaleGestureDetector}, {@link GestureDetector}, and {@link OverScroller} classes. Note
* that the platform-provided view scrolling behavior (e.g. {@link View#scrollBy(int, int)} is NOT
* used.
* <p>
* The view also demonstrates the correct use of
* <a href="http://developer.android.com/design/style/touch-feedback.html">touch feedback</a> to
* indicate to users that they've reached the content edges after a pan or fling gesture. This
* is done using the {@link EdgeEffectCompat} class.
* <p>
* Finally, this class demonstrates the basics of creating a custom view, including support for
* custom attributes (see the constructors), a simple implementation for
* {@link #onMeasure(int, int)}, an implementation for {@link #onSaveInstanceState()} and a fairly
* straightforward {@link Canvas}-based rendering implementation in
* {@link #onDraw(android.graphics.Canvas)}.
* <p>
* Note that this view doesn't automatically support directional navigation or other accessibility
* methods. Activities using this view should generally provide alternate navigation controls.
* Activities using this view should also present an alternate, text-based representation of this
* view's content for vision-impaired users.
*/
public class InteractiveLineGraphView extends View {
private static final String TAG = "InteractiveLineGraphView";
/**
* The number of individual points (samples) in the chart series to draw onscreen.
*/
private static final int DRAW_STEPS = 30;
/**
* Initial fling velocity for pan operations, in screen widths (or heights) per second.
*
* @see #panLeft()
* @see #panRight()
* @see #panUp()
* @see #panDown()
*/
private static final float PAN_VELOCITY_FACTOR = 2f;
/**
* The scaling factor for a single zoom 'step'.
*
* @see #zoomIn()
* @see #zoomOut()
*/
private static final float ZOOM_AMOUNT = 0.25f;
// Viewport extremes. See mCurrentViewport for a discussion of the viewport.
private static final float AXIS_X_MIN = -1f;
private static final float AXIS_X_MAX = 1f;
private static final float AXIS_Y_MIN = -1f;
private static final float AXIS_Y_MAX = 1f;
/**
* The current viewport. This rectangle represents the currently visible chart domain
* and range. The currently visible chart X values are from this rectangle's left to its right.
* The currently visible chart Y values are from this rectangle's top to its bottom.
* <p>
* Note that this rectangle's top is actually the smaller Y value, and its bottom is the larger
* Y value. Since the chart is drawn onscreen in such a way that chart Y values increase
* towards the top of the screen (decreasing pixel Y positions), this rectangle's "top" is drawn
* above this rectangle's "bottom" value.
*
* @see #mContentRect
*/
private RectF mCurrentViewport = new RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX);
/**
* The current destination rectangle (in pixel coordinates) into which the chart data should
* be drawn. Chart labels are drawn outside this area.
*
* @see #mCurrentViewport
*/
private Rect mContentRect = new Rect();
// Current attribute values and Paints.
private float mLabelTextSize;
private int mLabelSeparation;
private int mLabelTextColor;
private Paint mLabelTextPaint;
private int mMaxLabelWidth;
private int mLabelHeight;
private float mGridThickness;
private int mGridColor;
private Paint mGridPaint;
private float mAxisThickness;
private int mAxisColor;
private Paint mAxisPaint;
private float mDataThickness;
private int mDataColor;
private Paint mDataPaint;
// State objects and values related to gesture tracking.
private ScaleGestureDetector mScaleGestureDetector;
private GestureDetectorCompat mGestureDetector;
private OverScroller mScroller;
private Zoomer mZoomer;
private PointF mZoomFocalPoint = new PointF();
private RectF mScrollerStartViewport = new RectF(); // Used only for zooms and flings.
// Edge effect / overscroll tracking objects.
private EdgeEffectCompat mEdgeEffectTop;
private EdgeEffectCompat mEdgeEffectBottom;
private EdgeEffectCompat mEdgeEffectLeft;
private EdgeEffectCompat mEdgeEffectRight;
private boolean mEdgeEffectTopActive;
private boolean mEdgeEffectBottomActive;
private boolean mEdgeEffectLeftActive;
private boolean mEdgeEffectRightActive;
// Buffers for storing current X and Y stops. See the computeAxisStops method for more details.
private final AxisStops mXStopsBuffer = new AxisStops();
private final AxisStops mYStopsBuffer = new AxisStops();
// Buffers used during drawing. These are defined as fields to avoid allocation during
// draw calls.
private float[] mAxisXPositionsBuffer = new float[]{};
private float[] mAxisYPositionsBuffer = new float[]{};
private float[] mAxisXLinesBuffer = new float[]{};
private float[] mAxisYLinesBuffer = new float[]{};
private float[] mSeriesLinesBuffer = new float[(DRAW_STEPS + 1) * 4];
private final char[] mLabelBuffer = new char[100];
private Point mSurfaceSizeBuffer = new Point();
/**
* The simple math function Y = fun(X) to draw on the chart.
* @param x The X value
* @return The Y value
*/
protected static float fun(float x) {
return (float) Math.pow(x, 3) - x / 4;
}
public InteractiveLineGraphView(Context context) {
this(context, null, 0);
}
public InteractiveLineGraphView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public InteractiveLineGraphView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs, R.styleable.InteractiveLineGraphView, defStyle, defStyle);
try {
mLabelTextColor = a.getColor(
R.styleable.InteractiveLineGraphView_labelTextColor, mLabelTextColor);
mLabelTextSize = a.getDimension(
R.styleable.InteractiveLineGraphView_labelTextSize, mLabelTextSize);
mLabelSeparation = a.getDimensionPixelSize(
R.styleable.InteractiveLineGraphView_labelSeparation, mLabelSeparation);
mGridThickness = a.getDimension(
R.styleable.InteractiveLineGraphView_gridThickness, mGridThickness);
mGridColor = a.getColor(
R.styleable.InteractiveLineGraphView_gridColor, mGridColor);
mAxisThickness = a.getDimension(
R.styleable.InteractiveLineGraphView_axisThickness, mAxisThickness);
mAxisColor = a.getColor(
R.styleable.InteractiveLineGraphView_axisColor, mAxisColor);
mDataThickness = a.getDimension(
R.styleable.InteractiveLineGraphView_dataThickness, mDataThickness);
mDataColor = a.getColor(
R.styleable.InteractiveLineGraphView_dataColor, mDataColor);
} finally {
a.recycle();
}
initPaints();
// Sets up interactions
mScaleGestureDetector = new ScaleGestureDetector(context, mScaleGestureListener);
mGestureDetector = new GestureDetectorCompat(context, mGestureListener);
mScroller = new OverScroller(context);
mZoomer = new Zoomer(context);
// Sets up edge effects
mEdgeEffectLeft = new EdgeEffectCompat(context);
mEdgeEffectTop = new EdgeEffectCompat(context);
mEdgeEffectRight = new EdgeEffectCompat(context);
mEdgeEffectBottom = new EdgeEffectCompat(context);
}
/**
* (Re)initializes {@link Paint} objects based on current attribute values.
*/
private void initPaints() {
mLabelTextPaint = new Paint();
mLabelTextPaint.setAntiAlias(true);
mLabelTextPaint.setTextSize(mLabelTextSize);
mLabelTextPaint.setColor(mLabelTextColor);
mLabelHeight = (int) Math.abs(mLabelTextPaint.getFontMetrics().top);
mMaxLabelWidth = (int) mLabelTextPaint.measureText("0000");
mGridPaint = new Paint();
mGridPaint.setStrokeWidth(mGridThickness);
mGridPaint.setColor(mGridColor);
mGridPaint.setStyle(Paint.Style.STROKE);
mAxisPaint = new Paint();
mAxisPaint.setStrokeWidth(mAxisThickness);
mAxisPaint.setColor(mAxisColor);
mAxisPaint.setStyle(Paint.Style.STROKE);
mDataPaint = new Paint();
mDataPaint.setStrokeWidth(mDataThickness);
mDataPaint.setColor(mDataColor);
mDataPaint.setStyle(Paint.Style.STROKE);
mDataPaint.setAntiAlias(true);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mContentRect.set(
getPaddingLeft() + mMaxLabelWidth + mLabelSeparation,
getPaddingTop(),
getWidth() - getPaddingRight(),
getHeight() - getPaddingBottom() - mLabelHeight - mLabelSeparation);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int minChartSize = getResources().getDimensionPixelSize(R.dimen.min_chart_size);
setMeasuredDimension(
Math.max(getSuggestedMinimumWidth(),
resolveSize(minChartSize + getPaddingLeft() + mMaxLabelWidth
+ mLabelSeparation + getPaddingRight(),
widthMeasureSpec)),
Math.max(getSuggestedMinimumHeight(),
resolveSize(minChartSize + getPaddingTop() + mLabelHeight
+ mLabelSeparation + getPaddingBottom(),
heightMeasureSpec)));
}
////////////////////////////////////////////////////////////////////////////////////////////////
//
// Methods and objects related to drawing
//
////////////////////////////////////////////////////////////////////////////////////////////////
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draws axes and text labels
drawAxes(canvas);
// Clips the next few drawing operations to the content area
int clipRestoreCount = canvas.save();
canvas.clipRect(mContentRect);
drawDataSeriesUnclipped(canvas);
drawEdgeEffectsUnclipped(canvas);
// Removes clipping rectangle
canvas.restoreToCount(clipRestoreCount);
// Draws chart container
canvas.drawRect(mContentRect, mAxisPaint);
}
/**
* Draws the chart axes and labels onto the canvas.
*/
private void drawAxes(Canvas canvas) {
// Computes axis stops (in terms of numerical value and position on screen)
int i;
computeAxisStops(
mCurrentViewport.left,
mCurrentViewport.right,
mContentRect.width() / mMaxLabelWidth / 2,
mXStopsBuffer);
computeAxisStops(
mCurrentViewport.top,
mCurrentViewport.bottom,
mContentRect.height() / mLabelHeight / 2,
mYStopsBuffer);
// Avoid unnecessary allocations during drawing. Re-use allocated
// arrays and only reallocate if the number of stops grows.
if (mAxisXPositionsBuffer.length < mXStopsBuffer.numStops) {
mAxisXPositionsBuffer = new float[mXStopsBuffer.numStops];
}
if (mAxisYPositionsBuffer.length < mYStopsBuffer.numStops) {
mAxisYPositionsBuffer = new float[mYStopsBuffer.numStops];
}
if (mAxisXLinesBuffer.length < mXStopsBuffer.numStops * 4) {
mAxisXLinesBuffer = new float[mXStopsBuffer.numStops * 4];
}
if (mAxisYLinesBuffer.length < mYStopsBuffer.numStops * 4) {
mAxisYLinesBuffer = new float[mYStopsBuffer.numStops * 4];
}
// Compute positions
for (i = 0; i < mXStopsBuffer.numStops; i++) {
mAxisXPositionsBuffer[i] = getDrawX(mXStopsBuffer.stops[i]);
}
for (i = 0; i < mYStopsBuffer.numStops; i++) {
mAxisYPositionsBuffer[i] = getDrawY(mYStopsBuffer.stops[i]);
}
// Draws grid lines using drawLines (faster than individual drawLine calls)
for (i = 0; i < mXStopsBuffer.numStops; i++) {
mAxisXLinesBuffer[i * 4 + 0] = (float) Math.floor(mAxisXPositionsBuffer[i]);
mAxisXLinesBuffer[i * 4 + 1] = mContentRect.top;
mAxisXLinesBuffer[i * 4 + 2] = (float) Math.floor(mAxisXPositionsBuffer[i]);
mAxisXLinesBuffer[i * 4 + 3] = mContentRect.bottom;
}
canvas.drawLines(mAxisXLinesBuffer, 0, mXStopsBuffer.numStops * 4, mGridPaint);
for (i = 0; i < mYStopsBuffer.numStops; i++) {
mAxisYLinesBuffer[i * 4 + 0] = mContentRect.left;
mAxisYLinesBuffer[i * 4 + 1] = (float) Math.floor(mAxisYPositionsBuffer[i]);
mAxisYLinesBuffer[i * 4 + 2] = mContentRect.right;
mAxisYLinesBuffer[i * 4 + 3] = (float) Math.floor(mAxisYPositionsBuffer[i]);
}
canvas.drawLines(mAxisYLinesBuffer, 0, mYStopsBuffer.numStops * 4, mGridPaint);
// Draws X labels
int labelOffset;
int labelLength;
mLabelTextPaint.setTextAlign(Paint.Align.CENTER);
for (i = 0; i < mXStopsBuffer.numStops; i++) {
// Do not use String.format in high-performance code such as onDraw code.
labelLength = formatFloat(mLabelBuffer, mXStopsBuffer.stops[i], mXStopsBuffer.decimals);
labelOffset = mLabelBuffer.length - labelLength;
canvas.drawText(
mLabelBuffer, labelOffset, labelLength,
mAxisXPositionsBuffer[i],
mContentRect.bottom + mLabelHeight + mLabelSeparation,
mLabelTextPaint);
}
// Draws Y labels
mLabelTextPaint.setTextAlign(Paint.Align.RIGHT);
for (i = 0; i < mYStopsBuffer.numStops; i++) {
// Do not use String.format in high-performance code such as onDraw code.
labelLength = formatFloat(mLabelBuffer, mYStopsBuffer.stops[i], mYStopsBuffer.decimals);
labelOffset = mLabelBuffer.length - labelLength;
canvas.drawText(
mLabelBuffer, labelOffset, labelLength,
mContentRect.left - mLabelSeparation,
mAxisYPositionsBuffer[i] + mLabelHeight / 2,
mLabelTextPaint);
}
}
/**
* Rounds the given number to the given number of significant digits. Based on an answer on
* <a href="http://stackoverflow.com/questions/202302">Stack Overflow</a>.
*/
private static float roundToOneSignificantFigure(double num) {
final float d = (float) Math.ceil((float) Math.log10(num < 0 ? -num : num));
final int power = 1 - (int) d;
final float magnitude = (float) Math.pow(10, power);
final long shifted = Math.round(num * magnitude);
return shifted / magnitude;
}
private static final int POW10[] = {1, 10, 100, 1000, 10000, 100000, 1000000};
/**
* Formats a float value to the given number of decimals. Returns the length of the string.
* The string begins at out.length - [return value].
*/
private static int formatFloat(final char[] out, float val, int digits) {
boolean negative = false;
if (val == 0) {
out[out.length - 1] = '0';
return 1;
}
if (val < 0) {
negative = true;
val = -val;
}
if (digits > POW10.length) {
digits = POW10.length - 1;
}
val *= POW10[digits];
long lval = Math.round(val);
int index = out.length - 1;
int charCount = 0;
while (lval != 0 || charCount < (digits + 1)) {
int digit = (int) (lval % 10);
lval = lval / 10;
out[index--] = (char) (digit + '0');
charCount++;
if (charCount == digits) {
out[index--] = '.';
charCount++;
}
}
if (negative) {
out[index--] = '-';
charCount++;
}
return charCount;
}
/**
* Computes the set of axis labels to show given start and stop boundaries and an ideal number
* of stops between these boundaries.
*
* @param start The minimum extreme (e.g. the left edge) for the axis.
* @param stop The maximum extreme (e.g. the right edge) for the axis.
* @param steps The ideal number of stops to create. This should be based on available screen
* space; the more space there is, the more stops should be shown.
* @param outStops The destination {@link AxisStops} object to populate.
*/
private static void computeAxisStops(float start, float stop, int steps, AxisStops outStops) {
double range = stop - start;
if (steps == 0 || range <= 0) {
outStops.stops = new float[]{};
outStops.numStops = 0;
return;
}
double rawInterval = range / steps;
double interval = roundToOneSignificantFigure(rawInterval);
double intervalMagnitude = Math.pow(10, (int) Math.log10(interval));
int intervalSigDigit = (int) (interval / intervalMagnitude);
if (intervalSigDigit > 5) {
// Use one order of magnitude higher, to avoid intervals like 0.9 or 90
interval = Math.floor(10 * intervalMagnitude);
}
double first = Math.ceil(start / interval) * interval;
double last = Math.nextUp(Math.floor(stop / interval) * interval);
double f;
int i;
int n = 0;
for (f = first; f <= last; f += interval) {
++n;
}
outStops.numStops = n;
if (outStops.stops.length < n) {
// Ensure stops contains at least numStops elements.
outStops.stops = new float[n];
}
for (f = first, i = 0; i < n; f += interval, ++i) {
outStops.stops[i] = (float) f;
}
if (interval < 1) {
outStops.decimals = (int) Math.ceil(-Math.log10(interval));
} else {
outStops.decimals = 0;
}
}
/**
* Computes the pixel offset for the given X chart value. This may be outside the view bounds.
*/
private float getDrawX(float x) {
return mContentRect.left
+ mContentRect.width()
* (x - mCurrentViewport.left) / mCurrentViewport.width();
}
/**
* Computes the pixel offset for the given Y chart value. This may be outside the view bounds.
*/
private float getDrawY(float y) {
return mContentRect.bottom
- mContentRect.height()
* (y - mCurrentViewport.top) / mCurrentViewport.height();
}
/**
* Draws the currently visible portion of the data series defined by {@link #fun(float)} to the
* canvas. This method does not clip its drawing, so users should call {@link Canvas#clipRect
* before calling this method.
*/
private void drawDataSeriesUnclipped(Canvas canvas) {
mSeriesLinesBuffer[0] = mContentRect.left;
mSeriesLinesBuffer[1] = getDrawY(fun(mCurrentViewport.left));
mSeriesLinesBuffer[2] = mSeriesLinesBuffer[0];
mSeriesLinesBuffer[3] = mSeriesLinesBuffer[1];
float x;
for (int i = 1; i <= DRAW_STEPS; i++) {
mSeriesLinesBuffer[i * 4 + 0] = mSeriesLinesBuffer[(i - 1) * 4 + 2];
mSeriesLinesBuffer[i * 4 + 1] = mSeriesLinesBuffer[(i - 1) * 4 + 3];
x = (mCurrentViewport.left + (mCurrentViewport.width() / DRAW_STEPS * i));
mSeriesLinesBuffer[i * 4 + 2] = getDrawX(x);
mSeriesLinesBuffer[i * 4 + 3] = getDrawY(fun(x));
}
canvas.drawLines(mSeriesLinesBuffer, mDataPaint);
}
/**
* Draws the overscroll "glow" at the four edges of the chart region, if necessary. The edges
* of the chart region are stored in {@link #mContentRect}.
*
* @see EdgeEffectCompat
*/
private void drawEdgeEffectsUnclipped(Canvas canvas) {
// The methods below rotate and translate the canvas as needed before drawing the glow,
// since EdgeEffectCompat always draws a top-glow at 0,0.
boolean needsInvalidate = false;
if (!mEdgeEffectTop.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mContentRect.left, mContentRect.top);
mEdgeEffectTop.setSize(mContentRect.width(), mContentRect.height());
if (mEdgeEffectTop.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeEffectBottom.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(2 * mContentRect.left - mContentRect.right, mContentRect.bottom);
canvas.rotate(180, mContentRect.width(), 0);
mEdgeEffectBottom.setSize(mContentRect.width(), mContentRect.height());
if (mEdgeEffectBottom.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeEffectLeft.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mContentRect.left, mContentRect.bottom);
canvas.rotate(-90, 0, 0);
mEdgeEffectLeft.setSize(mContentRect.height(), mContentRect.width());
if (mEdgeEffectLeft.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (!mEdgeEffectRight.isFinished()) {
final int restoreCount = canvas.save();
canvas.translate(mContentRect.right, mContentRect.top);
canvas.rotate(90, 0, 0);
mEdgeEffectRight.setSize(mContentRect.height(), mContentRect.width());
if (mEdgeEffectRight.draw(canvas)) {
needsInvalidate = true;
}
canvas.restoreToCount(restoreCount);
}
if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//
// Methods and objects related to gesture handling
//
////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Finds the chart point (i.e. within the chart's domain and range) represented by the
* given pixel coordinates, if that pixel is within the chart region described by
* {@link #mContentRect}. If the point is found, the "dest" argument is set to the point and
* this function returns true. Otherwise, this function returns false and "dest" is unchanged.
*/
private boolean hitTest(float x, float y, PointF dest) {
if (!mContentRect.contains((int) x, (int) y)) {
return false;
}
dest.set(
mCurrentViewport.left
+ mCurrentViewport.width()
* (x - mContentRect.left) / mContentRect.width(),
mCurrentViewport.top
+ mCurrentViewport.height()
* (y - mContentRect.bottom) / -mContentRect.height());
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean retVal = mScaleGestureDetector.onTouchEvent(event);
retVal = mGestureDetector.onTouchEvent(event) || retVal;
return retVal || super.onTouchEvent(event);
}
/**
* The scale listener, used for handling multi-finger scale gestures.
*/
private final ScaleGestureDetector.OnScaleGestureListener mScaleGestureListener
= new ScaleGestureDetector.SimpleOnScaleGestureListener() {
/**
* This is the active focal point in terms of the viewport. Could be a local
* variable but kept here to minimize per-frame allocations.
*/
private PointF viewportFocus = new PointF();
private float lastSpanX;
private float lastSpanY;
@Override
public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) {
lastSpanX = ScaleGestureDetectorCompat.getCurrentSpanX(scaleGestureDetector);
lastSpanY = ScaleGestureDetectorCompat.getCurrentSpanY(scaleGestureDetector);
return true;
}
@Override
public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
float spanX = ScaleGestureDetectorCompat.getCurrentSpanX(scaleGestureDetector);
float spanY = ScaleGestureDetectorCompat.getCurrentSpanY(scaleGestureDetector);
float newWidth = lastSpanX / spanX * mCurrentViewport.width();
float newHeight = lastSpanY / spanY * mCurrentViewport.height();
float focusX = scaleGestureDetector.getFocusX();
float focusY = scaleGestureDetector.getFocusY();
hitTest(focusX, focusY, viewportFocus);
mCurrentViewport.set(
viewportFocus.x
- newWidth * (focusX - mContentRect.left)
/ mContentRect.width(),
viewportFocus.y
- newHeight * (mContentRect.bottom - focusY)
/ mContentRect.height(),
0,
0);
mCurrentViewport.right = mCurrentViewport.left + newWidth;
mCurrentViewport.bottom = mCurrentViewport.top + newHeight;
constrainViewport();
ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this);
lastSpanX = spanX;
lastSpanY = spanY;
return true;
}
};
/**
* Ensures that current viewport is inside the viewport extremes defined by {@link #AXIS_X_MIN},
* {@link #AXIS_X_MAX}, {@link #AXIS_Y_MIN} and {@link #AXIS_Y_MAX}.
*/
private void constrainViewport() {
mCurrentViewport.left = Math.max(AXIS_X_MIN, mCurrentViewport.left);
mCurrentViewport.top = Math.max(AXIS_Y_MIN, mCurrentViewport.top);
mCurrentViewport.bottom = Math.max(Math.nextUp(mCurrentViewport.top),
Math.min(AXIS_Y_MAX, mCurrentViewport.bottom));
mCurrentViewport.right = Math.max(Math.nextUp(mCurrentViewport.left),
Math.min(AXIS_X_MAX, mCurrentViewport.right));
}
/**
* The gesture listener, used for handling simple gestures such as double touches, scrolls,
* and flings.
*/
private final GestureDetector.SimpleOnGestureListener mGestureListener
= new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
releaseEdgeEffects();
mScrollerStartViewport.set(mCurrentViewport);
mScroller.forceFinished(true);
ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this);
return true;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
mZoomer.forceFinished(true);
if (hitTest(e.getX(), e.getY(), mZoomFocalPoint)) {
mZoomer.startZoom(ZOOM_AMOUNT);
}
ViewCompat.postInvalidateOnAnimation(InteractiveLineGraphView.this);
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// Scrolling uses math based on the viewport (as opposed to math using pixels).
/**
* Pixel offset is the offset in screen pixels, while viewport offset is the
* offset within the current viewport. For additional information on surface sizes
* and pixel offsets, see the docs for {@link computeScrollSurfaceSize()}. For
* additional information about the viewport, see the comments for
* {@link mCurrentViewport}.
*/
float viewportOffsetX = distanceX * mCurrentViewport.width() / mContentRect.width();
float viewportOffsetY = -distanceY * mCurrentViewport.height() / mContentRect.height();
computeScrollSurfaceSize(mSurfaceSizeBuffer);
int scrolledX = (int) (mSurfaceSizeBuffer.x
* (mCurrentViewport.left + viewportOffsetX - AXIS_X_MIN)
/ (AXIS_X_MAX - AXIS_X_MIN));
int scrolledY = (int) (mSurfaceSizeBuffer.y
* (AXIS_Y_MAX - mCurrentViewport.bottom - viewportOffsetY)
/ (AXIS_Y_MAX - AXIS_Y_MIN));
boolean canScrollX = mCurrentViewport.left > AXIS_X_MIN
|| mCurrentViewport.right < AXIS_X_MAX;
boolean canScrollY = mCurrentViewport.top > AXIS_Y_MIN
|| mCurrentViewport.bottom < AXIS_Y_MAX;
setViewportBottomLeft(
mCurrentViewport.left + viewportOffsetX,
mCurrentViewport.bottom + viewportOffsetY);
if (canScrollX && scrolledX < 0) {
mEdgeEffectLeft.onPull(scrolledX / (float) mContentRect.width());
mEdgeEffectLeftActive = true;
}
if (canScrollY && scrolledY < 0) {
mEdgeEffectTop.onPull(scrolledY / (float) mContentRect.height());
mEdgeEffectTopActive = true;
}
if (canScrollX && scrolledX > mSurfaceSizeBuffer.x - mContentRect.width()) {
mEdgeEffectRight.onPull((scrolledX - mSurfaceSizeBuffer.x + mContentRect.width())
/ (float) mContentRect.width());
mEdgeEffectRightActive = true;
}
if (canScrollY && scrolledY > mSurfaceSizeBuffer.y - mContentRect.height()) {
mEdgeEffectBottom.onPull((scrolledY - mSurfaceSizeBuffer.y + mContentRect.height())
/ (float) mContentRect.height());
mEdgeEffectBottomActive = true;
}
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
fling((int) -velocityX, (int) -velocityY);
return true;
}
};
private void releaseEdgeEffects() {
mEdgeEffectLeftActive
= mEdgeEffectTopActive
= mEdgeEffectRightActive
= mEdgeEffectBottomActive
= false;
mEdgeEffectLeft.onRelease();
mEdgeEffectTop.onRelease();
mEdgeEffectRight.onRelease();
mEdgeEffectBottom.onRelease();
}
private void fling(int velocityX, int velocityY) {
releaseEdgeEffects();
// Flings use math in pixels (as opposed to math based on the viewport).
computeScrollSurfaceSize(mSurfaceSizeBuffer);
mScrollerStartViewport.set(mCurrentViewport);
int startX = (int) (mSurfaceSizeBuffer.x * (mScrollerStartViewport.left - AXIS_X_MIN) / (
AXIS_X_MAX - AXIS_X_MIN));
int startY = (int) (mSurfaceSizeBuffer.y * (AXIS_Y_MAX - mScrollerStartViewport.bottom) / (
AXIS_Y_MAX - AXIS_Y_MIN));
mScroller.forceFinished(true);
mScroller.fling(
startX,
startY,
velocityX,
velocityY,
0, mSurfaceSizeBuffer.x - mContentRect.width(),
0, mSurfaceSizeBuffer.y - mContentRect.height(),
mContentRect.width() / 2,
mContentRect.height() / 2);
ViewCompat.postInvalidateOnAnimation(this);
}
/**
* Computes the current scrollable surface size, in pixels. For example, if the entire chart
* area is visible, this is simply the current size of {@link #mContentRect}. If the chart
* is zoomed in 200% in both directions, the returned size will be twice as large horizontally
* and vertically.
*/
private void computeScrollSurfaceSize(Point out) {
out.set(
(int) (mContentRect.width() * (AXIS_X_MAX - AXIS_X_MIN)
/ mCurrentViewport.width()),
(int) (mContentRect.height() * (AXIS_Y_MAX - AXIS_Y_MIN)
/ mCurrentViewport.height()));
}
@Override
public void computeScroll() {
super.computeScroll();
boolean needsInvalidate = false;
if (mScroller.computeScrollOffset()) {
// The scroller isn't finished, meaning a fling or programmatic pan operation is
// currently active.
computeScrollSurfaceSize(mSurfaceSizeBuffer);
int currX = mScroller.getCurrX();
int currY = mScroller.getCurrY();
boolean canScrollX = (mCurrentViewport.left > AXIS_X_MIN
|| mCurrentViewport.right < AXIS_X_MAX);
boolean canScrollY = (mCurrentViewport.top > AXIS_Y_MIN
|| mCurrentViewport.bottom < AXIS_Y_MAX);
if (canScrollX
&& currX < 0
&& mEdgeEffectLeft.isFinished()
&& !mEdgeEffectLeftActive) {
mEdgeEffectLeft.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller));
mEdgeEffectLeftActive = true;
needsInvalidate = true;
} else if (canScrollX
&& currX > (mSurfaceSizeBuffer.x - mContentRect.width())
&& mEdgeEffectRight.isFinished()
&& !mEdgeEffectRightActive) {
mEdgeEffectRight.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller));
mEdgeEffectRightActive = true;
needsInvalidate = true;
}
if (canScrollY
&& currY < 0
&& mEdgeEffectTop.isFinished()
&& !mEdgeEffectTopActive) {
mEdgeEffectTop.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller));
mEdgeEffectTopActive = true;
needsInvalidate = true;
} else if (canScrollY
&& currY > (mSurfaceSizeBuffer.y - mContentRect.height())
&& mEdgeEffectBottom.isFinished()
&& !mEdgeEffectBottomActive) {
mEdgeEffectBottom.onAbsorb((int) OverScrollerCompat.getCurrVelocity(mScroller));
mEdgeEffectBottomActive = true;
needsInvalidate = true;
}
float currXRange = AXIS_X_MIN + (AXIS_X_MAX - AXIS_X_MIN)
* currX / mSurfaceSizeBuffer.x;
float currYRange = AXIS_Y_MAX - (AXIS_Y_MAX - AXIS_Y_MIN)
* currY / mSurfaceSizeBuffer.y;
setViewportBottomLeft(currXRange, currYRange);
}
if (mZoomer.computeZoom()) {
// Performs the zoom since a zoom is in progress (either programmatically or via
// double-touch).
float newWidth = (1f - mZoomer.getCurrZoom()) * mScrollerStartViewport.width();
float newHeight = (1f - mZoomer.getCurrZoom()) * mScrollerStartViewport.height();
float pointWithinViewportX = (mZoomFocalPoint.x - mScrollerStartViewport.left)
/ mScrollerStartViewport.width();
float pointWithinViewportY = (mZoomFocalPoint.y - mScrollerStartViewport.top)
/ mScrollerStartViewport.height();
mCurrentViewport.set(
mZoomFocalPoint.x - newWidth * pointWithinViewportX,
mZoomFocalPoint.y - newHeight * pointWithinViewportY,
mZoomFocalPoint.x + newWidth * (1 - pointWithinViewportX),
mZoomFocalPoint.y + newHeight * (1 - pointWithinViewportY));
constrainViewport();
needsInvalidate = true;
}
if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
/**
* Sets the current viewport (defined by {@link #mCurrentViewport}) to the given
* X and Y positions. Note that the Y value represents the topmost pixel position, and thus
* the bottom of the {@link #mCurrentViewport} rectangle. For more details on why top and
* bottom are flipped, see {@link #mCurrentViewport}.
*/
private void setViewportBottomLeft(float x, float y) {
/**
* Constrains within the scroll range. The scroll range is simply the viewport extremes
* (AXIS_X_MAX, etc.) minus the viewport size. For example, if the extrema were 0 and 10,
* and the viewport size was 2, the scroll range would be 0 to 8.
*/
float curWidth = mCurrentViewport.width();
float curHeight = mCurrentViewport.height();
x = Math.max(AXIS_X_MIN, Math.min(x, AXIS_X_MAX - curWidth));
y = Math.max(AXIS_Y_MIN + curHeight, Math.min(y, AXIS_Y_MAX));
mCurrentViewport.set(x, y - curHeight, x + curWidth, y);
ViewCompat.postInvalidateOnAnimation(this);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//
// Methods for programmatically changing the viewport
//
////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Returns the current viewport (visible extremes for the chart domain and range.)
*/
public RectF getCurrentViewport() {
return new RectF(mCurrentViewport);
}
/**
* Sets the chart's current viewport.
*
* @see #getCurrentViewport()
*/
public void setCurrentViewport(RectF viewport) {
mCurrentViewport = viewport;
constrainViewport();
ViewCompat.postInvalidateOnAnimation(this);
}
/**
* Smoothly zooms the chart in one step.
*/
public void zoomIn() {
mScrollerStartViewport.set(mCurrentViewport);
mZoomer.forceFinished(true);
mZoomer.startZoom(ZOOM_AMOUNT);
mZoomFocalPoint.set(
(mCurrentViewport.right + mCurrentViewport.left) / 2,
(mCurrentViewport.bottom + mCurrentViewport.top) / 2);
ViewCompat.postInvalidateOnAnimation(this);
}
/**
* Smoothly zooms the chart out one step.
*/
public void zoomOut() {
mScrollerStartViewport.set(mCurrentViewport);
mZoomer.forceFinished(true);
mZoomer.startZoom(-ZOOM_AMOUNT);
mZoomFocalPoint.set(
(mCurrentViewport.right + mCurrentViewport.left) / 2,
(mCurrentViewport.bottom + mCurrentViewport.top) / 2);
ViewCompat.postInvalidateOnAnimation(this);
}
/**
* Smoothly pans the chart left one step.
*/
public void panLeft() {
fling((int) (-PAN_VELOCITY_FACTOR * getWidth()), 0);
}
/**
* Smoothly pans the chart right one step.
*/
public void panRight() {
fling((int) (PAN_VELOCITY_FACTOR * getWidth()), 0);
}
/**
* Smoothly pans the chart up one step.
*/
public void panUp() {
fling(0, (int) (-PAN_VELOCITY_FACTOR * getHeight()));
}
/**
* Smoothly pans the chart down one step.
*/
public void panDown() {
fling(0, (int) (PAN_VELOCITY_FACTOR * getHeight()));
}
////////////////////////////////////////////////////////////////////////////////////////////////
//
// Methods related to custom attributes
//
////////////////////////////////////////////////////////////////////////////////////////////////
public float getLabelTextSize() {
return mLabelTextSize;
}
public void setLabelTextSize(float labelTextSize) {
mLabelTextSize = labelTextSize;
initPaints();
ViewCompat.postInvalidateOnAnimation(this);
}
public int getLabelTextColor() {
return mLabelTextColor;
}
public void setLabelTextColor(int labelTextColor) {
mLabelTextColor = labelTextColor;
initPaints();
ViewCompat.postInvalidateOnAnimation(this);
}
public float getGridThickness() {
return mGridThickness;
}
public void setGridThickness(float gridThickness) {
mGridThickness = gridThickness;
initPaints();
ViewCompat.postInvalidateOnAnimation(this);
}
public int getGridColor() {
return mGridColor;
}
public void setGridColor(int gridColor) {
mGridColor = gridColor;
initPaints();
ViewCompat.postInvalidateOnAnimation(this);
}
public float getAxisThickness() {
return mAxisThickness;
}
public void setAxisThickness(float axisThickness) {
mAxisThickness = axisThickness;
initPaints();
ViewCompat.postInvalidateOnAnimation(this);
}
public int getAxisColor() {
return mAxisColor;
}
public void setAxisColor(int axisColor) {
mAxisColor = axisColor;
initPaints();
ViewCompat.postInvalidateOnAnimation(this);
}
public float getDataThickness() {
return mDataThickness;
}
public void setDataThickness(float dataThickness) {
mDataThickness = dataThickness;
}
public int getDataColor() {
return mDataColor;
}
public void setDataColor(int dataColor) {
mDataColor = dataColor;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//
// Methods and classes related to view state persistence.
//
////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.viewport = mCurrentViewport;
return ss;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
mCurrentViewport = ss.viewport;
}
/**
* Persistent state that is saved by InteractiveLineGraphView.
*/
public static class SavedState extends BaseSavedState {
private RectF viewport;
public SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeFloat(viewport.left);
out.writeFloat(viewport.top);
out.writeFloat(viewport.right);
out.writeFloat(viewport.bottom);
}
@Override
public String toString() {
return "InteractiveLineGraphView.SavedState{"
+ Integer.toHexString(System.identityHashCode(this))
+ " viewport=" + viewport.toString() + "}";
}
public static final Parcelable.Creator<SavedState> CREATOR
= ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in, ClassLoader loader) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
});
SavedState(Parcel in) {
super(in);
viewport = new RectF(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat());
}
}
/**
* A simple class representing axis label values.
*
* @see #computeAxisStops
*/
private static class AxisStops {
float[] stops = new float[]{};
int numStops;
int decimals;
}
}
|