DrawingIniOS

From HerzbubeWiki
Jump to navigation Jump to search

This page contains details about how to draw custom content on iOS. This involves various technologies / frameworks, such as UIKit, CoreGraphics, CoreAnimation, etc. The foundation for understanding how to


Glossary

Quartz
More a (fuzzy) marketing name than a real technology. The following description can be found in the "Drawing and Printing Guide for iOS" [1]: "Quartz is the general name for the native window server and drawing technology in iOS. The Core Graphics framework is at the heart of Quartz, and is the primary interface you use for drawing content."
OpenGL ES
OpenGL for Embedded Systems. OpenGL itself stands for "Open Graphics Library".
Native drawing technologies
Everything that is not OpenGL ES.
Core Graphics, CG
Function-oriented C-based API for drawing. Core Graphics is a framework (CoreGraphics.framework), the main header is <CoreGraphics/CoreGraphics.h>. Importing this header is usually not necessary if UIKit is also used, since most UIKit header files already include the CoreGraphics main header.
Quartz 2D
An API that is used for 2-dimensional drawing. Quartz 2D is a subset of the Core Graphics API. Because of this, Quartz 2D functions use the CG prefix. It is not clear which Core Graphics functions and/or data types are not part of Quartz 2D.
UIKit
Object-oriented Objective-C API for drawing. UIKit provides wrappers for some Core Graphics structures (e.g. UIColor instead of CGColor), and in all cases uses Core Graphics as the backend for drawing. UIKit is a framework (UIKit.framework), the main header is <UIKit/UIKit.h>.
Core Animation, CA
Core Animation is an Objective-C API for creating animations. Core Animation does not provide primitive drawing routines for creating shapes, images, or other types of content. Instead, it is a technology for manipulating and displaying content that were created using other technologies. For this reason, one sometimes encounters Core Animation through other technologies' APIs (e.g. block animation methods in UIView). When Core Animation functionality occurs as distinct separate classes or functions, though, it is located in the QuartzCore framework (e.g. CALayer class).
QuartzCore
A framework (QuartzCore.framework), the main header is <QuartzCore/QuartzCore.h. QuartzCore is a collection of other technologies. On iOS it mostly consists of Core Animation stuff, but on the Mac it also contains Core Image.
Graphics Context
Drawing operations operate on a graphics context. A graphics context defines the destination for drawing operations. The opaque data type CGContextRef is used to represent a graphics context. There are the following types of graphics contexts: Bitmap, PDF, Window, Layer and (in Mac OS X only) PostScript. To obtain a graphics context of the desired type
Coordinate system
The graphics context defines which coordinate system is used. For instance, the default coordinate system in Core Graphics (which for instance is used for PDF or bitmap graphics contexts) has its origin in the lower-left corner, whereas a window graphics context provided by UIKit (UIGraphicsGetCurrentContext) has a coordinate system with the origin set in the upper left corner. UIKit achieves this switch simply by modifying the CTM.
CTM
Current Transformation Matrix. A mathematical matrix that maps points in a graphics context's coordinate system to points on the graphics context's drawing destination.
Path
A path is a description of a 2D geometric scene that uses a sequence of lines and Bézier curves to represent that scene. Both Core Graphics and UIKit provide functions for creating paths.
Identity transform
An affine transformation that does not change its operand. The result of applying the identity transform to a coordinate pair is the same identical coordinate pair.


Resources

  • Drawing and Printing Guide for iOS [1]
  • Quartz 2D Programming Guide [2]
  • CGContext Reference [3]
  • CGeometry Reference [4]
  • Understanding UIKit Rendering (WWDC 2011 video) [5]
  • Optimizing 2D Graphics and Animation Performance (WWDC 2012 video) [6]


General Notes

  • Drawing must take place in the main thread, UIKit classes generally are not thread-safe
  • Regardless of how drawing is done (OpenGL, Quartz, etc.), it always happens inside a UIView
  • Drawing operations use points and a logical coordinate system, these are possibly different from the physical pixels displayed on the screen; the pixel-to-point ration is exposed by
    • UIView's contentScaleFactor property
    • CALayer's contentsScale property
    • For instance, a scale factor of 2.0 means that 1 point equals 2 pixels


