Core Animation difficulties with spinning dial control (very detailed)
Im trying to create a spinning dial control, basically a set of 6 di开发者_JAVA技巧gits 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
I am remembering there was something about the problem of the animation jumping back into its previous state in the CoreAnimation videos of WWDC10 you can download for free from apple.
I have to look it up later, but there is of course the delegate method - (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)flag
which gets called when the animation is done. This would be a nice place to do some housekeeping. Maybe you could there set the y-offset of the layer by looking at the toValue
property of the animation.
It is just a long shot, but maybe it is pointing into the right direction.
Do I understand correctly that the visual effect you want is to have a wheel of numbers (sort of a little like a dartboard) that spin around like the old-style gas/water meter on the side of our house when we were growing up?
If so, why don't you just have a single image that is the digits laid out in a circle, then rotate that one image the desired amount? Something like (untested code typed in browser):
const double degreesPerDigit = 360.0 / 10.0;
const double radPerDigit = degreesPerDigit * M_PI / 180.0;
CGAffineTransform xfm = CGAffineTransformMakeRotation(radPerDigit * digitUp);
// animated or otherwise:
myView.transform = xfm;
To your direct questions (again, code typed in browser):
To set a container layer's offset:
CGRect rect = myLayer.frame;
rect.origin.y = newYValue;
myLayer.frame = rect;
To help guard against flicker, there are many techniques, depending on the exact nature of the problem and the desired result, but a common one is to use 2 views that are nearly identical, make changes on view1 then, at the last moment, hide view1 and un-hide view2.
From your questions, it sounds like you have a decent understanding of this stuff, so I'm sorry if I answered below your level, but it's hard to figure out exactly what you're doing and what's going wrong.
Advice: It might be worth your while to both try to carve-back the question to just the important bits and, similarly, to write a stripped-down sample app that does ONLY this animation and nothing else, and then experiment with the minimal test case. Modularize everything, log everything, study the output. Repeat, rinse, fade.
精彩评论