Skip to content
This repository was archived by the owner on Jul 31, 2019. It is now read-only.

Commit dd70827

Browse files
committed
Added momentum scrolling and "rubber band" snap-back code from Travis Fischer.
--HG-- branch : momentumScrolling
1 parent e51d44b commit dd70827

File tree

4 files changed

+249
-6
lines changed

4 files changed

+249
-6
lines changed

License.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright (c) 2012, Drew McCormack, Brad Larson, Eric Skroch, Barry Wark, Dirkjan Krijnders, Rick Maddy, Vijay Kalusani, Caleb Cannon, Jeff Buck, Thomas Elstner, Jeroen Leenarts, Craig Hockenberry, Hartwig Wiesmann, Koen van der Drift, Nino Ag, Mike Lischke, and Trevor Harmon.
1+
Copyright (c) 2012, Drew McCormack, Brad Larson, Eric Skroch, Barry Wark, Dirkjan Krijnders, Rick Maddy, Vijay Kalusani, Caleb Cannon, Jeff Buck, Thomas Elstner, Jeroen Leenarts, Craig Hockenberry, Hartwig Wiesmann, Koen van der Drift, Nino Ag, Mike Lischke, Trevor Harmon, and Travis Fischer.
22
All rights reserved.
33

44
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

documentation/changelog.markdown

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ To be determined.
88
- **New**: Added support for styled text in all titles, labels, and text layers.
99
- **New**: Added a minor tick label shadow property to `CPTAxis`.
1010
- **New**: Added a property to hide plot data labels.
11+
- **New**: Added support for momentum scrolling.
12+
- **New**: Added support for "rubber band" snap-back when scrolling reaches the global x- and y-ranges.
1113
- **Changed**: Miscellaneous bug fixes and cleanup.
1214

1315

framework/Source/CPTXYPlotSpace.h

+14
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33

44
@class CPTPlotRange;
55

6+
typedef enum _CPTRubberBandType {
7+
kCPTRubberBandNone,
8+
kCPTRubberBandLeft,
9+
kCPTRubberBandRight
10+
}
11+
CPTRubberBandType;
12+
613
@interface CPTXYPlotSpace : CPTPlotSpace {
714
@private
815
CPTPlotRange *xRange;
@@ -13,6 +20,9 @@
1320
CPTScaleType yScaleType;
1421
CGPoint lastDragPoint;
1522
BOOL isDragging;
23+
BOOL hasMomentum;
24+
CPTRubberBandType rubberBand;
25+
CGPoint momentum;
1626
}
1727

1828
@property (nonatomic, readwrite, copy) CPTPlotRange *xRange;
@@ -22,4 +32,8 @@
2232
@property (nonatomic, readwrite, assign) CPTScaleType xScaleType;
2333
@property (nonatomic, readwrite, assign) CPTScaleType yScaleType;
2434

35+
@property (nonatomic, readwrite) BOOL allowsMomentum;
36+
@property (nonatomic, readwrite) BOOL elasticGlobalXRange;
37+
@property (nonatomic, readwrite) BOOL elasticGlobalYRange;
38+
2539
@end

framework/Source/CPTXYPlotSpace.m

+232-5
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ -(id)init
128128
yScaleType = CPTScaleTypeLinear;
129129
lastDragPoint = CGPointZero;
130130
isDragging = NO;
131+
hasMomentum = NO;
131132
}
132133
return self;
133134
}
@@ -162,6 +163,9 @@ -(void)encodeWithCoder:(NSCoder *)coder
162163
[coder encodeObject:self.globalYRange forKey:@"CPTXYPlotSpace.globalYRange"];
163164
[coder encodeInt:self.xScaleType forKey:@"CPTXYPlotSpace.xScaleType"];
164165
[coder encodeInt:self.yScaleType forKey:@"CPTXYPlotSpace.yScaleType"];
166+
[coder encodeBool:self.allowsMomentum forKey:@"CPTXYPlotSpace.allowsMomentum"];
167+
[coder encodeBool:self.elasticGlobalXRange forKey:@"CPTXYPlotSpace.elasticGlobalXRange"];
168+
[coder encodeBool:self.elasticGlobalYRange forKey:@"CPTXYPlotSpace.elasticGlobalYRange"];
165169

