views:

885

answers:

2

Ola Folks,

Once again, I want to leech experience, information and knowledge from those who have more of them than I do. Granted, I do make some effort looking for questions to answer... :)

Anyway, I want to draw an image that would effectively be a circular progress indicator on a UIButton. Because the image is supposed to represent progress of a task, I do not think I should handle the drawing code in the view's drawrect method.

I have a thread that is performing some tasks. After each task, it calls a method on the main thread. The called method is supposed to update the image on the button.

In the button update method, I create a CGContextRef by using CGBitmapContextCreate. Then I use the button's frame to create a CGRect. Then I attempt to draw into using the context I created. Lastly I set NeedsDisplay and clean up.

But none of this is inside the view's drawrect method.

I would like to know if anyone has used CGContext to draw on / in a view on-demand in a view while the view is being displayed.

I would like to get some ideas regarding an approach to doing this.

Here is an encapsulated version of what I am doing now: CGContextRef xContext = nil; CGColorSpaceRef xColorSpace; CGRect xRect; void* xBitmapData; int iBMPByteCount; int iBMPBytesPerRow; float fBMPWidth = 20.0f; float fBMPHeight = 20.0f; float fPI = 3.14159; float fRadius = 25.0f;

iBMPBytesPerRow = (fBMPWidth * 4);
iBMPByteCount = (iBMPBytesPerRow * fBMPHeight);
xColorSpace = CGColorSpaceCreateDeviceRGB();
xBitmapData = malloc(iBMPByteCount);
xContext = CGBitmapContextCreate(xBitmapData, fBMPWidth, fBMPHeight, 8, iBMPBytesPerRow, xColorSpace, kCGImageAlphaPremultipliedFirst);
CGColorSpaceRelease(xColorSpace);
UIGraphicsPushContext(xContext);
xRect = CGRectMake(30.0f, 400.0f, 50.0f, 50.0f);

float fWidth = xRect.size.width;
float fHeight = xRect.size.height;

CGContextClearRect(xContext, xRect);
CGContextSetRGBStrokeColor(xContext, 0.5f, 0.6f, 0.7f, 1.0f);
CGContextSetLineWidth(xContext, 1.0f);

float fArcBegin = 45.0f * fPI / 180.0f;
float fArcEnd = 90.0f * fPI / 180.0f;

CGContextSetFillColor(xContext, CGColorGetComponents( [[UIColor greenColor] CGColor]));
CGContextMoveToPoint(xContext, fWidth, fHeight);
CGContextAddArc(xContext, fWidth, fHeight, fRadius, fArcBegin, fArcEnd, 0);

CGContextClosePath(xContext); 
CGContextFillPath(xContext); 

UIGraphicsPopContext;
CGContextRelease(xContext);

[self.view setNeedsDisplay];

// [self.view setNeedsDisplayInRect: xRect];

The above is a little bit wonky because I've tried different tweaks. However, I think it communicates what I am trying to do.

Thanx,

-isdi-

A: 

Alternative approach:

You could create a series of images that represent the progress updates and then replace the UIButton currentImage property with the setImage:forState: method at each step of the process. This doesn't require drawing in the existing view and this approach has worked well for me to show simple "animation" of images (buttons or other).

Would this approach work for you? If not, why not?

Bart

Bart Gottschalk
Hi Bart, This is what I implemented after I could not make the original idea work. I am hoping that the image rotation implementation is going to be temporary, but I accept that it might be permanent.
ISDi
A: 

Ola Folks,

This was really bugging me so after dealing with a series of silly, but necessary issues regarding the project I want this functionality for, I played around with it. The end result is that I can now arbitrarily draw an arc representing the progress of a particular background task to a button.

The goal was to draw something like the little indicator in the lower right hand corner of the XCode windows while a project is being cleaned or compiled.

I created a function that will draw and fill an arc and return it as a UIImage.

The worker thread calls method (PerformSelectorOnMainThread) with the current values and a button identifier. In the called method, I call the arc image function with the percentage filled and such.

example call:

oImg = [self ArcImageCreate:100.0f fWidth:100.0f 
    fPercentFilled: 0.45f fAngleStart: 0.0f xFillColor:[UIColor blueColor]];

Then set the background image of the button:

[oBtn setBackgroundImage: oImg forState: UIControlStateNormal];

Here is the function:
It is not finished, but it works well enough to illustrate how I am doing this.



