Search code examples
ipadbar-chartcgaffinetransformscale

iPad: How can I create a dynamic barchart using CGAffineTransformScale?


I am trying to create an animated barchart by scaling the bar (slider) and translating the endcap. The endcap works as expected, however, the "slider" translates when I try to scale it horizontally.

-(void) animateBarOpen: (NSInteger) rowIndex {
    NSLog(@"%s: row=%d %@", __FUNCTION__, rowIndex, [self.values objectAtIndex:rowIndex]);
    NSMutableDictionary *row = [self.values objectAtIndex:rowIndex];
    UIImageView *slider = [row objectForKey:@"slider"];
    UIImageView *endCap = [row objectForKey:@"image"];
    UILabel *labelInfo = [row objectForKey:@"label"];
    [labelInfo setText: [NSString stringWithFormat: @"%@ %@", [row objectForKey:@"quantity"], [row objectForKey:@"color"]]];
    [labelInfo setTextColor: productColor];

    float quantity = [(NSNumber*) [row objectForKey:@"quantity"] floatValue];
    float tx = 10.0f * quantity;
    float sx = -40.0f * quantity;

    [UIView beginAnimations:@"open" context:nil];
    [UIView setAnimationDuration:0.25];
    [UIView setAnimationDelay:0.0];
    [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];

    slider.transform = CGAffineTransformScale(CGAffineTransformIdentity, sx, 1.0f);

    endCap.transform = CGAffineTransformTranslate(CGAffineTransformIdentity, tx, 0.0f);
    labelInfo.transform = CGAffineTransformTranslate(CGAffineTransformIdentity, tx, 0.0f);

    [UIView commitAnimations];
}

Solution

  • OK - I found a good ref for explaining the issue that allowed me to fix my code. Basically, since scaling is based upon the view's center property, it therefore translates the origin. As a result, you have to (re-)translate the view to the location you expected to translate as a ratio of the translation distance of location that the scaling moved.

    Demystifying CGAffineTransform

    Below is my code (that works) and you will notice a computation of the ratio of width before-to-after and then translating by that amount. Look at the computation for sx and the second computation of tx (after the animation). I thought both should go in the animation, but that does not work as you might expect if the second translation is inside the begin/commit.

    Also note: I left some of the cosmetic code for displaying a colorful caption at the tip of the bar.

    -

    (void) animateBarOpen: (NSInteger) rowIndex withDuration: (NSTimeInterval) duration {
        if (VERBOSE_LOG) {
            NSLog(@"%s: row=%d %@", __FUNCTION__, rowIndex, [self.values objectAtIndex:rowIndex]);
        }
    
        NSMutableDictionary *rowValues = [self.values objectAtIndex:rowIndex];
        NSMutableDictionary *rowOutlets = [self.iboutlets objectAtIndex:rowIndex];
        UIColor *productColor = [InfographView colorByName: [rowValues objectForKey: @"color"]];
        UIImageView *slider = [rowOutlets objectForKey:@"slider"];
        UIImageView *endCap = [rowOutlets objectForKey:@"image"];
        UILabel *labelInfo = [rowOutlets objectForKey:@"label"];
        NSAssert1(labelInfo != nil, @"labelInfo is NULL??", nil);
        [labelInfo setBackgroundColor: productColor];
        // (255, 253, 208)
        UIColor *creamColor = [UIColor colorWithRed:255.0/255.0f green:253.0/255.0f blue:208.0/255.0f alpha:0.9];
        [labelInfo setTextColor: creamColor];
        if ([(NSString*)[rowValues objectForKey:@"color"] isEqualToString:@"Black"]) {
            [labelInfo setShadowColor: [UIColor grayColor]];
        } else {
            [labelInfo setShadowColor: [UIColor blackColor]];
        }
        [labelInfo setText: [NSString stringWithFormat: @"%@ %@", [rowValues objectForKey:@"quantity"], [rowValues objectForKey:@"color"]]];
    
        [slider setHidden:YES];
        [slider setAlpha: 0.7f];
    
        float rangeScale = [self computeRangeScale: self.values forWidth: CGRectGetWidth(self.frame) - MARGINS];
    
        slider.transform = CGAffineTransformIdentity;
    
        float quantity = [(NSNumber*) [rowValues objectForKey:@"quantity"] floatValue];
        float tx = rangeScale * quantity;
        float sx = tx / CGRectGetWidth(slider.frame); // (CGRectGetWidth(endCap.frame) - CGRectGetWidth(slider.frame));
    
        float sliderWidth = CGRectGetWidth(slider.frame);
    
        [slider setHidden:(rowIndex == -1)];
    
        [UIView beginAnimations:@"open" context:nil];
        [UIView setAnimationDuration:duration];
        [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
    
        slider.transform = CGAffineTransformScale(CGAffineTransformIdentity, sx, 1.0f);
        endCap.transform = CGAffineTransformTranslate(CGAffineTransformIdentity, tx, 0.0f);
        labelInfo.transform = CGAffineTransformTranslate(CGAffineTransformIdentity, tx, (-rowIndex*1.0f));
        [labelInfo setAlpha:1.0];
    
        [UIView commitAnimations];
    
        tx = 1.0f * (CGRectGetWidth(slider.frame) - sliderWidth) / (sx*2.0f);      slider.transform = CGAffineTransformTranslate(slider.transform, tx, 0.0f);
        [slider setHidden:NO];
    
    }