Understanding UIKit Rendering

This section is a summary of the WWDC 2011 video "Understanding UIKit Rendering" [5].

Coordinate systems

UIKit vs. CoreAnimation

  • UIKit coordinate systems always have their origin in the top-left corner and their x/y axis pointing to the right and downwards. This is true for all devices.
  • CoreAnimation has different coordinate system s that depend on the device (e.g. iPhone devices have their origin in the top-left corner, the original iPad has its origin in the top-right corner, while later iPad devices apparently have their origin in the bottom-left corner)
  • Units in a UIKit coordinate systems are points, not pixels
  • Units in CoreAnimation coordinate systems are pixels
  • The recommendation is to not use CoreAnimation coordinate systems because UIKit coordinate systems are much more consistent


UIScreen

  • The UIScreen coordinate system is a UIKit coordinate system
  • The UIScreen coordinate system has its origin in the top-left corner of the screen when the device is held in portrait orientation. The x/y axis are pointing to the right and downwards.
  • The UIScreen coordinate system is fixed to the physical device screen, so when the device is rotated, the coordinate system origin and the direction of the axis' also rotate! For instance, if the device is rotated to the left into landscape orientation, the origin is now in the bottom-left corner of the screen and the x/y axis are pointing upwards and to the right.


UIView

  • Every UIView has its own coordinate system with an origin at 0/0
  • UIView coordinate systems are UIKit coordinate systems
  • UIView coordinate systems take device orientation into account, i.e. the coordinate system origin is in the top-left corner of the screen as it is currently held by the user! TODO: Is this true?


UIView location and size

Location

  • The location of a UIVIew on the screen is determined by its center point
  • The center point of a UIVIew is expressed in the coordinate system of its superview
  • TODO: Can the center of a UIVIew be at a coordinate with a fractional value (when the view size is an odd number)?


Size

  • The size of a UIView is determined by the size of its bounds (see below) plus any transforms


Bounds

  • The bounds of a UIView is a rectangle with an origin and a size
  • The bounds rectangle is expressed in the UIView's own coordinate system
  • The bounds rectangle defines the visible region of the UIVIew's content. This is important only if the UIView's content size is larger than the UIView's bound size.
  • The bounds rectangle is expressed before transforms are taken into account to calculate the frame.


Frame

  • The frame rectangle is the smallest rectangle that fully encloses the UIView in its superview's coordinate system. This definition is important because of transforms.
  • Although UIView and CALayer have a frame property, the value of that property is not stored somewhere, it is computed from a number of properties listed in the table further down
  • Most important to know here is that transforms contribute to the frame computation, so the frame rectangle size and the bounds rectangle size are not necessarily the same!
  • The frame rectangle is expressed in the coordinate system of its superview
  • This means that changing the frame will change some of the underlying properties that it is computed from. The underlying properties that are changed are
    • The center point of the view
    • The bounds size


The following table lists the components that are used to compute the frame of a view or layer. The table is presented at 13:20 of the video.

UIView Property CALayer Property Coordinate Space Remarks
bounds.size bounds.size View The bounds determines the layer's size. Setting the view's bounds also sets the layer's bounds.
center position Superview Setting the view's center also sets the layer's position.
transform affineTransform Superview This property has the type CGAffineTransform. Not sure what happens if the view's transform is set
- transform Superlayer This property has the type CATransform3D.
- anchorPoint Layer (Unit Coordinates) Changes how the other properties are interpreted. Has something to do with the difference between "center" and "position". Is not explained in the WWDC video, they tell you not to worry about this property, or to read the documentation if you do need to know about it.


Transforms

  • Transforms are always applied about the center point of a UIView, not the origin of the frame! This is important to know when applying scale and rotation transforms. It is not important for translate transforms because those change both the frame origin and the center point for the same value.

TODO: How do the various transforms affect the frame? Scale = Makes the frame smaller/larger in relation to the bounds. Rotate = Rotates the frame around the center point. Translate = ?. Other transforms?


Putting everything together

As I currently understand it, this is the rough sequence of how a view's content is drawn on the screen:

  1. A view's content is put together. For instance
    • Subviews in a given layout
    • An image
    • Something else
  2. The visible region of the view content is determined by looking at the view's bounds
  3. Transforms are applied to the visible region, and the content within, resulting in a rectangle that has the size of the frame
  4. The resulting rectangle is placed within its superview at view's center point. TODO: Is this correct?

