How to create handwritten annotations for Xuni iOS FlexChart
Several years back I remember talking to many other iOS users about popular and practical apps. I remember one of the most popular apps was a PDF annotation app that allowed you to input your own free hand annotations. Since then I’ve always considered that, on some level, people still want to have the ability to interact with digital data in much the same way they interact with printed materials. Writing your own free hand notes and annotations is sometimes the most natural way to interact with data. In this blog we’ll examine how you can add the ability to write your own free hand notes and annotations to a FlexChart.
The App Setup
The easiest way to handle FlexChart annotations is to provide two separate ViewControllers. The initial ViewController simply handles generating the FlexChart and normal chart interactions, while the second will handle the user's handwritten annotations. The most notable part of our first ViewController is that we’ll need to capture a snapshot image of the chart and pass it to the second. We can generate the chart snapshot with the GetImage method. Once we’ve done that, we can place the image data into a UIImage and pass it along to the second ViewController.
-(void)annotateButtonClicked{
ImageAnnotateController *imageAnnnotateController = [[ImageAnnotateController alloc] init];
imageAnnnotateController.chartImage = [UIImage imageWithData:[chart getImage]];
[self.navigationController pushViewController:imageAnnnotateController animated:true];
}
This requires that we also create an image property on the second ViewController.
@interface ImageAnnotateController : ViewController
@property UIImage *chartImage;
@end
iOS handwritten annotations
Our approach to free-hand chart annotations is simple: use two UIImageViews (one with of an image of the chart and another for the overlaid drawing) and merge the annotations from the drawing overlay onto the chart image as the user completes drawing their shapes. The end result will provide natural looking notes: The first step is to create some private variables. We’ll separate these into four distinct groups: a variable that holds the last point the user touched, internal parameters that configure the brush stroke used for drawing the annotations, buttons to switch drawing styles, and UIImageViews that hold the chart we’re drawing over and the overlaid annotation.
@interface ImageAnnotateController (){
CGPoint lastPoint;
CGFloat red;
CGFloat green;
CGFloat blue;
CGFloat brush;
CGFloat opacity;
UIButton *highlightButton;
UIButton *redPenButton;
UIButton *blackPenButton;
UIButton *clearButton;
UIBarButtonItem *shareButton;
UIImageView *annotationsImageView;
UIImageView *chartImageView;
}
Drawing the user input
We need the last point so that we can always connect the current point the user is touching to the previous point (which will keep the line continuous). Touch events can be responded to using a series of methods from the UIResponder class. First, we’ll use the touchesBegan method to capture the initial point that’s been touched by the user.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
lastPoint = [touch locationInView:self.view];
}
As the user moves their touch input across the screen, we’ll use the touchesMoved method to draw a line onto the annotationsImageView. We’ll be using a number of CGContext methods for line drawing in this method.* CGContextMoveToPoint and CGContextAddLine to handle creating a line path that connects the last point to the current one.
- CGContextSetLineCap, CGContextSetLineWidth, and CGContextSetRGBStrokeColor style the line that we’re drawing. While setting line width and stroke color are self-explanatory, line cap may be less familiar. It dictates the shape of the line that we draw (in this case a rounded shape) which should mimic the circular nature of a pen point.
- The CGContextBlendMode sets the way the current context will be drawn over the background image. In this case we’re just drawing over top of background without any kind of special blending.
- Finally, CGContextStrokePath draws the path to the current ImageContext which is then assigned to the annotationsImageView.
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self.view];
UIGraphicsBeginImageContext(self.view.frame.size);
[annotationsImageView.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), lastPoint.x, lastPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), currentPoint.x, currentPoint.y);
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), brush );
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), red, green, blue, 1.0);
CGContextSetBlendMode(UIGraphicsGetCurrentContext(),kCGBlendModeNormal);
CGContextStrokePath(UIGraphicsGetCurrentContext());
annotationsImageView.image = UIGraphicsGetImageFromCurrentImageContext();
[annotationsImageView setAlpha:opacity];
UIGraphicsEndImageContext();
lastPoint = currentPoint;
}
The last event that we’ll need to handle is when user input ends using the touchesEnded method. In this method we’ll merge the chartImageView and annotationsImageView together. After this is done we can assign the resulting image back to the chartImageView and clear the annotationsImageView.
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UIGraphicsBeginImageContext(chartImageView.frame.size);
[chartImageView.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height - 30) blendMode:kCGBlendModeNormal alpha:1.0];
[annotationsImageView.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height - 30) blendMode:kCGBlendModeNormal alpha:opacity];
chartImageView.image = UIGraphicsGetImageFromCurrentImageContext();
annotationsImageView.image = nil;
UIGraphicsEndImageContext();
}
Styling the lines
The buttons that we added mostly deal with providing different types of styling for the lines that we’re drawing so that we can provide options for a highlighter, red pen, and black pen. The highlighter has a wider brush stroke, yellow color, and low opacity. The pens on the other hand have a thinner stroke and are fully opaque.
-(void)highlightButtonClicked{
red = 255.0/255.0;
green = 255.0/255.0;
blue = 0.0/255.0;
brush = 25.0;
opacity = .3;
}
-(void)redPenButtonClicked{
red = 255.0/255.0;
green = 0.0/255.0;
blue = 0.0/255.0;
brush = 5.0;
opacity = 1.0;
}
-(void)blackPenButtonClicked{
red = 0.0/255.0;
green = 0.0/255.0;
blue = 0.0/255.0;
brush = 5.0;
opacity = 1.0;
}
We'll also add a clear button that performs the action of clearing the annotationImageView and resetting the chartImageView to it’s initial state.
-(void)clearButtonClicked{
annotationsImageView.image = nil;
[chartImageView setImage:self.chartImage];
}
A note on sharing
I won’t go into full details on the sharing implementation here, but you can easily provide a share button for your annotated chart (see our previous blog for an implementation). This will allow you users to easily save, print, or share the annotated image.
Summing things up
Handwritten annotations provide your users an easy and novel way of interacting with their data through bridging a gap between analog and digital interactions. This is a very simple implementation, but it gives you a rough blueprint for how you can provide this feature. All Xuni controls provide a GetImage method too, so you can easily substitute a different Xuni control for the FlexChart that we've used here.