/**
ArcImageCreate
@ingroup    UngroupedFunctions
@brief  Create a filled or unfilled solid arc and return it as a UIImage.

        Allows for dynamic / arbitrary update of an object that allows a UIImage to be drawn on it. \
    This can be used for some sort of pie chart or progress indicator by Image Flipping.

@param  fHeight     The height of the created UIImage.
@param  fWidth      The width of the created UIImage.
@param  fPercentFilled  A percentage of the circle to be filled by the arc. 0.0 to 1.0.
@param  AngleStart  The angle where the arc should start. 0 to 360. Clock Reference.
@param  xFillColor  The color of the filled area.

@return Pointer to a UIImage.

@todo [] Validate object creation at each step.

@todo [] Convert PercentFilled (0.0 to 1.0) to appropriate radian(?) (-3.15 to +3.15)

@todo [] Background Image Support. Allow for the arc to be drawn on top of an image \
    and the whole thing returned.

@todo [] Background Image Reduction. Background images will have to be resized to fit the specfied size. \
    Do not want to return a 65KB object because the background is 60K or whatever.

@todo [] UIColor RGBA Components. Determine a decent method of extracting RGVA values \
    from a UIColor*. Check out arstechnica.com/apple/guides/2009/02/iphone-development-accessing-uicolor-components.ars \
    for an idea.

*/
- (UIImage*) ArcImageCreate: (float)fHeight fWidth:(float)fWidth fPercentFilled:(float)fPercentFilled fAngleStart:(float)fAngleStart xFillColor:(UIColor*)xFillColor
{
    UIImage* fnRez = nil;
    float fArcBegin = 0.0f;
    float fArcEnd = 0.0f;
    float fArcPercent = 0.0f;
    UIColor* xArcColor = nil;
    float fArcImageWidth = 0.0f;
    float fArcImageHeight = 0.0f;
    CGRect xArcImageRect;

    CGContextRef xContext = nil;
    CGColorSpaceRef xColorSpace;
    void* xBitmapData;
    int iBMPByteCount;
    int iBMPBytesPerRow;
    float fPI = 3.14159;
    float fRadius = 25.0f;

// @todo Force default of 100x100 px if out of bounds. \
//  Check max image dimensions for iPhone. \
//  If negative, flip values *if* values are 'reasonable'. \
//  Determine minimum useable pixel dimensions. 10x10 px is too small. Or is it?
    fArcImageWidth = fHeight;
    fArcImageHeight = fWidth;

    // Get the passed target percentage and clip it between 0.0 and 1.0
    fArcPercent = (fPercentFilled  1.0f) ? 1.0f : fPercentFilled;
    fArcPercent = (fArcPercent > 1.0f) ? 1.0f : fArcPercent;

    // Get the passed start angle and clip it between 0.0 to 360.0
    fArcBegin = (fAngleStart  359.0f) ? 0.0f : fAngleStart;
    fArcBegin = (fArcBegin > 359.0f) ? 0.0f : fArcBegin;

    fArcBegin = (fArcBegin * fPI) / 180.0f;
    fArcEnd = ((360.0f * fArcPercent) * fPI) / 180.0f;

    // 
    if (xFillColor == nil) {
        // random color
    } else {
        xArcColor = xFillColor;
    }

    // Calculate memory required for image.
    iBMPBytesPerRow = (fArcImageWidth * 4);
    iBMPByteCount = (iBMPBytesPerRow * fArcImageHeight);
    xBitmapData = malloc(iBMPByteCount);

    // Create a color space. Behavior changes at OSXv10.4. Do not rely on it for consistency across devices.
    xColorSpace = CGColorSpaceCreateDeviceRGB();

    // Set the system to draw. Behavior changes at OSXv10.3.
    //  Both of these work. Not sure which is better.
//  xContext = CGBitmapContextCreate(xBitmapData, fArcImageWidth, fArcImageHeight, 8, iBMPBytesPerRow, xColorSpace, kCGImageAlphaPremultipliedFirst);
    xContext = CGBitmapContextCreate(NULL, fArcImageWidth, fArcImageHeight, 8, iBMPBytesPerRow, xColorSpace, kCGImageAlphaPremultipliedFirst);

    // Let the system know the colorspace reference is no longer required.
    CGColorSpaceRelease(xColorSpace);

    // Set the created context as the current context.
//  UIGraphicsPushContext(xContext);

    // Define the image's box.
    xArcImageRect = CGRectMake(0.0f, 0.0f, fArcImageWidth, fArcImageHeight);

    // Clear the image's box.
//  CGContextClearRect(xContext, xRect);

    // Draw the ArcImage's background image.
//  CGContextDrawImage(xContext, xArcImageRect, [oBackgroundImage CGImage]);

    // Set Us Up The Transparent Drawing Area.
    CGContextBeginTransparencyLayer(xContext, nil);

    // Set the fill and stroke colors
// @todo [] Determine why SetFilColor does not. Use alternative method.
//  CGContextSetFillColor(xContext, CGColorGetComponents([xArcColor CGColor]));
//  CGContextSetFillColorWithColor(xContext, CGColorGetComponents([xArcColor CGColor]));
// Test Colors
    CGContextSetRGBFillColor(xContext, 0.3f, 0.4f, 0.5f, 1.0f);
    CGContextSetRGBStrokeColor(xContext, 0.5f, 0.6f, 0.7f, 1.0f);
    CGContextSetLineWidth(xContext, 1.0f);

// Something like this to reverse drawing?
//  CGContextTranslateCTM(xContext, TranslateXValue, TranslateYValue);
//  CGContextScaleCTM(xContext, -1.0f, 1.0f); or CGContextScaleCTM(xContext, 1.0f, -1.0f);

    // Test Vals
//  fArcBegin = 45.0f * fPI / 180.0f;   // 0.785397
//  fArcEnd = 90.0f * fPI / 180.0f; // 1.570795

    // Move to the start point and draw the arc.
    CGContextMoveToPoint(xContext, fArcImageWidth/2.0f, fArcImageHeight/2.0f);
    CGContextAddArc(xContext, fArcImageWidth/2.0f, fArcImageHeight/2.0f, fRadius, fArcBegin, fArcEnd, 0);


    // Ask the OS to close the arc (current point to starting point). 
    CGContextClosePath(xContext);

    // Fill 'er up. Implicit path closure.
    CGContextFillPath(xContext);
//  CGContextEOFillPath(context);

    // Close Transparency drawing area.
    CGContextEndTransparencyLayer(xContext);

    // Create an ImageReference and create a UIImage from it.
    CGImageRef xCGImageTemp = CGBitmapContextCreateImage(xContext);
    CGContextRelease(xContext);
    fnRez = [UIImage imageWithCGImage: xCGImageTemp];
    CGImageRelease(xCGImageTemp);

//  UIGraphicsPopContext;

    return fnRez;
}


ISDi