166170
// No need to archive these properties:
167171
// lastDragPoint
@@ -178,8 +182,13 @@ -(id)initWithCoder:(NSCoder *)coder
178182
xScaleType = (CPTScaleType)[coder decodeIntForKey : @"CPTXYPlotSpace.xScaleType"];
179183
yScaleType = (CPTScaleType)[coder decodeIntForKey : @"CPTXYPlotSpace.yScaleType"];
180184

185+
self.allowsMomentum = [coder decodeBoolForKey:@"CPTXYPlotSpace.allowsMomentum"];
186+
self.elasticGlobalXRange = [coder decodeBoolForKey:@"CPTXYPlotSpace.elasticGlobalXRange"];
187+
self.elasticGlobalYRange = [coder decodeBoolForKey:@"CPTXYPlotSpace.elasticGlobalYRange"];
188+
181189
lastDragPoint = CGPointZero;
182190
isDragging = NO;
191+
hasMomentum = NO;
183192
}
184193
return self;
185194
}
@@ -272,7 +281,15 @@ -(void)setXRange:(CPTPlotRange *)range
272281
NSParameterAssert(range);
273282

274283
if ( ![range isEqualToRange:xRange] ) {
275-
CPTPlotRange *constrainedRange = [self constrainRange:range toGlobalRange:self.globalXRange];
284+
CPTPlotRange *constrainedRange;
285+
286+
if ( self.elasticGlobalXRange ) {
287+
constrainedRange = [range copy];
288+
}
289+
else {
290+
constrainedRange = [self constrainRange:range toGlobalRange:self.globalXRange];
291+
}
292+
276293
[xRange release];
277294
xRange = [constrainedRange copy];
278295

@@ -297,7 +314,15 @@ -(void)setYRange:(CPTPlotRange *)range
297314
NSParameterAssert(range);
298315

299316
if ( ![range isEqualToRange:yRange] ) {
300-
CPTPlotRange *constrainedRange = [self constrainRange:range toGlobalRange:self.globalYRange];
317+
CPTPlotRange *constrainedRange;
318+
319+
if ( self.elasticGlobalXRange ) {
320+
constrainedRange = [range copy];
321+
}
322+
else {
323+
constrainedRange = [self constrainRange:range toGlobalRange:self.globalYRange];
324+
}
325+
301326
[yRange release];
302327
yRange = [constrainedRange copy];
303328

@@ -822,8 +847,16 @@ -(void)scaleBy:(CGFloat)interactionScale aboutPoint:(CGPoint)plotAreaPoint
822847
newRangeY = [theDelegate plotSpace:self willChangePlotRangeTo:newRangeY forCoordinate:CPTCoordinateY];
823848
}
824849

850+
BOOL elasticGlobalXRange = self.elasticGlobalXRange;
851+
BOOL elasticGlobalYRange = self.elasticGlobalYRange;
852+
self.elasticGlobalXRange = NO;
853+
self.elasticGlobalYRange = NO;
854+
825855
self.xRange = newRangeX;
826856
self.yRange = newRangeY;
857+
858+
self.elasticGlobalXRange = elasticGlobalXRange;
859+
self.elasticGlobalYRange = elasticGlobalYRange;
827860
}
828861

829862
/// @endcond
@@ -858,7 +891,8 @@ -(BOOL)pointingDeviceDownEvent:(CPTNativeEvent *)event atPoint:(CGPoint)interact
858891
BOOL handledByDelegate = [super pointingDeviceDownEvent:event atPoint:interactionPoint];
859892

860893
if ( handledByDelegate ) {
861-
isDragging = NO;
894+
isDragging = NO;
895+
hasMomentum = NO;
862896
return YES;
863897
}
864898

@@ -872,6 +906,9 @@ -(BOOL)pointingDeviceDownEvent:(CPTNativeEvent *)event atPoint:(CGPoint)interact
872906
// Handle event
873907
lastDragPoint = pointInPlotArea;
874908
isDragging = YES;
909+
hasMomentum = NO;
910+
rubberBand = kCPTRubberBandNone;
911+
875912
return YES;
876913
}
877914