Additional notes:

  • Setting the UIView property clipsToBounds to YES prevents the view contents from "spilling" outside of the UIView's bounds


UIView and CALayer

  • The relationship between UIView and CALayer is "has-a", i.e. composition
  • The content displayed by a UIVIew is coming from the view.layer.contents property
    • This property should not be set directly - it is set by UIKit through a variety of other means
    • For instance, when a UIView's drawRect: returns, the bitmap painted inside that method is used to set the contents property of the view's layer
    • view.layer.contents has the type id; it may be a CGImageRef, but can also be something else (it's unclear what it is).


Optimizing 2D Graphics and Animation Performance

This section is a summary of the WWDC 2012 video "Optimizing 2D Graphics and Animation Performance" [6].

Retina Displays

  • 1 point != 1 pixel
  • The "scale factor" expresses how many pixels per point there are on a device
  • UIView: contentScaleFactor property
  • CALayer: contentsScale property
  • The propery for UIView objects should be set up correctly
  • The property for CALayer objects created by UIKit should be set up correctly
  • The property for CALayer objects that the developer creates must be set manually. The canonical way how to do this is this:
layer.contentsScale = [UIScreen mainScreen].scale;
  • The property must be set only if the view or layer content is high-resolution. For instance, if an image is still at 1x resolution, the property must not be set. The effect will be that the 1x resolution image is scaled up - although this will look ugly, the image will at least be rendered with the correct dimensions.
  • The CGContext provided in the view or layer's drawing method is already set up with the correct scale factor
  • Drawing into a CGContextBitmap: The context needs to be set up with pixel, not point dimensions, and the drawing must be scaled up appropriately.
  • Alternatively, use UIGraphicsBeginImageContextWithOptions to create a bitmap context. Specify 0 as the scale factor and UIKit will figure out the correct scale factor for the device screen. The context can thus be set up with point dimensions.


Core Animation Instrument

  • When the Core Animation instrument is running (only when it is running, not when it is stopped), a number of useful debugging options are available
  • "Flash Updated Regions" shows if more parts of the screen than necessary were updated
  • "Color Misaligned images" shows images that are not on pixel boundaries
  • "Color Offscreen-Rendered Yellow" shows if something has been rendered offscreen. Offscreen-rendering is slow and should be avoided if possible. This is explained more in-depth in Understanding UIKit Rendering.
  • "Color Hits Green and Misses Red" shows if offscreen-rendered content could be re-used from a cache (green) or not (red)
  • "Color Blended Layers" shows areas that are NOT opaque. This is important because opaque views can be rendered much more quickly.


Optimizing drawing

  • Instead of invoking setNeedsDisplay, you can invoke setNeedsDisplayInRect:
  • This invalidates only a particular rectangle instead of the entire view or layer.
  • On the next call to the drawing method of a UIView or CALayer, the graphics context will be automatically set up with a clipping rectangle
  • The implementation of the drawing method does not need to do anything special, any drawing made outside of the clipping rectangle will simply be discarded


Drawing with CoreGraphics

Drawing basics

  • The origin of the CoreGraphics coordinate system is at the bottom-left corner
  • In order to implement custom drawing, one needs to override either UIView's drawRect: or CALayer's drawLayer:inContext: methods
  • Before either of these methods is invoked by the system, a graphics context (CGContextRef) is created that must be used the subsequent drawing operation
    • CALayer passes the graphics context to drawLayer:inContext:
    • UIView does not pass the graphics context, one needs to obtain it like this:
CGContextRef myContext = UIGraphicsGetCurrentContext();
  • The graphics context may already be set up to account for certain view or layer properties (e.g. the scale factor, if one is set)
  • The current transformation matrix (CTM) can be used to add scaling, rotation, and translation factors to the actual drawing operations


More on graphics contexts

  • Contexts can be saved and restored using the functions UIGraphicsPushContext() and UIGraphicsPopContext()
  • CGBitmapContextCreate or CGPDFContextCreate create custom graphics contexts. Important: The coordinate system in these contexts is different from the view's coordinate system - the origin is in the bottom-left corner. This must be taken into account when the content of the context is rendered in a view
  • A bitmap context draws into a memory buffer; a PDF context records drawing commands and writes them to a file or sends them to a data consumer
  • An image can be created from a bitmap context by invoking CGBitmapContextCreateImage(). This results in a CGImageRef that can then be drawn onto the view using CGContextDrawImage(); the function must be supplied with the view's graphics context and the bitmap object
  • See the section Creating a Bitmap Graphics Context in the Quartz 2D Programming Guide.


Types

  • Coordinates use floating point values
  • Sometimes CGPoint is used to refer to a x/y coordinate
  • Sometimes CGRect is used to refer to a rectangle (x/y coordinate of the lower-left corner, together with a width and height)


Creating a path

  • This seems to work like Turtle Graphics :-)
  • Creating a path does not draw it!
  • Some path functions use the current point to form the requested path
  • CGContextBeginPath() starts a path; if a previous path exists in the context, it is discarded
  • CGContextMoveToPoint(context, 17.0, 42.0) sets the current point; after a path is started, this is required to establish the path's initial starting point
  • The path can now be extended with lines, arcs, curves and rectangles
  • CGContextClosePath closes the current sub-path by connecting the current point to the starting point; calling this function is optional
  • CGContextAddLineToPoint(context, 17.0, 42.0) draws a line from the current point to the specified end point; the end point becomes the new current point
  • CGContextAddLines(context, arrayOfCGPoint, count) draws a series of lines, starting from the current point
  • CGContextAddArc(context, xCenter, yCenter, radius, startAngle, endAngle, clockWise) draws an arc that is a segment of a circle; angles are measured in radians.
  • CGContextAddArcToPoint() is an alternative way to paint an arc, using 2 tangents
  • CGContextAddCurveToPoint() draws a cubic Bézier curve from the current point, using specified control points, to the specified end point
  • CGContextAddQuadCurveToPoint() draws a quadratic Bézier curve from the current point to the specified end point, using the specified control point
  • CGContextAddEllipseInRect() draws an ellipse that is bounded by the specified rectangle; the ellipse is approximated using a sequence of Bézier curves
  • CGContextAddRect() draws a rectangle using the specified CGRect; The current point has no influence on the rectangle.
    • Important: This function uses CGRectGetMaxX() and CGRectGetMaxY() to calculate the points that form the path. As a result, the rectangle that is drawn is 1 pixel wider/higher than the width/height of the specified CGRect (if a line width of 1 is used to stroke the path)
    • Example: The CGRect specified has an origin x-cordinate of 0, and a width of 12. CGRectGetMinX() therefore returns 0, and CGRectGetMaxX() returns 12. As a result, CGContextAddRect() will draw horizontal lines that go from a point at x-coordinate 0 to a point at x-coordinate 12 - which means a line that is 13 pixels long!
  • When a path is painted, it is flushed from the graphics context. CGPathRef and CGMutablePathRef are used to create reusable paths; the functions above have duplicates that operate on a CGPath instead of a graphics context. CGContextAddPath is used to apply a CGPath object to a graphics context
  • UIBezierPath is an Objective-C wrapper for some of the path-related functions above


