UIView drawRect is jerky i.e., does not scroll smoothly across the screen. I have tried performing 'setNeedsDisplay' at various intervals from 200-ms to 500-ms and nothing seems to improve the look. That is as the plots move across the screen they stop and start. Is there any way that I can improve this to make the display more smooth?
/*==============================================================================*/
/* 'drawRect' - override function. */
/* */
/* This function is responsible for plotting cardiac waveforms (ECG) and also */
/* erases any prior ECG plot values that may be currently visible on the */
/* screen. */
/* */
/* It does this indirectly via 'EcgThread' or 'ECGErase' which send the */
/* 'setNeedsDisplay' message which triggers the 'drawRect' callback function. */
/* Once an ECG is started 'EcgThread' controls the frequency of 'drawRect' */
/* being by simple timer. (See 'EcgThread' for current timer value.) */
/* */
/*==============================================================================*/
/* Here's how the entire ECG process works: */
/* */
/* 1) User starts ECG which causes a 'send_ecg' command to be sent to */
/* 'CommThread'. */
/* */
/* 2) 'CommThread' the sends the 'send_ecg' command to the Pacemaker and then */
/* sends itself the 'read_ecg' command.*/
/* */
/* 3) The Pacemaker then starts acquiring 64 cardiac A/D samples (10-bits) per */
/* second and sends them back to 'CommThread'. */
/* */
/* 4) As samples are received by 'CommThread' (via 'read_ecg') on a streaming */
/* basis they are stored in 'ECGY' array (vertical plots) in reverse order */
/* i.e., from top to bottom (currently room for 128 samples). */
/* */
/* 5) 'EcgThread' runs continuously on a timer basis sending 'setNeedsDisplay' */
/* message which causes 'iOS' to perform callbacks to 'drawRect' who is */
/* responsible for drawing the cardiac (plots) waveforms from left to right */
/* across (horizontally) the screen. */
/* */
/* 6) 'drawRect' processes 'ECGY' bottom to top (opposite of 'CommThread') and */
/* each draw loop (plotting new values) takes about 13-millseconds. This is */
/* necessary because not doing so results in drawing upside down plots. */
/* */
/* 7) User stops ECG and 'TimerProc' sends a series of 'send_ecgstop' commands */
/* to 'CommThread' who in turn sends the 'send_ecgstop' commands to the PM. */
/* The reason why we send multiple 'send_ecgstop' is due to the streaming */
/* nature of the sending ECG samples and getting the PM to 'listen' to us. */
/* Usually stopping will go smoothly. Occasionally it may be necessary to */
/* move the Wand away, wait a few seconds and place Wand back over patient's */
/* chest (causing an interrupt) before normal operation returns. */
/* */
/*==============================================================================*/
- (void) drawRect : (CGRect) rect // Callback routine
{
int i, ii, x, xx, y; // Local array indices
fEcgDraw = YES; // Show 'drawRect' is running
[super drawRect : rect]; // Call standard handler
CGContextRef context = UIGraphicsGetCurrentContext (); // Get graphics context
CGContextSetAllowsAntialiasing (context, NO); // Turn off anti-alliaing
CGContextSetLineWidth (context, 1); // Set width of 'pen'
/*==============================================================================*/
/* 'HH' is used as a height bias in order to position the waveform plots in */
/* middle of the view (screen). */
/*==============================================================================*/
HH = 424; // Force height bias for now
if (fEcgErase == YES) // Show we erase the view?
{
CGContextSetStrokeColorWithColor (context, // Set color of 'pen'
[UIColor blackColor].CGColor); // Black (to erase)
/*==============================================================================*/
/* Erase the last screen. */
/*==============================================================================*/
for (i = 0, x = 0; i < 127; i++) // Iterate for all array elements
{
CGContextMoveToPoint (context, // Update current position to specified point
ECGX[x], // Starting X-coordinate
(HH - ECGS[x])); // Starting Y-coordinate (with height bias)
CGContextAddLineToPoint (context, ECGX[(x + 1)], // Draw line from current position
(HH - ECGS[((x + 1) % 127)])); // Ending Y-coordinate (with height bias)
x++; // Step to next array element
} // end - for (i = 0; i < 127; i++)
CGContextClosePath (context); // Close current path
CGContextStrokePath (context); // Stroke current path (paint the path)
fEcgErase = NO; // Reset erase flag
} // end - if (fEcgErase == YES)
else if (fECGLOOP) // Did request come from 'EcgThread'?
/*==============================================================================*/
/* Draw ECG cardiac waveforms on view. */
/*==============================================================================*/
{
xx = 1; // Counts markers
x = 0; // Reset horizontal axis
y = YY; // Use saved startimg ECGY[] index
ii = 0; // Initialize marker count
#define GRIDSIZE 12 // Grid width in pixels
int width = rect.size.width; // Get the view width
int height = rect.size.height; // Get the view height
/*==============================================================================*/
/* First draw a grid pattern to draw ECG waveforms into. */
/*==============================================================================*/
CGContextSetStrokeColorWithColor (context, // Set color of 'pen'
[UIColor lightGrayColor].CGColor); // Use 'light gray' for grid pattern
for (i = 0; i <= width; i = i+GRIDSIZE) // First the vertical lines
{
CGContextMoveToPoint (context, i, 0); // Update current position to specified point
CGContextAddLineToPoint (context, i, height); // Draw line from current position
} // end - for (i = 0; i <= width; i = i+GRIDSIZE)
for (i = 0 ; i <= height; i = i+GRIDSIZE) // Then the horizontal lines
{
CGContextMoveToPoint (context, 0, i); // Update current position to specified point
CGContextAddLineToPoint (context, width, i); // Draw line from current position
} // end - for (i = 0 ; i <= height; i = i+GRIDSIZE)
CGContextClosePath (context); // Close current path
CGContextStrokePath (context); // Stroke current path (paint the path)
/*==============================================================================*/
/* Now draw (plot) cardiac waveforms using using pre-stored ECG sample values. */
/*==============================================================================*/
for (i = 0; i < 127; i++) // Iterate for number ECGY[] entries
{
/*========================================================================开发者_开发百科======*/
/* Erase the prior ECG A/D plot value. */
/*==============================================================================*/
#if 0 // NOT NEEDED CUZ WE SELECTED CLEAR CONTEXT IN EcgViewController.xib
CGContextSetStrokeColorWithColor (context, // Set color of 'pen'
[UIColor blackColor].CGColor); // Black to erase old plot
CGContextMoveToPoint (context, // Update current position to specified point
ECGX[x], // Starting X-coordinate of prior position
(HH - ECGS[x])); // Starting Y-corrdinate with height bias
CGContextAddLineToPoint (context, // Draw line from current position
ECGX[(x + 1)], // Ending X-coordinate
(HH - ECGS[((x + 1))])); // Ending Y-coordinate using saved Y-axis (with height bias)
CGContextClosePath (context); // Close current path
CGContextStrokePath (context); // Stroke current path (paint the path)
#endif // NOT NEEDED CUZ WE SELECTED CLEAR CONTEXT IN EcgViewController.xib
/*==============================================================================*/
/* Plot the next ECG A/D plot value. */
/*==============================================================================*/
CGContextSetStrokeColorWithColor (context, // Set color of 'pen'
[UIColor greenColor].CGColor); // White to draw new plot
CGContextMoveToPoint (context, // Update current position to specified point
ECGX[x], // Starting X-coordinate of new position
(HH - ECGY[y])); // Starting Y-coordinate with height bias
CGContextAddLineToPoint (context, // Draw line & prevent overrun
ECGX[(x + 1)], // Ending X-coordinate
(HH - ECGY[((y + 1) % 127)])); // Ending Y-axis (with height bias)
CGContextClosePath (context); // Close current path
CGContextStrokePath (context); // Stroke current path (paint the path)
ECGS[x] = ECGY[y]; // Save last plot value for erase
x++; // Next ECGX[] (y-axis) plot value index
/*==============================================================================*/
/* Below as we increment 'y' it will eventually roll to zero and when we get */
/* to the end of the above 'for' loop 'y' will have its starting value. */
/*==============================================================================*/
y = ((y + 1) % 127); // Next ECGY[] (y-axis) plot value & prevent overrun
ulPlots++; // Count number of plots
} // end - for (i = 0; i < 127; i++)
y = ((y + 16) % 127); // Next starting y-axis 'ECGY' index
YY = y; // Save it for next iteration
EcgCount = 0; // Reset skip count (inc. by 'CommThread'
} // end - if (fEcgErase == YES)
// UIGraphicsPopContext();
fEcgDraw = NO; // Show 'drawRect' not running
// [NSThread sleepForTimeInterval : 0.1]; // Delay a little
} // end - 'drawRect'
/*===============================END OF FUNCTION================================*/
Keep in mind that drawing only ever happens on the main thread, which means that while drawRect:
is running, your entire UI blocks. If this drawing code is slow, then your application will be nonresponsive while the drawing is taking place. Based on the size of the code and amount of individual drawing operations you're doing in this method, that seems to be what's happening here.
drawRect:
gets its name from the fact that you're supposed to limit your drawing to the area described by rect
. It's not 100% clear from your question if the entire graph changes every time, or if only a little bit of new data is being added. If there's a way you can restructure your code so that the entire thing doesn't need to be redrawn every time, that will almost certainly eliminate your jerkiness problems.
For example, you could also avoid drawing the gridlines on each refresh by putting them in a separate view behind the view that contains the plot. The gridlines (I'm guessing) don't need to be redrawn very often, so if you make your plot view transparent, UIKit will composite them for you and you can avoid a drawing operation. Probably a small savings, but anything helps. That would also mean that you can erase your view by filling it with the [UIColor clearColor]
. Drawing over the old plot with the background color is an extremely expensive operation, but rectangle fills are dirt cheap. It's less code and it runs faster.
If that's not enough, you could also do your drawing into a separate UIImage
offscreen, and then simply replace the contents of your view with this image. This would give you better performance because you would be able to do the image drawing (which is the expensive part) in a separate thread, and so the drawing operation wouldn't block the main application thread, which will eliminate the jerkiness.
I had a similar problem where I wanted to rotate an object through a specific number of radians based on a touch gesture. The rotation was jittery. I solved the problem by using CADisplayLink to synchronize the updates with the run loop:
@property (nonatomic, strong) CADisplayLink *displayLink;
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(rotate)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
Every time the run loop updated the UI, my 'rotate' method was called. The rotation was smooth and I didn't notice any performance hits.
This was for an affine transformation that is not a very expensive operation. I suppose you could put setNeedsDisplay in a method that is called by a CADisplayLink object but that would likely be an expensive operation. Still, maybe worth a try.
精彩评论