@@ -909,7 +946,17 @@ -(BOOL)pointingDeviceUpEvent:(CPTNativeEvent *)event atPoint:(CGPoint)interactio
909946
}
910947

911948
if ( isDragging ) {
912-
isDragging = NO;
949+
isDragging = NO;
950+
hasMomentum = self.allowsMomentum;
951+
rubberBand = kCPTRubberBandNone;
952+
953+
if ( self.allowsMomentum ) {
954+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC / 60), dispatch_get_current_queue(), ^{
955+
[self handleMomentum];
956+
}
957+
);
958+
}
959+
913960
return YES;
914961
}
915962

@@ -961,6 +1008,8 @@ -(BOOL)pointingDeviceDraggedEvent:(CPTNativeEvent *)event atPoint:(CGPoint)inter
9611008
pointToUse = CPTPointMake(lastDragPoint.x + displacement.x, lastDragPoint.y + displacement.y);
9621009
}
9631010

1011+
momentum = displacement;
1012+
9641013
NSDecimal lastPoint[2], newPoint[2];
9651014
[self plotPoint:lastPoint forPlotAreaViewPoint:lastDragPoint];
9661015
[self plotPoint:newPoint forPlotAreaViewPoint:pointToUse];
@@ -975,8 +1024,31 @@ -(BOOL)pointingDeviceDraggedEvent:(CPTNativeEvent *)event atPoint:(CGPoint)inter
9751024

9761025
CPTPlotRange *globalX = self.globalXRange;
9771026
if ( globalX ) {
978-
newRangeX = (CPTMutablePlotRange *)[self constrainRange:newRangeX toGlobalRange:globalX];
1027+
if ( !self.elasticGlobalXRange ) {
1028+
newRangeX = (CPTMutablePlotRange *)[self constrainRange:newRangeX toGlobalRange:globalX];
1029+
}
1030+
else if ( newRangeX.locationDouble < globalX.locationDouble ) {
1031+
if ( shiftX._isNegative ) {
1032+
NSDecimal diff = CPTDecimalDivide(CPTDecimalSubtract(globalX.location, newRangeX.location), newRangeX.length);
1033+
diff = CPTDecimalFromDouble( MIN(1.0, [[NSDecimalNumber decimalNumberWithDecimal:CPTDecimalMultiply( diff, CPTDecimalFromDouble(3.0) )] doubleValue]) );
1034+
1035+
// diff=1 => no scaling
1036+
// diff=0 => full scaling
1037+
newRangeX.location = CPTDecimalAdd( self.xRange.location, CPTDecimalMultiply( shiftX, CPTDecimalSubtract(CPTDecimalFromDouble(1.0), diff) ) );
1038+
}
1039+
}
1040+
else if ( newRangeX.endDouble > globalX.endDouble ) {
1041+
if ( !shiftX._isNegative ) {
1042+
NSDecimal diff = CPTDecimalDivide(CPTDecimalSubtract(newRangeX.end, globalX.end), newRangeX.length);
1043+
diff = CPTDecimalFromDouble( MIN(1.0, [[NSDecimalNumber decimalNumberWithDecimal:CPTDecimalMultiply( diff, CPTDecimalFromDouble(3.0) )] doubleValue]) );
1044+
1045+
// diff=1 => no scaling
1046+
// diff=0 => full scaling
1047+
newRangeX.location = CPTDecimalAdd( self.xRange.location, CPTDecimalMultiply( shiftX, CPTDecimalSubtract(CPTDecimalFromDouble(1.0), diff) ) );
1048+
}
1049+
}
9791050
}
1051+
9801052
CPTPlotRange *globalY = self.globalYRange;
9811053
if ( globalY ) {
9821054
newRangeY = (CPTMutablePlotRange *)[self constrainRange:newRangeY toGlobalRange:globalY];
@@ -1000,6 +1072,161 @@ -(BOOL)pointingDeviceDraggedEvent:(CPTNativeEvent *)event atPoint:(CGPoint)inter
10001072
return NO;
10011073
}
10021074