Painting a path

  • Stroking or filling, or both, is used to paint a path
  • Stroking paints the line, filling paints the area inside the path
  • Stroke and fill attributes are properties of the graphics context
  • Normally a paint function uses the current path
  • There are also convenience functions that allow to specify a path to be painted, together with the actual paint operation. For instance, CGContextFillRect immediately paints a filled rectangle


Affine Transformations

References

  • The section "Transforms" in the "Quartz 2D Programming Guide" [7]
  • CGAffineTransform Reference [7]
  • CALayer Class Reference [7]


Basics

Affine transformations allow the user to express the following geometric changes that should be applied to drawing operations:

  • Translation
  • Scaling
  • Rotation

Affine transformations can be applied to...

  • Core Graphics drawing operations. This is done by changing the CTM (current transformation matrix) of a drawing context.
  • UIView and CALayer. This is done by setting the transform property of a view or layer.

The mathematical background for affine transformations are matrix multiplications.


Origin

  • The origin for the CTM (i.e. when doing Core Graphics drawing) is in the lower-left corner.
  • The origin for transforms applied to views and layers by default is the center of the view or layer.
    • This can be changed by modifying a CALayer's anchorPoint property
    • Despite its name and type (CGPoint), this property does not express a coordinate
    • Instead it is a kind of multiplication factor
    • The default value which represents the center of the view is 0.5 / 0.5
    • The anchor point value 0.0 / 0.0 places the origin for transformations in the upper-left corner of the view
    • The anchor point is expressed in relation to the view/layer bounds


