views:

119

answers:

2

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 UIViews (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 UIImageViews 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 UIImageViews 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 UIImageViews 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

A: 

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.

Olie
Your idea about spinning a circular image is interesting, could you elaborate or this a little more? If it is what I think it is, (a circle of numbers) wouldn't it look like the numbers are coming in on an angle instead of strait down? Does that make sense? Thanks for the response!
Mark
On another note, how hard would it be to implement a 3d spinner? Could you do this simply using Core Animation? That would rock!
Mark
This is why I said it's not clear what visual effect you want. Are you looking to get something like a slot machine? Or something more like the old-school gas-meters? Or something else, entirely. If you could provide a link to the general look you want, that would help us answer.
Olie
yeah old, school gas meters is what im after, I do have something working at the moment, but it just feels WAY overcomplicated for what it is...
Mark
A: 

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.

GorillaPatch
Yeah thanks, the way that I got around this issue in the end was to change the fillMode property to kCAFillModeForwards, set removedOnCompletion = NO and then after the animation completed (in the animationDidStop: method, call removeAllAnimations on the layer that was being animated, then setting it immediatly afterwards worked like a charm with no jumping...
Mark
Thanks for the comment. So at least I guessed kind of right.
GorillaPatch
marking this as correct as my comment and your post solve the issues that I was having in the question
Mark