Im trying to create a spinning dial control, basically a set of 6 digits that spin around and around to give the effect of a spinning number meter (similar to your power/water meters, or perhaps a poker machine, in fact very similar to the existing UIPickerView control, but with a completly different look and feel).
So far, I almost have it working, but Im at a stage where core animation is giving me grief.
Its quite complex, so code snippets would be tough to give a good snapshot of what is going on, so I think pseudo code will suffice.
Firstly, in terms of the view setup, I have 6 separate UIView
s (called NumberView1, NumberView2, etc...), each one for each number in the control.
Inside each NumberViewX I have another UIView
, which is a container view, called ContainerView1, 2, etc...
I then have 10 UIImageView
s stacked on top of each other at different Y offsets. These images are all 30x30 which makes it nice. 9 is first, then 8 at y-offset 30, then 7 at y-offset 60, etc... all the way down to 0 at y-offset 270.
IMPORTANT NOTE: My numbers only ever scroll upwards
The numbers represent a 5-decimal point number (i.e. 2.34677) which scrolls up (to, for example, 2.61722).
I also have some dictionaries which hold the current numeric value for each number and the offsets for each number:
NSArray *offsets = [[NSArray alloc] initWithObjects:
[NSNumber numberWithInt:0], //9
[NSNumber numberWithInt:-30], //8
[NSNumber numberWithInt:-60], //7
[NSNumber numberWithInt:-90], //6
[NSNumber numberWithInt:-120], //5
[NSNumber numberWithInt:-150], //4
[NSNumber numberWithInt:-180], //3
[NSNumber numberWithInt:-210], //2
[NSNumber numberWithInt:-240], //1
[NSNumber numberWithInt:-270], //0
nil];
NSArray *keys = [[NSArray alloc] initWithObjects:
[NSNumber numberWithInt:9],
[NSNumber numberWithInt:8],
[NSNumber numberWithInt:7],
[NSNumber numberWithInt:6],
[NSNumber numberWithInt:5],
[NSNumber numberWithInt:4],
[NSNumber numberWithInt:3],
[NSNumber numberWithInt:2],
[NSNumber numberWithInt:1],
[NSNumber numberWithInt:0], nil];
offsetDict = [[NSDictionary alloc] initWithObjects:offsets forKeys:keys];
[currentValuesForDecimalPlace setObject:[NSNumber numberWithInt:2] forKey: [NSValue valueWithNonretainedObject: self.containerViewDollarSlot1]];
[currentValuesForDecimalPlace setObject:[NSNumber numberWithInt:8] forKey: [NSValue valueWithNonretainedObject: self.containerViewDecimalPlace1]];
[currentValuesForDecimalPlace setObject:[NSNumber numberWithInt:3] forKey: [NSValue valueWithNonretainedObject: self.containerViewDecimalPlace2]];
[currentValuesForDecimalPlace setObject:[NSNumber numberWithInt:3] forKey: [NSValue valueWithNonretainedObject: self.containerViewDecimalPlace3]];
[currentValuesForDecimalPlace setObject:[NSNumber numberWithInt:8] forKey: [NSValue valueWithNonretainedObject: self.containerViewDecimalPlace4]];
[currentValuesForDecimalPlace setObject:[NSNumber numberWithInt:5] forKey: [NSValue valueWithNonretainedObject: self.containerViewDecimalPlace5]];
Now for the logic, I start out setting the ContainerView's layer properties to be certain offsets of Y which will move the current numbers into their correct spots, like so:
self.containerViewDollarSlot1.layer.transform = CATransform3DTranslate(self.containerViewDollarSlot1.layer.transform, 0, -210, 0);
self.containerViewDecimalPlace1.layer.transform = CATransform3DTranslate(self.containerViewDecimalPlace1.layer.transform, 0, -30, 0);
self.containerViewDecimalPlace2.layer.transform = CATransform3DTranslate(self.containerViewDecimalPlace2.layer.transform, 0, -180, 0);
self.containerViewDecimalPlace3.layer.transform = CATransform3DTranslate(self.containerViewDecimalPlace3.layer.transform, 0, -180, 0);
self.containerViewDecimalPlace4.layer.transform = CATransform3DTranslate(self.containerViewDecimalPlace4.layer.transform, 0, -30, 0);
self.containerViewDecimalPlace5.layer.transform = CATransform3DTranslate(self.containerViewDecimalPlace5.layer.transform, 0, -120, 0);
which will show the numbers, 2.83385, which is an arbitrary number for testing sake.
Then I enter in another value in a textbox and hit the go button which kicks off the animation logic, which is where I get the difficulties in Core Animation (as the title suggests)
I call:
[self animateDecimalPlace: 0
withAnimation: self.dollarSlot1Animation
andContainerView: self.containerViewDollarSlot1];
[self animateDecimalPlace: 1
withAnimation: self.decimalPlace1Animation
andContainerView: self.containerViewDecimalPlace1];
//etc... for all 6 numbers
where dollarSlot1Animation is a CABasicAnimation
ivar
The method is defined below:
- (void) animateDecimalPlace: (int) decimalIndex withAnimation: (CABasicAnimation*)animation andContainerView: (UIView*) containerView{
NSRange decimalRange = {decimalIndex == 0 ? 0 : 2,
decimalIndex == 0 ? 1 : decimalIndex};
double diff = 0;
if (decimalIndex == 0)
{
int decimalPartTarget = [[[NSString stringWithFormat:@"%f", targetNumber] substringWithRange: decimalRange] intValue];
int decimalPartCurrent = [[[NSString stringWithFormat:@"%f", currentValue] substringWithRange: decimalRange] intValue];
diff = decimalPartTarget - decimalPartCurrent;
}
else {
double fullDiff = targetNumber - currentValue;
diff = [[[NSString stringWithFormat:@"%f", fullDiff] substringWithRange: decimalRange] doubleValue];
}
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeRemoved;
animation.duration = 5.0;
NSNumber *n = [currentValuesForDecimalPlace objectForKey:[NSValue valueWithNonretainedObject: containerView]];
NSNumber *offset = (NSNumber*)[offsetDict objectForKey: n];
int rotations = [self setupNumberForAnimation: diff decimalIndex: decimalIndex forContainer: containerView];
int finalOffset = (rotations*30)+([offset intValue]);
CATransform3D translation = CATransform3DMakeTranslation(0, finalOffset, 0);
animation.fromValue = [NSValue valueWithCATransform3D:containerView.layer.transform];
animation.toValue = [NSValue valueWithCATransform3D:translation];
animation.delegate = self;
[containerView.layer addAnimation: animation forKey: [NSString stringWithFormat:@"%i", decimalIndex] ];
}
As you can see (or perhaps not:)) I am treating each number basically independent, and telling it to translate to a new Y position, but you may have noticed a method in there called setupNumberForAnimation
which is another large method that dynamically adds more UIImageView
s to the container UIView above the initial 10 image tiles.
For example, there are 10 tiles to start out with, if I want to scroll the dial UP 21 spots (going from 3 to 24, for example) I would need to add 15 new numbers above 9, then animate the translation of the container view up to the top most image tile.
After the scroll is done, I remove the dynamically added UIImageView
s and re-position the container view's y-offset back to a value within 0 to -270 (essentially ending up on the same number, but removing all the un-necessary image views).
This gives the smooth animation that im after. And it works. Trust me :)
My problem is two fold, firstly when the animation stops Core Animation reverts the animations changes to the layer because I have set animation.fillMode = kCAFillModeRemoved;
I couldnt figure out how, after the animations is complete, to set the container layer's y offset, I know it sounds odd, but if I have the fillMode property set to kCAFillModeForwards, nothing I tried seemed to have an effect on the layer.
Secondly, when the animation stops and the change is reverted, there is a tiny flicker between when the animation reverts the translation and when I set it again, I cannot figure out how to get around this.
I know there is a heap of details and specificity here, but any help or ideas on how to accomplish this would be greatly greatly appreciated.
Thanks a lot