Ordering of multiple transforms

Due to the nature of matrix multiplications (they are not commutative), the order in which transformations are applied is usually important. It's the same as with subtraction or division: If you switch the operands the result of the operation will usually change (e.g. 2/3 is not the same as 3/2).


For instance:

  • Translate followed by scale affects the translation distance, while scale followed by translate does not
  • Scale followed by rotate results in skewing (aka shearing), while rotate followed by scale does not


The function to combine two transforms into one is called

CGAffineTransformConcat


The natural way how to combine a translation with a scale or rotation transform is to first do the scale or rotation, then do the translation. Doing it this way will not affect the translation distance. The following two functions (source) help with this task because you don't have to think about the order of the transforms, you simply specify the values:

CGAffineTransform CGAffineTransformMakeScaleTranslate(CGFloat sx, CGFloat sy, CGFloat dx, CGFloat dy)
{
    return CGAffineTransformMake(sx, 0.f, 0.f, sy, dx, dy);
}

CGAffineTransform CGAffineTransformMakeRotateTranslate(CGFloat angle, CGFloat dx, CGFloat dy)
{
    return CGAffineTransformMake(cosf(angle), sinf(angle), -sinf(angle), cosf(angle), dx, dy);
}


Identity transform

The so-called "identity transform" is a transformation that does not change its operand. The result of applying the identity transform to a coordinate pair is the same identical coordinate pair. The identity transform is referred to with the constant

CGAffineTransformIdentity


Code snippets

Create a rectangular clipping path:

CGContextSaveGState(context);

CGRect clippingRect = CGRectMake(10, 10, 100, 20);
CGPathRef clippingPath = CGPathCreateWithRect(clippingRect, NULL);
CGContextAddPath(context, clippingPath);
CGContextClip(context);

// do some drawing that will be clipped, e.g. a gradient

CGContextRestoreGState(context);
CGPathRelease(clippingPath);


Draw a gradient (more on gradients)

const size_t numberOfLocations = 2;
const int numberOfComponentsPerLocation = 4;
CGFloat locations[numberOfLocations] = { 0.0, 1.0 };
CGFloat components[numberOfLocations * numberOfComponentsPerLocation] =
{
  1.0, 0.5, 0.4, 1.0,  // Start color
  0.8, 0.8, 0.3, 1.0   // End color
};
CGColorSpaceRef aColorspace = CGColorSpaceCreateDeviceRGB();
CGGradientRef aGradient = CGGradientCreateWithColorComponents (aColorspace, components, locations, numberOfLocations);
CGPoint gradientStartPoint = CGPointMake(0, 0);
CGPoint gradientEndPoint = CGPointMake(100, 0);
CGContextDrawLinearGradient(context, aGradient, gradientStartPoint, gradientEndPoint, 0);
CGColorSpaceRelease(aColorspace);
CGGradientRelease(aGradient);


References

  1. 1.0 1.1 Drawing and Printing Guide for iOS
  2. Quartz 2D Programming Guide
  3. CGContext Reference (global functions to draw in a CoreGraphics context)
  4. CGeometry Reference (structure for geometric operations and associated functions; e.g. CGPoint, CGSize, CGRect)
  5. 5.0 5.1 Understanding UIKit Rendering (WWDC 2011 video)
  6. 6.0 6.1 Optimizing 2D Graphics and Animation Performance (WWDC 2012 video)
  7. 7.0 7.1 7.2 Section "Transforms" in the "Quartz 2D Programming Guide" Cite error: Invalid <ref> tag; name "transforms-quartz-2d" defined multiple times with different content Cite error: Invalid <ref> tag; name "transforms-quartz-2d" defined multiple times with different content