1075+
-(void)handleMomentum
1076+
{
1077+
CPTPlotArea *plotArea = self.graph.plotAreaFrame.plotArea;
1078+
1079+
if ( !self.allowsUserInteraction || !plotArea || !hasMomentum || !self.allowsMomentum ) {
1080+
return;
1081+
}
1082+
1083+
CGPoint pointToUse = CPTPointMake(lastDragPoint.x + momentum.x, lastDragPoint.y + momentum.y);
1084+
1085+
id<CPTPlotSpaceDelegate> theDelegate = self.delegate;
1086+
1087+
CGFloat friction = 0.9f;
1088+
CPTPlotRange *globalX = self.globalXRange;
1089+
CPTPlotRange *globalY = self.globalYRange;
1090+
1091+
if ( rubberBand != kCPTRubberBandNone ) {
1092+
NSDecimal diff;
1093+
1094+
if ( rubberBand == kCPTRubberBandLeft ) {
1095+
diff = CPTDecimalSubtract(globalX.location, self.xRange.location);
1096+
}
1097+
else {
1098+
diff = CPTDecimalSubtract(self.xRange.end, globalX.end);
1099+
}
1100+
1101+
diff = CPTDecimalDivide(diff, self.xRange.length);
1102+
diff = CPTDecimalFromDouble( MIN(1.0, [[NSDecimalNumber decimalNumberWithDecimal:CPTDecimalMultiply( diff, CPTDecimalFromDouble(3.0) )] doubleValue]) );
1103+
1104+
// diff=0 => friction=0.9
1105+
// diff=1 => friction=0.6
1106+
friction = 0.9f - ([[NSDecimalNumber decimalNumberWithDecimal:diff] floatValue]) * 0.25f;
1107+
// NSLog(@"rubberBand %f; %f", momentum.x, friction);
1108+
}
1109+
1110+
momentum = CPTPointMake(momentum.x * friction, momentum.y * friction);
1111+
1112+
NSDecimal lastPoint[2], newPoint[2];
1113+
[self plotPoint:lastPoint forPlotAreaViewPoint:lastDragPoint];
1114+
[self plotPoint:newPoint forPlotAreaViewPoint:pointToUse];
1115+
1116+
CPTMutablePlotRange *newRangeX = [[self.xRange mutableCopy] autorelease];
1117+
CPTMutablePlotRange *newRangeY = [[self.yRange mutableCopy] autorelease];
1118+
1119+
NSDecimal shiftX = CPTDecimalSubtract(lastPoint[0], newPoint[0]);
1120+
NSDecimal shiftY = CPTDecimalSubtract(lastPoint[1], newPoint[1]);
1121+
1122+
if ( rubberBand != kCPTRubberBandNone ) {
1123+
if ( rubberBand == kCPTRubberBandLeft ) {
1124+
shiftX._isNegative = 0;
1125+
}
1126+
else if ( rubberBand == kCPTRubberBandRight ) {
1127+
shiftX._isNegative = 1;
1128+
}
1129+
}
1130+
1131+
newRangeX.location = CPTDecimalAdd(newRangeX.location, shiftX);
1132+
newRangeY.location = CPTDecimalAdd(newRangeY.location, shiftY);
1133+
1134+
if ( globalX ) {
1135+
if ( !self.elasticGlobalXRange ) {
1136+
newRangeX = (CPTMutablePlotRange *)[self constrainRange:newRangeX toGlobalRange:globalX];
1137+
}
1138+
else {
1139+
if ( newRangeX.locationDouble < globalX.locationDouble ) {
1140+
if ( rubberBand == kCPTRubberBandNone ) {
1141+
NSDecimal diff = CPTDecimalDivide(CPTDecimalSubtract(globalX.location, newRangeX.location), newRangeX.length);
1142+
diff = CPTDecimalFromDouble( MIN(1.0, [[NSDecimalNumber decimalNumberWithDecimal:CPTDecimalMultiply( diff, CPTDecimalFromDouble(3.0) )] doubleValue]) );
1143+
1144+
// slow momentum down faster past global range bounds
1145+
friction = 0.9f - ([[NSDecimalNumber decimalNumberWithDecimal:diff] floatValue]) / 3.0f;
1146+
momentum = CPTPointMake(momentum.x * friction, momentum.y);
1147+
1148+
// diff=1 => no scaling
1149+
// diff=0 => full scaling
1150+
newRangeX.location = CPTDecimalAdd( self.xRange.location, CPTDecimalMultiply( shiftX, CPTDecimalSubtract(CPTDecimalFromDouble(1.0), diff) ) );
1151+
}
1152+
}
1153+
else if ( newRangeX.endDouble > globalX.endDouble ) {
1154+
if ( rubberBand == kCPTRubberBandNone ) {
1155+
NSDecimal diff = CPTDecimalDivide(CPTDecimalSubtract(newRangeX.end, globalX.end), newRangeX.length);
1156+
diff = CPTDecimalFromDouble( MIN(1.0, [[NSDecimalNumber decimalNumberWithDecimal:CPTDecimalMultiply( diff, CPTDecimalFromDouble(3.0) )] doubleValue]) );
1157+
1158+
// slow momentum down faster past global range bounds
1159+
friction = 0.9f - ([[NSDecimalNumber decimalNumberWithDecimal:diff] floatValue]) / 3.0f;
1160+
momentum = CPTPointMake(momentum.x * friction, momentum.y);
1161+
1162+
// diff=1 => no scaling
1163+
// diff=0 => full scaling
1164+
newRangeX.location = CPTDecimalAdd( self.xRange.location, CPTDecimalMultiply( shiftX, CPTDecimalSubtract(CPTDecimalFromDouble(1.0), diff) ) );
1165+
}
1166+
}
1167+
else if ( rubberBand != kCPTRubberBandNone ) {
1168+
if ( rubberBand == kCPTRubberBandLeft ) {
1169+
newRangeX.location = globalX.location;
1170+
}
1171+
else if ( rubberBand == kCPTRubberBandRight ) {
1172+
newRangeX.location = CPTDecimalSubtract(globalX.end, newRangeX.length);
1173+
}
1174+
1175+
rubberBand = NO;
1176+
hasMomentum = NO;
1177+
}
1178+
}
1179+
}
1180+
1181+
if ( globalY ) {
1182+
newRangeY = (CPTMutablePlotRange *)[self constrainRange:newRangeY toGlobalRange:globalY];
1183+
}
1184+
1185+
// Delegate override
1186+
if ( [theDelegate respondsToSelector:@selector(plotSpace:willChangePlotRangeTo:forCoordinate:)] ) {
1187+
self.xRange = [theDelegate plotSpace:self willChangePlotRangeTo:newRangeX forCoordinate:CPTCoordinateX];
1188+
self.yRange = [theDelegate plotSpace:self willChangePlotRangeTo:newRangeY forCoordinate:CPTCoordinateY];
1189+
}
1190+
else {
1191+
self.xRange = newRangeX;
1192+
self.yRange = newRangeY;
1193+
}
1194+
1195+
lastDragPoint = pointToUse;
1196+
1197+
if ( hasMomentum ) {
1198+
if ( ABS(momentum.x) < 0.5 ) {
1199+
CGFloat rubberBandMomentum = 22.0f;
1200+
1201+
if ( rubberBand != kCPTRubberBandNone ) {
1202+
momentum = CGPointMake(momentum.x / friction, momentum.y / friction);
1203+
}
1204+
else {
1205+
if ( globalX && (self.xRange.locationDouble < globalX.locationDouble) ) {
1206+
momentum.x = rubberBandMomentum;
1207+
rubberBand = kCPTRubberBandLeft;
1208+
// NSLog(@"rubberBand left");
1209+
}
1210+
else if ( globalX && (self.xRange.endDouble > globalX.endDouble) ) {
1211+
momentum.x = -rubberBandMomentum;
1212+
rubberBand = kCPTRubberBandRight;
1213+
// NSLog(@"rubberBand right");
1214+
}
1215+
else {
1216+
hasMomentum = NO;
1217+
}
1218+
}
1219+
}
1220+
1221+
if ( hasMomentum ) {
1222+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC / 60), dispatch_get_current_queue(), ^{
1223+
[self handleMomentum];
1224+
}
1225+
);
1226+
}
1227+
}
1228+
}
1229+
10031230
/// @}
10041231

10051232
@end

0 commit comments

Comments
 (0)