Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Core Animation difficulties with spinning dial control (very detailed)

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

like image 983
Mark Avatar asked Sep 01 '10 04:09

Mark


1 Answers

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.

like image 81
GorillaPatch Avatar answered Oct 22 '22 16:10

GorillaPatch