iOS_41_绘图

最终效果图:



















下面两张,是画文字时,用到的字典


最终效果图:

storyboard截图:

控制器代码:
//
//  BasicShapeController.m
//  41_绘图
//
//  Created by beyond on 14-9-21.
//  Copyright (c) 2014年 com.beyond. All rights reserved.
//

#import "BasicShapeController.h"
// 导入自定义控件
#import "MyLabel.h"
#import "MyImg.h"
#import "YellowDraft.h"
#import "Yellow.h"
#import "ContextStack.h"
#import "Matrix.h"
#import "Clip.h"
#import "Snow.h"
#import "ReDraw.h"

// 画布的矩形区域,专门为自定义View的,drawRect方法,准备的
#define kRectForDraw  CGRectMake(0, 64, 320, 342)
@interface BasicShapeController ()

- (IBAction)drawLabel;
- (IBAction)drawImage;
- (IBAction)dismiss;
// 小黄人草稿(未完成)
- (IBAction)drawYellowDraft;
- (IBAction)drawYellow;
// 演示 上下文栈 的保存和恢复
- (IBAction)contextStackDemo;
// 演示矩阵变换
- (IBAction)showMartix;
// 演示裁剪
- (IBAction)clipDemo;
// 演示雪花飘落
- (IBAction)snowDemo;
// 演示重绘,setNeedsDisplay
- (IBAction)reDrawDemo;
@property (nonatomic,weak) ReDraw *redrawView;
@end

@implementation BasicShapeController
- (IBAction)dismiss
{
    [self dismissViewControllerAnimated:YES completion:nil];
}

#pragma mark - 演示代码
// 实例化一个,自定义的Label,其内部会覆写drawRect方法


- (IBAction)drawLabel
{
    MyLabel *view = [[MyLabel alloc]init];
//    view.frame = CGRectMake(100, 60, 150, 150);
    view.backgroundColor = [UIColor grayColor];
    view.frame = kRectForDraw;
    [self.view addSubview:view];
    
}
// 实例化一个,自定义的Label,其内部会覆写drawRect方法
- (IBAction)drawImage
{
    MyImg *view = [[MyImg alloc]init];
    view.backgroundColor = [UIColor grayColor];
//    view.frame = CGRectMake(100, 60, 150, 350);
    view.frame = kRectForDraw;
    [self.view addSubview:view];
}



- (IBAction)drawYellowDraft
{
    // 小黄人 草稿
    YellowDraft *view = [[YellowDraft alloc]init];
    view.frame = self.view.frame;
    
    view.frame = kRectForDraw;
    view.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:view];
}

- (IBAction)drawYellow
{
    // 小黄人
    Yellow *view = [[Yellow alloc]init];
    view.frame = kRectForDraw;
    view.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:view];
}
// 演示 上下文栈 的保存和恢复
- (IBAction)contextStackDemo
{
    ContextStack *view = [[ContextStack alloc]init];
    view.backgroundColor = [UIColor grayColor];
    view.frame = kRectForDraw;
    [self.view addSubview:view];
}
// 演示矩阵变换
- (IBAction)showMartix
{
    Matrix *view = [[Matrix alloc]init];
    view.backgroundColor = [UIColor grayColor];
    view.frame = kRectForDraw;
    [self.view addSubview:view];
}
// 演示裁剪
- (IBAction)clipDemo
{
    Clip *view = [[Clip alloc]init];
    view.backgroundColor = [UIColor grayColor];
    view.frame = kRectForDraw;
    [self.view addSubview:view];
}
// 演示雪花飘落
- (IBAction)snowDemo
{
    Snow *view = [[Snow alloc]init];
    view.backgroundColor = [UIColor grayColor];
    view.frame = kRectForDraw;
    [self.view addSubview:view];
}
// 演示重绘,setNeedsDisplay
- (IBAction)reDrawDemo
{
    //
    ReDraw *view = [[ReDraw alloc]init];
    view.backgroundColor = [UIColor grayColor];
    view.frame = kRectForDraw;
    [self.view addSubview:view];
    // 添加一个slider
    UISlider *slider = [[UISlider alloc]init];
    slider.frame = CGRectMake(0, 300, 200, 42);
    // 圆的半径范围
    slider.minimumValue = 10;
    slider.maximumValue = 50;
    // 监听拖动
    [slider addTarget:self action:@selector(radiusChange:) forControlEvents:UIControlEventValueChanged];
    [self.view addSubview:slider];
    
    //3.用成员变量记住,因为要设置圆的半径
    self.redrawView = view;
}
- (void)radiusChange:(UISlider *)slider
{
    CGFloat radius = slider.value;
    // 内部会拦截setter方法,调用setNeedsDisplay方法
    [self.redrawView setRadius:radius];
}
@end


自定义View,画基本图形
//
//  Shape.m
//  41_绘图
//
//  Created by beyond on 14-9-21.
//  Copyright (c) 2014年 com.beyond. All rights reserved.
//  画形状

#import "Shape.h"

@implementation Shape



// 在view第一次显示到屏幕上的时候会调用一次
// 其他时候,只有在setNeedsDisplay方法,才调用
- (void)drawRect:(CGRect)rect
{
    
}

void drawSomething()
{
    // 1.无论做啥,首先取得上下文
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    
    // 2.画1/4圆
    CGContextMoveToPoint(ctx, 100, 100);
    CGContextAddLineToPoint(ctx, 100, 150);
    // 圆心:x,y
    // 半径:50
    // 角度:从 -90  到 180,注意 x轴向右为正方向,x的下方为-,x上方为+
    // 1表示,顺时针
    // 综上所述,就是画一个四分之一圆,且是左下角的四分之一
    CGContextAddArc(ctx, 100, 100, 50, -M_PI_2, M_PI, 1);
    CGContextClosePath(ctx);
    
    [[UIColor redColor] set];
    
    // 3.最后,闭合路径,显示所绘制的东西
    CGContextFillPath(ctx);
}
/**
 *  画圆弧
 */
void drawArc()
{
    // 1.无论做啥,首先取得上下文
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    
    // 2.画圆弧
    // x\y : 圆心
    // radius : 半径
    // startAngle : 开始角度
    // endAngle : 结束角度
    // clockwise : 圆弧的伸展方向(0:顺时针, 1:逆时针)
    CGContextAddArc(ctx, 100, 100, 50, M_PI_2, M_PI, 0);
    
    
    // 3.最后,闭合路径,显示所绘制的东西
    CGContextFillPath(ctx);
}

/**
 *  画圆
 */
void drawCircle()
{
    // 1.无论做啥,首先取得上下文
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    
    // 2.画圆
    CGContextAddEllipseInRect(ctx, CGRectMake(50, 10, 100, 100));
    
    CGContextSetLineWidth(ctx, 10);
    
    // 3.最后,闭合路径,显示所绘制的东西
    CGContextStrokePath(ctx);
}
/**
 *  画线段
 */
void drawLine()
{

    // 1.获得图形上下文
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    
    // 2.拼接图形(路径)
    // 设置线段宽度
    CGContextSetLineWidth(ctx, 10);
    
    // 设置线段头尾部的样式
    CGContextSetLineCap(ctx, kCGLineCapRound);
    
    // 设置线段转折点的样式
    CGContextSetLineJoin(ctx, kCGLineJoinRound);
    
    /**  第1根线段  **/
    // 设置颜色
    CGContextSetRGBStrokeColor(ctx, 1, 0, 0, 1);
    // 设置一个起点
    CGContextMoveToPoint(ctx, 10, 10);
    // 添加一条线段到(100, 100)
    CGContextAddLineToPoint(ctx, 100, 100);
    
    // 渲染一次
    CGContextStrokePath(ctx);
    
    
    /**  第2根线段 设置新的上下文对象,画另一个样式的线段**/
    // 设置颜色
    CGContextSetRGBStrokeColor(ctx, 0, 0, 1, 1);
    // 设置一个起点
    CGContextMoveToPoint(ctx, 200, 190);
    // 添加一条线段到(150, 40)
    CGContextAddLineToPoint(ctx, 150, 40);
    CGContextAddLineToPoint(ctx, 120, 60);
    
    
    // 3.渲染显示到view上面
    CGContextStrokePath(ctx);
}
/**
 *  画四边形
 */
void draw4Rect()
{
    // 1.获得上下文
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    
    // 2.画矩形
    CGContextAddRect(ctx, CGRectMake(10, 10, 150, 100));
    
    // set : 同时设置为实心和空心颜色
    // setStroke : 设置空心颜色
    // setFill : 设置实心颜色
    [[UIColor whiteColor] set];
    
    //    CGContextSetRGBFillColor(ctx, 0, 0, 1, 1);
    
    // 3.绘制图形
    CGContextFillPath(ctx);
}

/**
 *  画三角形
 */
void drawTriangle()
{
    // 1.获得上下文
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    
    // 2.画三角形
    CGContextMoveToPoint(ctx, 0, 0);
    CGContextAddLineToPoint(ctx, 100, 100);
    CGContextAddLineToPoint(ctx, 150, 80);
    // 关闭路径(连接起点和最后一个点)
    CGContextClosePath(ctx);
    
    //
    CGContextSetRGBStrokeColor(ctx, 0, 1, 0, 1);
    
    // 3.绘制图形
    CGContextStrokePath(ctx);
}
/**
 *  画文字
 */
void drawLabel()
{
    // 1.无论做啥,首先取得上下文
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    // 2.画矩形
    CGRect cubeRect = CGRectMake(50, 50, 100, 100);
    CGContextAddRect(ctx, cubeRect);
    // 3.显示所绘制的东西
    CGContextFillPath(ctx);
    
    // 4.1  C 画文字
    
    
    // 4.2  OC 画文字
    NSString *str = @"空对着,山中高士晶莹雪;终不忘,世外仙姝寂寞林";
    //    [str drawAtPoint:CGPointZero withAttributes:nil];
    
    NSMutableDictionary *attrs = [NSMutableDictionary dictionary];
    // NSForegroundColorAttributeName : 文字颜色
    // NSFontAttributeName : 字体
    attrs[NSForegroundColorAttributeName] = [UIColor whiteColor];
    attrs[NSFontAttributeName] = [UIFont systemFontOfSize:18];
    [str drawInRect:cubeRect withAttributes:attrs];
}
/**
 *  画图片
 */
void drawImage()
{
    // 1.取得图片
    UIImage *image = [UIImage imageNamed:@"nanaLogo.jpg"];
    UIImage *pattern = [UIImage imageNamed:@"pattern.jpg"];
    // 2.画
    //    [image drawAtPoint:CGPointMake(50, 50)];
    //    [image drawInRect:CGRectMake(0, 0, 150, 150)];
    [image drawAsPatternInRect:CGRectMake(0, 0, 150, 150)];
    [pattern drawAsPatternInRect:CGRectMake(0, 150, 200, 200)];
    // 3.画文字
    NSString *str = @"为xxx所画";
    NSMutableDictionary *attrDict = [NSMutableDictionary dictionary];
    // NSForegroundColorAttributeName : 文字颜色
    attrDict[NSForegroundColorAttributeName] = [UIColor whiteColor];
    [str drawInRect:CGRectMake(0, 130, 100, 30) withAttributes:attrDict];
}
@end


画小黄人_1_草稿
//
//  YellowDraft.m
//  41_绘图
//
//  Created by beyond on 14-9-21.
//  Copyright (c) 2014年 com.beyond. All rights reserved.
//  小黄人_草稿_未完成

#import "YellowDraft.h"


#define kRadius 70
#define kTopY 100
#define kColor(r, g, b) [UIColor colorWithRed:(r)/255.0 green:(g)/255.0 blue:(b)/255.0 alpha:1.0]

@implementation YellowDraft

- (void)drawRect:(CGRect)rect
{
    // 1.无论做啥,首先取得上下文
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    
    // 2.身体
    drawBody(ctx, rect);
    
    // 3.嘴(微笑)
    drawZui(ctx, rect);
    
    // 3.画眼睛
    drawEyes(ctx, rect);
}

/**
 *  眼睛
 */
void drawEyes(CGContextRef ctx, CGRect rect)
{
    // 1.黑色绑带
    CGFloat startX = rect.size.width * 0.5 - kRadius;
    CGFloat startY = kTopY;
    CGContextMoveToPoint(ctx, startX, startY);
    CGFloat endX = startX + 2 * kRadius;
    CGFloat endY = startY;
    CGContextAddLineToPoint(ctx, endX, endY);
    CGContextSetLineWidth(ctx, 15);
    [[UIColor blackColor] set];
    // 绘制线条
    CGContextStrokePath(ctx);
    
    // 2.最外圈的镜框
    [kColor(61, 62, 66) set];
    CGFloat kuangRadius = kRadius * 0.4;
    CGFloat kuangY = startY;
    CGFloat kuangX = rect.size.width * 0.5 - kuangRadius;
    CGContextAddArc(ctx, kuangX, kuangY, kuangRadius, 0, M_PI * 2, 0);
    CGContextFillPath(ctx);
    
    // 3.里面的白色框
    [[UIColor whiteColor] set];
    CGFloat whiteRadius = kuangRadius * 0.7;
    CGFloat whiteX = kuangX;
    CGFloat whiteY = kuangY;
    CGContextAddArc(ctx, whiteX, whiteY, whiteRadius, 0, M_PI * 2, 0);
    CGContextFillPath(ctx);
}

/**
 *  画嘴
 */
void drawZui(CGContextRef ctx, CGRect rect)
{
    // 中间的控制点
    CGFloat controlX = rect.size.width * 0.5;
    CGFloat controlY = rect.size.height * 0.5;
    
    // 当前点
    CGFloat marginX = 20;
    CGFloat marginY = 10;
    CGFloat currentX = controlX - marginX;
    CGFloat currentY = controlY - marginY;
    CGContextMoveToPoint(ctx, currentX, currentY);
    
    // 结束点
    CGFloat endX = controlX + marginX;
    CGFloat endY = currentY;
    
    // 贝塞尔曲线
    CGContextAddQuadCurveToPoint(ctx, controlX, controlY, endX, endY);
    
    // 设置颜色
    [[UIColor blackColor] set];
    
    // 渲染
    CGContextStrokePath(ctx);
}

/**
 *  画身体
 */
void drawBody(CGContextRef ctx, CGRect rect)
{
    // 上半圆
    CGFloat topX = rect.size.width * 0.5;
    CGFloat topY = kTopY;
    CGFloat topRadius = kRadius;
    CGContextAddArc(ctx, topX, topY, topRadius, 0, M_PI, 1);
    
    // 向下延伸
    CGFloat middleX = topX - topRadius;
    CGFloat middleH = 100; // 中间身体的高度
    CGFloat middleY = topY + middleH;
    CGContextAddLineToPoint(ctx, middleX, middleY);
    
    // 下半圆
    CGFloat bottomX = topX;
    CGFloat bottomY = middleY;
    CGFloat bottomRadius = topRadius;
    CGContextAddArc(ctx, bottomX, bottomY, bottomRadius, M_PI, 0, 1);
    
    // 合并路径
    CGContextClosePath(ctx);
    
    // 设置颜色
    [kColor(252, 218, 0) set];
    
    // 利用填充方式画出之前的路径
    CGContextFillPath(ctx);
}

@end


画小黄人_2
//
//  Yellow.m
//  41_绘图
//
//  Created by beyond on 14-9-21.
//  Copyright (c) 2014年 com.beyond. All rights reserved.
//  小黄人

#import "Yellow.h"

@implementation Yellow

// rect 尺寸为全屏
- (void)drawRect:(CGRect)rect
{
    // 0.无论做啥,首先取得上下文
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    

    // 1.画身体
    [[UIColor colorWithRed:252/255.0 green:221/255.0 blue:12/255.0 alpha:1] set];
    CGFloat bodyW = 150;
    CGFloat topCenterX = rect.size.width * 0.5;
    CGFloat topCenterY = 150;
    CGFloat topRadius = bodyW * 0.5;
    CGContextAddArc(ctx, topCenterX, topCenterY, topRadius, 0, M_PI, 1);
    
    CGFloat middleH = 100;
    CGFloat middleLeftX = topCenterX - bodyW * 0.5;
    CGContextAddLineToPoint(ctx, middleLeftX, middleH);
    
    CGFloat bottomCenterX = topCenterX;
    CGFloat bottomCenterY = topCenterY + middleH;
    CGContextAddArc(ctx, bottomCenterX, bottomCenterY, topRadius, M_PI, 0, 1);
    CGContextFillPath(ctx);
    
    // 2.画眼睛
    [[UIColor blackColor] set];
    CGFloat margin = 2;
    CGFloat badgeX = middleLeftX - margin;
    CGFloat badgeY = topCenterY - 10;
    CGFloat badgeW = bodyW + margin * 2;
    CGFloat badgeH = 15;
    CGContextAddRect(ctx, CGRectMake(badgeX, badgeY, badgeW, badgeH));
    CGContextFillPath(ctx);
    
    [[UIColor colorWithRed:79/255.0 green:78/255.0 blue:83/255.0 alpha:1] set];
    CGFloat eyeW = bodyW * 0.46;
    CGFloat leftEyeX = middleLeftX + eyeW * 0.5 + 10;
    CGFloat leftEyeY = badgeY + badgeH * 0.5;
    drawCircle(ctx, eyeW * 0.5, leftEyeX, leftEyeY);
    
    CGFloat rightEyeX = rect.size.width - leftEyeX;
    CGFloat rightEyeY = badgeY + badgeH * 0.5;
    drawCircle(ctx, eyeW * 0.5, rightEyeX, rightEyeY);
    CGContextFillPath(ctx);
    
    [[UIColor whiteColor] set];
    CGFloat innerEyeW = eyeW * 0.7;
    drawCircle(ctx, innerEyeW * 0.5, leftEyeX, leftEyeY);
    drawCircle(ctx, innerEyeW * 0.5, rightEyeX, rightEyeY);
    CGContextFillPath(ctx);
    
    [[UIColor colorWithRed:99/255.0 green:29/255.0 blue:10/255.0 alpha:1] set];
    CGFloat leftBrownX = leftEyeX + 10;
    CGFloat rightBrownX = rightEyeX - 10;
    drawCircle(ctx, 10, leftBrownX, leftEyeY);
    drawCircle(ctx, 10, rightBrownX, leftEyeY);
    CGContextFillPath(ctx);
    
    [[UIColor blackColor] set];
    drawCircle(ctx, 4, leftBrownX, leftEyeY);
    drawCircle(ctx, 4, rightBrownX, leftEyeY);
    CGContextFillPath(ctx);
    
    [[UIColor whiteColor] set];
    drawCircle(ctx, 2, leftBrownX - 3, leftEyeY - 3);
    drawCircle(ctx, 2, rightBrownX - 3, leftEyeY - 3);
    CGContextFillPath(ctx);
    
    // 3.弧线
    [[UIColor blackColor] set];
    CGFloat centerY = topCenterY + middleH * 0.7;
    CGFloat cp1x = topCenterX - 30;
    CGFloat cp2x = rect.size.width - cp1x;
    CGFloat cpy = centerY - 15;
    CGContextMoveToPoint(ctx, cp1x, cpy);
    CGContextAddQuadCurveToPoint(ctx, topCenterX, centerY, cp2x, cpy);
    CGContextStrokePath(ctx);
    
    // 4.头发
    CGContextMoveToPoint(ctx, topCenterX, topCenterY - topRadius + 5);
    CGContextAddLineToPoint(ctx, topCenterX, topCenterX - topRadius - 30);
    
    
    CGContextMoveToPoint(ctx, topCenterX - 10, topCenterY - topRadius + 5);
    CGContextAddLineToPoint(ctx, topCenterX - 20, topCenterX - topRadius - 30);
    
    
    CGContextMoveToPoint(ctx, topCenterX + 10, topCenterY - topRadius + 5);
    CGContextAddLineToPoint(ctx, topCenterX + 20, topCenterX - topRadius - 30);
    
    
    
    CGContextMoveToPoint(ctx, topCenterX - 20, topCenterY - topRadius + 7);
    CGContextAddLineToPoint(ctx, topCenterX - 30, topCenterX - topRadius - 30);
    
    
    CGContextMoveToPoint(ctx, topCenterX + 20, topCenterY - topRadius + 7);
    CGContextAddLineToPoint(ctx, topCenterX + 30, topCenterX - topRadius - 30);
    CGContextStrokePath(ctx);
}

void drawCircle(CGContextRef ctx, int radius, CGFloat centerX, CGFloat centerY)
{
    CGContextAddArc(ctx, centerX, centerY, radius, M_PI * 2, 0, 0);
}


@end


图形上下文栈 演示
//
//  ContextStack.m
//  41_绘图
//
//  Created by beyond on 14-9-21.
//  Copyright (c) 2014年 com.beyond. All rights reserved.
//  上下文栈

#import "ContextStack.h"

@implementation ContextStack

- (void)drawRect:(CGRect)rect
{
    // 1.无论做啥,首先取得上下文
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    
    // 2.将ctx拷贝一份放到栈中
    // 作用是:将当前的默认的纯洁的干净的状态,全部保存起来
    CGContextSaveGState(ctx);
    
    // 3.设置个性化的绘图状态
    CGContextSetLineWidth(ctx, 10);
    [[UIColor redColor] set];
    CGContextSetLineCap(ctx, kCGLineCapRound);
    
    // 绘制 第1根粗红线
    CGContextMoveToPoint(ctx, 50, 50);
    CGContextAddLineToPoint(ctx, 120, 190);
    CGContextStrokePath(ctx);
    
    // 4.将栈顶的(默认的纯洁的干净的)上下文出栈,替换当前的上下文
    CGContextRestoreGState(ctx);
    
    
    // 5.绘制 第2根线 (默认的纯洁的干净的)样式
    CGContextMoveToPoint(ctx, 10, 70);
    CGContextAddLineToPoint(ctx, 220, 290);
    CGContextStrokePath(ctx);
    //    CGContextDrawPath(ctx, kCGPathStroke);
}

@end


图形上下文_矩阵变换
//
//  Matrix.m
//  41_绘图
//
//  Created by beyond on 14-9-21.
//  Copyright (c) 2014年 com.beyond. All rights reserved.
//  上下文中的矩阵变换

#import "Matrix.h"

@implementation Matrix

/**
 渐变色
 虚线
 pattern
 blend
 .....
 .....
 阴影
 */

- (void)drawRect:(CGRect)rect
{
    // 1.无论做啥,首先取得上下文
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    
    // 2.先保存默认的上下文参数
    CGContextSaveGState(ctx);
    // 3.设置个性的上下文参数
    // 旋转矩阵,正弧度,表示:顺时针旋转
    // 此时的上下文,已经逆时针转了 一定角度
    CGContextRotateCTM(ctx, -M_PI_4 * 0.1);
    // 此时的上下文,已经缩小了一半
    CGContextScaleCTM(ctx, 0.5, 0.5);
    // 此时的上下文,已经沿Y轴,向下走了150个距离
    CGContextTranslateCTM(ctx, 0, 150);
    // 画个矩形(实质上:画在了点:160,160,长宽也只有25,25,并且还是逆时针旋转的)
    CGContextAddRect(ctx, CGRectMake(10, 10, 50, 50));
    // 闭合路径
    CGContextStrokePath(ctx);
    // 4.谢天谢地,终于恢复默认的正常的上下文参数
    CGContextRestoreGState(ctx);
    // 5.使用默认的正常的 上下文参数,画的什么,就是什么,画在哪儿,就是哪儿
    // 画圆
    CGContextAddEllipseInRect(ctx, CGRectMake(100, 100, 100, 100));
    // 画线段
    CGContextMoveToPoint(ctx, 150, 100);
    CGContextAddLineToPoint(ctx, 150, 250);
    // 闭合路径
    CGContextStrokePath(ctx);
}

@end


图片裁剪 演示
//
//  Clip.m
//  41_绘图
//
//  Created by beyond on 14-9-21.
//  Copyright (c) 2014年 com.beyond. All rights reserved.
//  演示上下文的裁剪

#import "Clip.h"

@implementation Clip

- (void)drawRect:(CGRect)rect
{
    // 1.无论做啥,首先取得上下文
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    // 2.先保存默认的参数
     CGContextSaveGState(ctx);
    
    // 3.画圆
    CGContextAddEllipseInRect(ctx, CGRectMake(100, 100, 150, 150));
    // 重要~~~~裁剪,上下文中本来的rect就不复存在了,现在的rect就只剩下一个圆了,未来的所有东东,全在这个新的小rect中了,直到恢复现场
    CGContextClip(ctx);
    // 闭合路径,现在的rect就只剩下一个圆了,未来的所有东东,全在这个新的小rect中了,直到恢复现场
    CGContextFillPath(ctx);
    
    // 4.显示图片
    UIImage *image = [UIImage imageNamed:@"nanaLogo.jpg"];
    // 现在的rect就只剩下一个圆了,图片画在这个新的小rect中了,直到恢复现场
    [image drawAtPoint:CGPointMake(100, 100)];
    
    // 5.终于,恢复默认的上下文参数,此时的rect又还原为完整的rect了
     CGContextRestoreGState(ctx);
    // 使用默认的上下文,即rect为完整的rect,画个矩形
    CGContextAddRect(ctx, CGRectMake(0, 0, 100, 100));
    // 闭合路径
    CGContextFillPath(ctx);
}

@end


重绘setNeedsDisplay+时钟,营造动画效果
//
//  Snow.m
//  41_绘图
//
//  Created by beyond on 14-9-21.
//  Copyright (c) 2014年 com.beyond. All rights reserved.
//  雪花

#import "Snow.h"

@interface Snow ()
@property (nonatomic, assign) CGFloat snowY;
@end

@implementation Snow
// 当一个view,是alloc init创建的,就会调用此方法
- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // 一进来就,开启定时器
        [self setupDisplayLink];
    }
    return self;
}

// 当一个view,是从storyboard中创建的,就会调用此方法
- (void)awakeFromNib
{
    // 一进来就,开启定时器
    [self setupDisplayLink];
}

- (void)setupDisplayLink
{
    CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(setNeedsDisplay)];
    [link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    // 频率低的,才用NSTimer
    //    [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(setNeedsDisplay) userInfo:nil repeats:YES];
}
- (void)drawRect:(CGRect)rect
{
    self.snowY++;
    
    if (self.snowY >= rect.size.height) {
        self.snowY = -100;
    }
    
    UIImage *image = [UIImage imageNamed:@"snow.jpg"];
    [image drawAtPoint:CGPointMake(0, self.snowY)];
}

@end


又一个重绘效果
//
//  ReDraw.m
//  41_绘图
//
//  Created by beyond on 14-9-21.
//  Copyright (c) 2014年 com.beyond. All rights reserved.
//  重绘,setNeedsDisplay


#import "ReDraw.h"



@implementation ReDraw



/**
 *  默认只会在view第一次显示的时候调用(只能由系统自动调用, 不能手动调用)
 */
- (void)drawRect:(CGRect)rect
{
    // 1.无论做啥,首先取得上下文
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    // 2.画圆
    CGContextAddArc(ctx, 125, 125, self.radius, 0, M_PI * 2, 0);
    // 3.闭合路径
    CGContextFillPath(ctx);
}

#pragma mark - 拦截setter方法
// 只要外界,设置圆的半径,就调用setNeedsDisplay方法,重绘一次
- (void)setRadius:(float)radius
{
    _radius = radius;
    // 重新绘制一次 整个bounds
    [self setNeedsDisplay];
}
@end


Paths中的几个重要元素

Points

 void CGContextMoveToPoint (

    CGContextRef c,

    CGFloat x,

    CGFloat y

 );

 指定一个点成为current point

 Quartz会跟踪current point一般执行完一个相关函数后,current point都会相应的改变.

 

Lines

 相关的几个函数

 void CGContextAddLineToPoint (

    CGContextRef c,

    CGFloat x,

    CGFloat y

 );

 创建一条直线,从current point到 (x,y)

 然后current point会变成(x,y)

 

 void CGContextAddLines (

    CGContextRef c,

    const CGPoint points[],

    size_t count

 );

 创建多条直线,比如points有两个点,那么会画两条直线 从current point到 (x1,y1),

 然后是(x1,y1)到(x2,y2)

 然后current point会变成points中的最后一个点

 

Arcs

 两种方法创建弧度 第一种

 void CGContextAddArc (

    CGContextRef c,    

    CGFloat x,             //圆心的x坐标

    CGFloat y,    //圆心的x坐标

    CGFloat radius,   //圆的半径 

    CGFloat startAngle,    //开始弧度

    CGFloat endAngle,   //结束弧度

    int clockwise          //0表示顺时针,1表示逆时针

 );


 假如想创建一个完整的圆圈,

 那么 开始弧度就是0 结束弧度是 2pi, 因为圆周长是 2*pi*r.

 最后,函数执行完后,current point就被重置为(x,y).

 还有一点要注意的是,假如当前path已经存在一个subpath,那么 这个函数执行的另外一个效果是

 会有一条直线,从current point到弧的起点

 

 

 第二种

 void CGContextAddArcToPoint (

    CGContextRef c,

    CGFloat x1,  //端点1的x坐标

    CGFloat y1,  //端点1的y坐标

    CGFloat x2,  //端点2的x坐标

    CGFloat y2,  //端点2的y坐标

    CGFloat radius //半径

 );

 原理:首先画两条线,这两条线分别是 current point to (x1,y1) 和(x1,y1) to (x2,y2).

 这样就是出现一个以(x1,y1)为顶点的两条射线,

 然后定义半径长度,这个半径是垂直于两条射线的,这样就能决定一个圆了

 最后,函数执行完后,current point就被重置为(x2,y2).

 还有一点要注意的是,假如当前path已经存在一个subpath,那么这个函数执行的另外一个效果是

 会有一条直线,从current point到(x1,y1)

 

Curves

 画曲线,一般是一条直线,然后定义几个控制点,使直线变弯曲。

 三次曲线函数

 void CGContextAddCurveToPoint (

    CGContextRef c,

    CGFloat cp1x, //控制点1 x坐标

    CGFloat cp1y, //控制点1 y坐标

    CGFloat cp2x, //控制点2 x坐标

    CGFloat cp2y, //控制点2 y坐标

    CGFloat x,  //直线的终点 x坐标

    CGFloat y  //直线的终点 y坐标

 );

 假如第二个控制点(cp2x,cp2y)比(cp1x,cp1y) 更接近current point,那么会形成一个封闭的曲线


 

 二次曲线函数

 void CGContextAddQuadCurveToPoint (

    CGContextRef c,

    CGFloat cpx,  //控制点 x坐标

    CGFloat cpy,  //控制点 y坐标

    CGFloat x,  //直线的终点 x坐标

    CGFloat y  //直线的终点 y坐标

 );

 

 

 

Ellipses

 void CGContextAddEllipseInRect (

    CGContextRef context,

    CGRect rect  //一矩形

 ); 

 如果矩形是一个正方形,那么画出来就是一个圆

 执行完函数貌似current point不会变化,没有具体测试过

Rectangles

 void CGContextAddRect (

    CGContextRef c,

    CGRect rect

 );

 

 一次性画出多个矩形

 void CGContextAddRects (

    CGContextRef c,

    const CGRect rects[],

    size_t count

 );

 需要注意的是,画矩形有一些特别,current point没有发生变化

 


Creating a Path

 调用函数 CGContextBeginPath 开始创建路径,线调用函数CGContextMoveToPoint设置起点

 然后开始画自己想画的路径,注意一下几点:

 1.Lines, arcs, and curves,是从current point开始的

 2.假如想封闭一条路径,那么调用函数 CGContextClosePath 把当前点和起点连接起来

 3.当在画 arcs的时候,Quartz会画一条线从current point 到 starting point

 4.画矩形的时候不会有第三条那这样的的一条直线

 5.创建完路径后,必须调用 painting 函数  fill or stroke the path,不然不会画上面东东在相应的设备上】

 6.开始创建一个新的路径的时候,使用函数 CGContextBeginPath。

 

 重复利用路径的相关函数和数据类型

 CGPathCreateMutable 类似于 CGContextBeginPath

 CGPathMoveToPoint 类似于 CGContextMoveToPoint

 CGPathAddLineToPoint 类似于 CGContextAddLineToPoint

 CGPathAddCurveToPoint 类似于 CGContextAddCurveToPoint

 CGPathAddEllipseInRect 类似于 CGContextAddEllipseInRect

 CGPathAddArc 类似于 CGContextAddArc

 CGPathAddRect 类似于 CGContextAddRect

 CGPathCloseSubpath 类似于 CGContextClosePath

 CGPathRef

 CGMutablePathRef

 

 用CGContextAddPath函数把一个路径添加到graphics context中

 void CGContextAddPath (

    CGContextRef context,

    CGPathRef path

 );


Painting a Path

 Stroking :画出路径

 Filling :填充路径的封闭区域

 

 影响Stroking的参数

 Line width

  void CGContextSetLineWidth (

     CGContextRef c,

     CGFloat width

  );

 Line join:线转弯的时候的样式,

【连接处、接头处】比如圆滑的方式

  void CGContextSetLineJoin (

     CGContextRef c,

     CGLineJoin join

  );


  

 Line cap:线的两端的样式,【帽子】比如两端变的圆滑

  void CGContextSetLineCap (

     CGContextRef c,

     CGLineCap cap

  );


 

 Miter limit:当Line join的模式是Miter join的时候,这个参数会有影响

  void CGContextSetMiterLimit (

     CGContextRef c,

     CGFloat limit

  );

 Line dash pattern:虚线相关

  void CGContextSetLineDash (

     CGContextRef c,

     CGFloat phase,

     const CGFloat lengths[],

     size_t count

  );


 

 Stroke color space

  void CGContextSetStrokeColorSpace (

     CGContextRef c,

     CGColorSpaceRef colorspace

  );

 Stroke color

  void CGContextSetStrokeColor (

     CGContextRef c,

     const CGFloat components[]

  );

  void CGContextSetStrokeColorWithColor (

     CGContextRef c,

     CGColorRef color

  );

 Stroke pattern(和透明度相关)

  void CGContextSetStrokePattern (

     CGContextRef c,

     CGPatternRef pattern,

     const CGFloat components[]

  );



 Stroking的相关函数

 Strokes当前path.

 void CGContextStrokePath (

    CGContextRef c

 );

 

 Strokes 指定的 矩形.

 void CGContextStrokeRect (

    CGContextRef c,

    CGRect rect

 );

 

 Strokes 指定的 矩形, 使用指定的宽度.

 void CGContextStrokeRectWithWidth (

    CGContextRef c,

    CGRect rect,

    CGFloat width

 );

  

 Strokes 指定的椭圆.

 void CGContextStrokeEllipseInRect (

    CGContextRef context,

    CGRect rect

 );

 

 Strokes 一些直线.

 void CGContextStrokeLineSegments (

    CGContextRef c,

    const CGPoint points[],

    size_t count

 );

 

 决定是Stroking 还是Filling

 void CGContextDrawPath (

    CGContextRef c,

    CGPathDrawingMode mode

 );

  

Filling a Path

填充一个路径的时候,路径里面的子路径都是独立填充的。

假如是重叠的路径,决定一个点是否被填充,有两种规则

1,nonzero winding number rule:非零绕数规则,假如一个点被从左到右跨过,计数器+1,从右到左跨过,计数器-1,最后,如果结果是0,那么不填充,如果是非零,那么填充。

2,even-odd rule: 奇偶规则,假如一个点被跨过,那么+1,最后是奇数,那么要被填充,偶数则不填充,和方向没有关系。


 

 



 

Setting Blend Modes

设置当一个颜色覆盖上另外一个颜色,两个颜色怎么混合

默认方式是

result = (alpha * foreground) + (1 - alpha) * background


CGContextSetBlendMode :设置blend mode.

CGContextSaveGState :保存blend mode.

CGContextRestoreGState:在没有保存之前,用这个函数还原blend mode.


 


 

 Note:  这个规则也可以应用于图片,用函数:CGContextSetBlendMode 来设置

   

 


 

 


 



 

 


 

 


 

 


 

 


 

 


 

 


 

  


 

 


 

 


 

 


 

 


 

 


 

 


 

Clipping to a Path

这个用在,假如我们只想把图片的部分打印到屏幕的时候


CGContextBeginPath (context);


CGContextAddArc (context, w/2, h/2, ((w>h) ? h : w)/2, 0, 2*PI, 0);


CGContextClosePath (context);


CGContextClip (context);



 




Quartz 2D是一个二维图形绘制引擎,支持iOS环境和Mac OS X环境。

使用Quartz 2D API可实现许多功能,如基本路径的绘制、透明度、描影、绘制阴影、透明层、颜色管理、反锯齿、PDF文档生成和PDF元数据访问

在需要的时候,Quartz 2D还可以借助图形硬件的功能。

在Mac OS X中,Quartz 2D可以与其它图形图像技术混合使用

Core Image、Core Video、OpenGL、QuickTime。

例如,通过使用 QuickTime的GraphicsImportCreateCGImage函数,可以用 Quartz从一个 QuickTime图形导入器中创建一个图像。

Quartz 2D在图像中使用了绘画者模型(painter’s model)

绘画者模型中,每个连续的绘制操作都是将一个绘制层(a layer of ‘paint’)放置于一个画布(‘canvas’),我们通常称这个画布为(Page)。

 Page上的绘图可以通过额外的绘制操作来叠加更多的绘图。

Page上的图形对象只能通过叠加更多的绘图来改变。

Page可以是一张纸(如果输出设备是打印机),也可以是虚拟的纸张(如果输出设备是PDF文件),还可以是bitmap图像。

这根据实际使用的图形上下文graphics context而定。

绘制目标:Graphics Context【图形上下文】
Graphics Context是一个数据类型(CGContextRef),用于封装Quartz绘制图像到输出设备的信息。

设备可以是PDF文件、bitmap或者显示器的窗口上。

Graphics Context中的信息包括在Page中的图像的图形绘制参数和设备相关的表现形式。

Quartz中所有的对象都是绘制到一个【图形上下文】Graphics Context中。
可以将【图形上下文】Graphics Context想像成绘制目标,

Quartz提供了以下五种类型的【图形上下文】Graphics Context

  • 【位图】Bitmap Graphics Context
  • 【PDF】PDF Graphics Context
  • 【窗口】Window Graphics Context
  • 【图层】Layer Context
  • 【打印】Post Graphics Context

Quartz 2D 数据类型
除了 【图形上下文】Graphics Context 之外,

Quartz 2D API还定义一些数据类型。

由于这些API就Core Graphics框架的一部分,所以这些数据类型都是以CG开头的。
 
下面列出了Quartz 2D包含的数据类型:

  • CGPathRef:用于向量图(矢量),可创建路径,并进行填充或描画(stroke)
  • CGImageRef:用于表示bitmap图像和基于采样数据的bitmap图像遮罩。
  • CGLayerRef:用于表示可用于重复绘制(如背景)和幕后(offscreen)绘制的绘画层
  • CGPatternRef:用于重绘图(底纹)
  • CGShadingRef、CGGradientRef:用于绘制渐变
  • CGFunctionRef:用于定义回调函数,该函数包含一个随机的浮点值参数。当为阴影创建渐变时使用该类型
  • CGColorRef, CGColorSpaceRef:用于告诉Quartz如何使用颜色
  • CGImageSourceRef,CGImageDestinationRef:用于在Quartz中指明数据来源和目的地
  • CGFontRef:用于绘制文本时的字体
  • CGPDFDictionaryRef, CGPDFObjectRef, CGPDFPageRef, CGPDFStream, CGPDFStringRef, and CGPDFArrayRef:用于访问PDF的元数据
  • CGPDFScannerRef, CGPDFContentStreamRef:用于解析PDF元数据
  • CGPSConverterRef:用于将Mac OS中的PostScript转化成PDF。在iOS中不能使用。

当前图形状态

Quartz通过修改当前图形状态(current graphics state)来修改绘制操作的结果。

图形状态包含用于绘制程序的参数。绘制程序根据这些绘图状态来决定如何渲染结果。

例如,当你调用设置填充颜色的函数时,你将改变存储在当前绘图状态中的颜色值。
Graphics Context包含一个绘图状态栈

当Quartz创建一个【图形上下文】Graphics Context时,栈为空。

当保存图形状态时,Quartz将当前图形状态的一个副本压入栈中。

当还原图形状态时,Quartz将栈顶的图形状态出栈。

出栈的状态成为当前图形状态。
可使用函数CGContextSaveGState来保存图形状态,CGContextRestoreGState来还原图形状态。
注意:并不是当前绘制环境的所有方面都是图形状态的元素。

如,图形状态不包含当前路径(current path)。

下面列出了图形状态相关的参数:

  • Current transformation matrix (CTM):当前转换矩阵
  • Clipping area:裁剪区域
  • Line: 线
  • Accuracy of curve estimation (flatness):曲线平滑度
  • Anti-aliasing setting:反锯齿设置
  • Color: 颜色
  • Alpha value (transparency):透明度
  • Rendering intent:渲染目标
  • Color space: 颜色空间
  • Text: 文本
  • Blend mode:混合模式

Quartz 2D 坐标系统(笛卡尔坐标系)

原点在左下方,向右为X,向上为Y
坐标系统定义是被绘制到Page上的对象的位置及大小范围

 
由于不同的设备有不同的图形功能,所以图像的位置及大小依赖于设备

例如,一个显示设备可能每英寸只能显示少于96个像素,而打印机可能每英寸能显示300个像素。

如果在设备级别上定义坐标系统,则在一个设备上绘制的图形无法在其它设备上正常显示。
因此,Quartz通过使用当前转换矩阵(current transformation matrix, CTM)将一个独立的坐标系统(user space)映射到输出设备的坐标系统(device space),以此来解决设备依赖问题。 

CTM是一种特殊类型的矩阵(affine transform, 仿射矩阵),通过平移(translation)、旋转(rotation)、缩放(scale)操作将点从一个坐标空间映射到另外一个坐标空间

CTM还有另外一个目的:

允许你通过转换来决定对象如何被绘制。

例如,为了绘制一个旋转了45度的盒子,我们可以在绘制盒子之前旋转Page的坐标系统。Quartz使用旋转过的坐标系统来将盒子绘制到输出设备中。
用户空间的点用坐标对(x, y)来表示,(0, 0)表示坐标原点。Quartz中默认的坐标系统是:笛卡尔坐标系

沿着x轴从左到右坐标值逐渐增大;

沿着y轴从下到上坐标值逐渐增大。


有一些技术在设置它们的graphics context时使用了不同于Quartz的默认坐标系统。

相对于Quartz来说,这些坐标系统是修改的坐标系统(modified coordinate system),

当在这些坐标系统中显示Quartz绘制的图形时,必须进行转换。

最常见的一种修改的坐标系统是:UIKit坐标系

即:原点位于左上角,而沿着y轴向下 逐渐增大。

我们可以在如下一些地方见到这种坐标系统:

  • 在Mac OS X中,重写过isFlipped方法以返回yes的NSView类的子类
  • 在IOS中,由UIView返回的绘图上下文 
  • 在IOS中,通过调用UIGraphicsBeginImageContextWithOptions函数返回的绘图上下文 

如果应用程序想以相同的绘制程序在一个UIView对象和PDF Graphics Context上进行绘制,

需要做一个变换,以使PDF Graphics Context使用与UIView相同的坐标系。

要达到这一目的,只需要对PDF的上下文的原点做一个平移(移到左上角)、

用-1对y坐标值进行反向

如果你想要一个图片或PDF正确的绘制到一个【图形上下文】Graphics Context中,

你的应用程序可能需要临时调整Graphics Context的CTM

在IOS中,如果使用UIImage对象来创建CGImage对象,则不需要修改CTM。

UIImage将自动进行补偿以适用UIKit的坐标系统。


内存管理:对象所有权
Quartz使用Core Foundation内存管理模型(引用计数)。所以,对象的创建与销毁与通常的方式是一样的。在Quartz中,需要记住如下一些规则:

  • 如果创建create或拷贝copy一个对象,你将拥有它,因此你必须释放它。
  • 通常,如果使用含有”Create”或“Copy”单词的函数获取一个对象,
  • 当使用完后必须释放,否则将导致内存泄露。
  • 如果使用不含有”Create”或“Copy”单词的函数获取一个对象,你将不会拥有对象的引用,不需要释放它。
  • 如果你不拥有一个对象而打算保持它,则必须retain它并且在不需要时release掉。
  • 可以使用Quartz 2D的函数来指定retain和release一个对象。
  • 例如,如果创建了一个CGColorspace对象,
  • 则使用函数CGColorSpaceRetain和CGColorSpaceRelease来retain和release对象。
  • 同样,可以使用Core Foundation的CFRetain和CFRelease,但是注意不能传递NULL值给这些函数。


一、CATransform3D是什么?

CATransform3D这个结构体表示三维的齐次坐标变换矩阵

齐次坐标是一种坐标的表示方法,

n维空间的坐标需要用n+1个元素的坐标元组来表示,

Quartz 2D Transform中就有关于齐次坐标的应用,

那里是关于二维空间的变换,

其某点的齐次坐标的最后一个元素始终设置为1


二、为什么需要齐次坐标

之所以要使用齐次坐标,而不是简单的数学坐标

目的是:为了方便图形进行仿射变换

仿射变换可以通过仿射变换矩阵来实现,

3D的仿射变换功能强大,它可以实现诸如:

平移(translation),旋转(rotation),缩放(scaling),切变(shear)

如果不用齐次坐标那么进行坐标变换可能就涉及到两种运算了:

加法(平移)和乘法(旋转,缩放)

然而,使用齐次坐标以及齐次坐标变换矩阵后,问题就简单多了

只需要矩阵乘法就可以完成一切了。

以下需要线性代数、图形变换的相关知识,进行矩阵的乘法。

三、CATransform3D中的最关键的m34

iOS中的CALayer的3D本质上并不能算真正的3D

因为其视点即观察点或者所谓的照相机的位置是无法变换的,

而仅仅只是3D在二维平面上的投影,

投影平面就是:手机屏幕

也就是xy轴组成的UIKit坐标平面(注意UIKit中为左手坐标系),

那么视点的位置是如何确定的呢?

可以通过CATransform3D中的m34来间接指定:

m34是相当常用的一个属性

 m34 = -1/z,

其中z为观察点在z轴上的值,

下面代码是XCode文档中摘取的
1 CATransform3D aTransform = CATransform3DIdentity; 2 // the value of zDistance affects the sharpness of the transform. 3 zDistance = 850 ; 4 aTransform.m34 = 1.0 / -zDistance;

而Layer的z轴的位置则是通过anchorPoint来指定的,

所谓的anchorPoint(锚点)就是在变换中保持不变的点,

也就是某个Layer在变换中的原点,

xyz三轴相交于此原点(锚点)

在iOS中,Layer的anchorPoint使用单位坐标系来描述,

单位坐标系,范围是0~1

无需指定具体真实的坐标点,

而是使用layer bounds中的相对位置

下图展示了一个Layer中的几个特殊的锚点, 默认是(0.5,0.5)


在上面的:

m34 = -1/z中,

当z值为的时候(如上面的850),

是我们人眼观察现实世界的效果,

即在投影平面上表现出近大远小的效果,

z越靠近原点则这种效果越明显,越远离原点则越不明显,

当z为正无穷大的时候,则失去了近大远小的效果,

此时投影线垂直于投影平面,也就是视点在无穷远处,

CATransform3D中m34的默认值为0,即视点在无穷远处.

特别注意的是:

齐次坐标到数学坐标的转换 

通用的齐次坐标为 (a, b, c, h),

其转换成数学坐标则为 (a/h, b/h, c/h).

四.代数解释

假设一个Layer anchorPoint为默认的 (0.5, 0.5 ), 

其三维空间中一个A点 (6, 0, 0),z=1000

m34 = -1/z  =  -1/1000.0, 

则此点往z轴负方向移动10个单位之后,

则在投影平面上看到的点的坐标是多少呢?




上面的两个矩阵相乘则会得到最终的变换矩阵

所以一个矩阵就可以完成变换和投影

将A点坐标乘上最终的变换矩阵,

则得到 {6, 0 , -10, 1.01}, 

转换成数学坐标点为 {6/1.01, 0, 10/1.01},

则可以知道其在投影平面上的投影点为 {6/1.01, 0, 0} 

也就是我们看到的变换后的点。

其比之前较靠近原点。

越往z轴负方向移动,则在投影平面上越靠近原点。

五.几何作图解释

将上面的例子使用几何的方式来进行解释分析,

当人的眼睛 沿着y轴的正方向向下看时候,可以得到如下的景象

此时y轴垂直于屏幕指向人的眼晴


虚线为投影线,其和x轴的交点即为A点的投影点。 

由相似三角形的定理算出投影的点,

1000/(1000 + 10) = x/6,则x = 6*1000/1010 = 6/1.01



iOS设备给用户视觉反馈其实都是通过QuartzCore框架来进行的,用户最终看到的显示界面都是图层合成的结果,

而图层即是QuartzCore中的CALayer。

通常我们所说的视图即UIView,并不是直接显示在屏幕上,

而是在创建UIView对象的时候会自动创建一个CALayer层(根层),

而视图对象把要显示的东西绘制在层上,待到需要显示时硬件将所有的层拷贝,

最后,按Z轴的高低合成最终的显示结果。

CALayer本质上是一块包含一幅位图的缓冲区,由视图创建的根层为隐式层,而手动创建的层称为显示层。

所有的动画效果都是通过CAAnimation类的子类来完成的。

(因为CAAnimation是抽象类)

CAAnimation类的子类包括了三个:

CAAnimationGroup,CAPropertyAnimation,CATransition,

而CAPropertyAniamtion(同为抽象类)也衍生了两个子类:

CABasicAnimation和CAKeyframeAnimation。

用UIView的animation实现的动画本质上也是通过CALayer来实现的,

iOS系统中CALayer的很多属性都是隐含有动画效果的,

如果不想要隐式动画或者想要显示动画效果,也可以通过CATransaction来设置是否显示动画效果。

同时,在CATransaction内可同时修改多个属性,然后再一并同时渲染,

此外,CATransaction还是可嵌套的。

CABasicAnimation是一个最多只能有两个关键帧的动画,fromValue和toValue

而CAKeyframeAnimation除了可含有多个关键帧,而且还可以修改每个关键帧的速度。

CATransition能够为层提供移出以及移入屏幕的效果。

api只开放了其中的四种,当然你可以调用未公开的api,但是假如苹果以后出于安全还是什么原因调整接口的话,就不一定能用了,所以最好还是不要调用私有api,

例如@"flip”,@"pageCurl”这些type是属于未公开的

一般来说不应该调用这些做动画,下面的这句

1 [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:containerView cache:YES];

同样也可实现翻转的效果。

还可以通过设置@"transform.rotation.y"这个keypath来旋转,

或者直接通过CATransform3D来设置@"transform”属性。

总而言之,没必要用未公开的api,会留下安全隐患

变换过程中需要注意的是:

该CALayer的anchorPoint,也就是旋转缩放基准点,默认情况下是(0.5, 0.5)也就是在layer的中心。

基准点如果变了,旋转或者缩放所得到的的结果将会不同。

positionanchorPoint共同决定了CALayer在父层中的frame。

此外,CATransform3D的一个很常用到的数值,那就是m34这个值,

下面这段代码是从Xcode文档中得到的

1 CATransform3D aTransform = CATransform3DIdentity;
2 // the value of zDistance affects the sharpness of the transform.
3 zDistance = 850;
4 aTransform.m34 = 1.0 / -zDistance;

 默认情况下, m34的值为0,即zDistance为无穷大

而(0,0,zDistance)点表示照相机(透视点)所在的位置

从照相机位置看到的影像在xy平面上的投影即是我们可以在手机屏幕中所看到的影像

动画的速度控制函数  CAMediaTimingFunction

这个函数是用于描述时间和距离之间的关系

距离=toValue-fromValue, 

时间=duration, 

将它们标准化为1×1的空间,

x,t取值在[0, 1]区间内。

假设在时刻t(时间过去了duration*t)时,

动画目标的位置应为 fromValue+x*(toValue-fromValue)。

系统提供的四种函数

kCAMediaTimingFunctionLinear为匀速运动,

kCAMediaTimingFunctionEaseIn为加速运动,

kCAMediaTimingFunctionEaseOut为减速运动,

kCAMediaTimingFunctionEaseInEaseOut为先加速后减速运动。

当然,系统也提供了可自定义速度控制函数,

通过initWithControlPoints:x1:y1:x2:y2自定义两个插值点

然后所得的函数曲线为由(0,0),(x1,y1),(x2,y2),(1,1)这四个点为控制点的Bezier曲线

自行绘制UI,而自行绘制UI就需要用到CoreGraphics这个框架(甚至OpenGL)

CGContext类,相当于Android里面的Canvas

使用UIGraphicsGetCurrentContext()获取当前CGContext的引用CGContextRef。

我们在每一次的绘制之前都应该保存下原来的状态,待绘制完成后再恢复回原来的状态

 CGContextSaveGState(ctx)

 CGContextRestoreGState(ctx)

都应该成对的出现,在他们之间的是绘制UI的代码。

CGPath类用于描述绘制的区域或者路线。

在CGContext中addLine,addRect,其实都是在添加path,

在添加完成path以后我们可以选择是fill该path,还是stroke该path。

设置相应的颜色以及线宽即可。

如果我们只需要在某个区域中绘制,

我们可以在描述完path以后使用CGContextClip(ctx)来裁剪当前画布。

CGAffineTransform是一个仿射变换的结构体,相当于一个矩阵,

用于进行二维平面的几何变换(平移,旋转,缩放等),

而且这些几何变换都已经有封装好的函数可调用,变换的顺序就是矩阵连乘的顺序,

切不可把矩阵连乘的顺序放反,否则得到的结果可能不一样

CGColorSpace类是用于界面颜色显示,通常颜色组成为RGBA(红,绿,蓝,透明度)四种,

使用CGColorSpaceCreateDeviceRGB获取CGColorSpace的引用CGColorSpaceRef,

需要注意的是,在CoreGraphics中,使用create方法调用release方法释放掉,如CGColorSpaceCreateDeviceRGB()对应的就是CGColorSpaceRelease(rgb)。

在用CoreGraphics绘制时,我们应该注意的是:

当使用CGContextShowTextAtPoint绘制文字时,如果不进行几何变换,得到的是文字的倒影

所以我们需要使用CGContextSetTextMatrix (ctx, myTextTransform); 

其中的transform为关于x轴对称的变换矩阵。

当然也可以不用这么做,只需要把原来CGContext的坐标先关于x轴对称,然后再平移即可,

CGContextConcatCTM(ctx, CGAffineTransformConcat(myTextTransform, CGAffineTransformMakeTranslation(140.0f, 30.0f))); 

其中的transform为关于x轴对称的变换矩阵。

同样的,使用如下语句绘制图片时,得到的也将是图片的倒影:

CGContextDrawImage(ctx, CGRectMake(0, 0, image.size.width, image.size.height), image.CGImage);

所以同样的也需要进行关于x轴的坐标变换

但是使用[image drawInRect:CGRectMake(0.0f, 0.0f, image.size.width, image.size.height)];

绘制图片时就不需要关于x轴对称,默认已经是使用UIKit坐标系了。

总结,绘制文字以及图片时会得到倒影,需做一下相关的变换即可

一.图层基本概念

Animation(动画)对CALayer来说就是:

在一段时间内,其(可动画属性)Animatable Property发生了变化.

从CALayer(CA = Core Animation)类名可知:

iOS的Layer就是为动画而生的,便于实现良好的交互体验. 

一是Layer(基类CALayer),

另一是Animation(基于CAAnimation). 

Animation作用于Layer.

CALayer提供了接口用于给自己添加Animation.

用于显示的Layer本质上讲是一个数据模型,

里面包含了Layer的各种属性值.

 Animation则包含了动画的时间,变化,以及变化的速度函数.

二.CALayer及时间模型

UIView的职责有两个:

一个是:界面的显示

另一个是:界面事件的处理.

每一个View的背后都有一个【根层】layer

(可以通过view.layer进行访问),

本质上,layer才是用于界面显示的.


1.Layer的渲染架构

Layer也和View一样存在着一个层级树状结构,

称之为图层树(Layer Tree),

直接创建的或者通过UIView获得的(view.layer)用于显示的图层树,

被称之为模型树(Model Tree),

模型树的背后还存在两份图层树的拷贝,

一个是呈现树(Presentation Tree),

一个是渲染树(Render Tree). 

呈现树可以通过普通layer(其实就是模型树)的layer.presentationLayer获得,

而模型树则可以通过modelLayer属性获得.

模型树的属性在其被修改的时候就变成了新的值,

这个是可以用代码直接操控的部分;

呈现树的属性值和动画运行过程中界面上看到的是一致的.

而渲染树是私有的,你无法访问,渲染树是对呈现树的数据进行渲染,

为了不阻塞主线程,渲染的过程是在单独的进程或线程中进行的,

所以你会发现Animation的动画并不会阻塞主线程.

2.事务管理

CALayer的那些可用于动画的(Animatable)属性,

称之为【可动画属性】Animatable Properties,


如果一个Layer对象存在对应着的View,

则称这个Layer是一个Root Layer,

非Root Layer一般都是通过CALayer或其子类直接创建的.

下面的subLayer就是一个典型的非Root Layer,它没有对应的View对象关联着.

    subLayer = [[CALayer alloc] init];
    subLayer.frame = CGRectMake(0, 0, 300, 300);
    subLayer.backgroundColor = [[UIColor redColor] CGColor];
    [self.view.layer addSublayer:subLayer];

所有的非Root Layer在设置Animatable Properties的时候都存在着隐式动画,默认的duration是0.25秒.

    subLayer.position = CGPointMake(300,400);

像上面这段代码当下一个RunLoop开始的时候,

并不是直接将subLayer的position变成(300,400)的,

而是有个移动的动画进行过渡完成的.

任何Layer的animatable属性的设置过程 都应该属于一个CA事务(CATransaction),

事务的作用是为了保证多个animatable属性的变化同时进行,

不管是同一个layer还是不同的layer之间的.

CATransaction也分两类,显式的和隐式的,

当在某次RunLoop中设置一个animatable属性的时候,

如果发现当前没有事务,则会自动创建一个CA事务,

在线程的下个RunLoop开始时自动commit这个事务,

如果在没有RunLoop的地方设置layer的animatable属性,

则必须使用显式的事务.

显式事务的使用如下:

[CATransaction begin];
...  
[CATransaction commit];

事务可以嵌套.当事务嵌套时候,只有当最外层的事务commit了之后,整个动画才开始.

可以通过CATransaction来设置一个事务级别的动画属性,

覆盖掉隐式动画的相关属性,

比如覆盖隐式动画的duration,timingFunction.

如果是显式动画没有设置duration或者timingFunction,

那么CA事务设置的这些参数也会对这个显式动画起作用.

还可以设置completionBlock:

在当前CATransaction的所有动画执行结束后, completionBlock将会被调用.

3.时间系统

CALayer实现了CAMediaTiming协议.

 CALayer通过CAMediaTiming协议实现了一个有层级关系的时间系统.

除了CALayer,CAAnimation也采纳了此协议,用来实现动画的时间系统. 

在CA中,有一个Absolute Time(绝对时间)的概念,

可以通过CACurrentMediaTime()获得,

其实这个绝对时间就是将mach_absolute_time()转换成秒后的值.

这个时间和系统的启动时间有关,

系统重启后CACurrentMediaTime()会被重置. 
就和坐标存在相对坐标一样,

不同的实现了CAMediaTiming协议的存在层级关系的对象也存在相对时间,

经常需要进行时间的转换,CALayer提供了两个时间转换的方法:

- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;

现在来重点研究CAMediaTiming协议中几个重要的属性.

beginTime

无论是图层还是动画,都有一个时间线Timeline的概念,

他们的beginTime是相对于父级对象的开始时间. 

虽然苹果的文档中没有指明,但是通过代码测试可以发现,

默认情况下所有的CALayer图层的时间线都是一致的,

他们的beginTime都是0,

绝对时间转换到当前Layer中的时间大小就是绝对时间的大小.

所以对于图层而言,虽然创建有先后,

但是他们的时间线都是一致的(只要不主动去修改某个图层的beginTime),

所以我们可以想象成所有的图层默认都是从系统重启后开始了他们的时间线的计时.

但是动画的时间线的情况就不同了,

当一个动画创建好,被加入到某个Layer的时候,

会先被拷贝一份出来用于加入当前的图层,

在CA事务被提交的时候,如果图层中的动画的beginTime为0,

则beginTime会被设定为当前图层的当前时间,使得动画立即开始.

如果你想某个直接加入图层的动画稍后执行,

可以通过手动设置这个动画的beginTime,

但需要注意的是这个beginTime需要为 CACurrentMediaTime()+延迟的秒数,

因为beginTime是指其父级对象的时间线上的某个时间,

这个时候动画的父级对象为加入的这个图层,

图层当前的时间其实为[layer convertTime:CACurrentMediaTime() fromLayer:nil],

其实就等于CACurrentMediaTime(),

那么再在这个layer的时间线上往后延迟一定的秒数便得到上面的那个结果.

timeOffset

这个timeOffset可能是这几个属性中比较难理解的一个,

 local time也分成两种一种是active local time 

一种是basic local time.

timeOffset则是active local time的偏移量. 
你将一个动画看作一个环,timeOffset改变的其实是动画在环内的起点,

比如一个duration为5秒的动画,将timeOffset设置为2(或者7,模5为2),

那么动画的运行则是从原来的2秒开始到5秒,接着再0秒到2秒,完成一次动画.

speed

speed属性用于设置当前对象的时间流相对于父级对象时间流的流逝速度,

比如一个动画beginTime是0,但是speed是2,

那么这个动画的1秒处   相当于  父级对象时间流中的2秒处. 

speed越大则说明时间流逝速度越快,那动画也就越快.

比如一个speed为2的layer其所有的父辈的speed都是1,

它有一个subLayer,speed也为2,那么一个8秒的动画在这个运行于这个subLayer只需2秒(8 / (2 * 2)).

所以speed有叠加的效果.

fillMode

fillMode的作用就是决定当前对象过了非active时间段的行为. 

比如动画开始之前,动画结束之后。

如果是一个动画CAAnimation,

则需要将其removedOnCompletion设置为NO,

要不然fillMode不起作用. 

下面来讲各个fillMode的意义 
kCAFillModeRemoved 这个是默认值,

也就是说当动画开始前和动画结束后,动画对layer都没有影响,

动画结束后,layer会恢复到之前的状态 

kCAFillModeForwards 当动画结束后,layer会一直保持着动画最后的状态 
kCAFillModeBackwards 这个和kCAFillModeForwards是相对的,

就是在动画开始前,你只要将动画加入了一个layer,

layer便立即进入动画的初始状态并等待动画开始.

你可以这样设定测试代码:

将一个动画加入一个layer的时候延迟5秒执行.

然后就会发现在动画没有开始的时候,只要动画被加入了layer,

layer便处于动画初始状态 
kCAFillModeBoth 理解了上面两个,这个就很好理解了,

这个其实就是上面两个的合成.动画加入后开始之前,layer便处于动画初始状态,动画结束后layer保持动画最后的状态.

其他的一些参数都是比较容易理解的.

实际应用
-(void)pauseLayer:(CALayer*)layer
{
    CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
    layer.speed = 0.0;
    layer.timeOffset = pausedTime;
}

-(void)resumeLayer:(CALayer*)layer
{
    CFTimeInterval pausedTime = [layer timeOffset];
    layer.speed = 1.0;
    layer.timeOffset = 0.0;
    layer.beginTime = 0.0;
    CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
    layer.beginTime = timeSincePause;
}

三.显式动画Animation

当需要对非Root Layer进行动画或者需要对动画做更多自定义的行为的时候,就必须使用到显式动画了,

显式动画的基类为CAAnimation,

常用的是CABasicAnimation,CAKeyframeAnimation

有时候还会使用到:

CAAnimationGroup,CATransition(过渡、转场动画). 

这里再强调关于动画的两个重要的点:

一是中间状态的插值计算(Interpolation),

二是动画节奏控制(TimingFunction);

 有时候插值计算也和Timing有一定关系. 

如果状态是一维空间的值(比如透明度),那么插值计算的结果必然再起点值和终点值之间,

如果状态是二维空间的值(比如position),

那么一般情况下插值得到的点会落在起点和终点之间的线段上(当然也有可能连线是圆滑曲线).

1.CABasicAnimation

不管是CABasicAnimation还是CAKeyframeAnimation

它们都是继承于CAPropertyAnimation. 


CABasicAnimation有三个比较重要的属性:

fromValue,toValue,byValue

这三个属性都是可选的

最终都是为了确定animation变化的起点和终点.

设置了动画的起点和终点之后,

中间的值都是通过插值方式计算出来的.

插值计算的结果由timingFunction指定,

默认timingFunction为nil,即使用线性linear,也就是变化均匀.

2.Timing Function的作用

Timing Function被用于变化起点和终点之间的插值计算.

形象点说是Timing Function决定了动画运行的节奏(Pacing),

比如是均匀变化(相同时间变化量相同),

还是   先快后慢,

抑或是   先慢后快

还是        先慢再快再慢.

时间函数是使用的一段函数来描述的,

横坐标是时间t取值范围是0.0  ~  1.0,

纵坐标是变化量x(t)

也是取值范围也是0.0  ~  1.0

假设有一个动画:

duration是8秒,变化值的起点是a终点是b(假设是透明度),

那么在4秒处的值是多少呢? 

可以通过计算为 a + x(4/8) * (b-a), 

为什么这么计算呢?

讲实现的时间映射到单位值的时候4秒相对于总时间8秒就是0.5

然后可以得到0.5的时候单位变化量是 x(0.5), x(0.5)/1 = 实际变化量/(b-a), 

其中b-a为总变化量,所以实际变化量就是x(0.5) * (b-a) ,

最后4秒时的值就是 a + x(0.5) * (b-a),

所以计算的本质是映射.

Timing Function对应的类是CAMediaTimingFunction,

它提供了两种获得时间函数的方式:

一种是使用预定义的五种时间函数,

一种是通过给点两个控制点得到一个时间函数. 

相关的方法为:

+ (id)functionWithName:(NSString *)name;
// 控制点
+ (id)functionWithControlPoints:(float)c1x :(float)c1y :(float)c2x :(float)c2y;

- (id)initWithControlPoints:(float)c1x :(float)c1y :(float)c2x :(float)c2y;

五种预定义的时间函数名字的常量变量分别为 
kCAMediaTimingFunctionLinear
kCAMediaTimingFunctionEaseIn
kCAMediaTimingFunctionEaseOut
kCAMediaTimingFunctionEaseInEaseOut
kCAMediaTimingFunctionDefault
下图展示了前面四种Timing Function的曲线图,

横坐标表示时间,纵坐标表示变化量,

这点需要搞清楚(并不是平面座标系中xy). 


自定义的Timing Function的函数图像:

就是一条三次贝塞尔曲线Cubic Bezier Curve,

贝塞尔曲线的优点就是光滑,用在这里就使得变化显得光滑.

一条三次贝塞尔曲线可以由起点终点以及两个控制点决定. 

上面的kCAMediaTimingFunctionDefault对应的函数曲线

其实就是通过[(0.0,0.0), (0.25,0.1), (0.25,0.1), (1.0,1.0)]

这四个点决定的三次贝塞尔曲线,

头尾为起点和终点,中间的两个点是控制点. 
 
下图中P0是起点,P3是终点,P1和P2是两个控制点


如果时间变化曲线既不是直线也不是贝塞尔曲线,而是自定义的,

又或者某个图层运动的轨迹不是直线而是一个曲线,

这些是基本动画无法做到的,必须使用CAKeyframeAnimation,

也即所谓的关键帧动画.

3.CAKeyframeAnimation

任何动画要表现出运动或者变化,至少需要两个不同的关键状态,

而中间的状态的变化可以通过插值计算完成,从而形成补间动画,

表示关键状态的帧叫做关键帧. 


CABasicAnimation其实可以看作一种特殊的关键帧动画,

即:只有头尾两个关键帧.

CAKeyframeAnimation则可以支持任意多个关键帧,

关键帧有两种方式来指定:

使用path或者使用values,

path是一个CGPathRef的值,

且path只能对CALayer的 anchorPoint 和 position 属性起作用,

且设置了path之后values就不再起效了.

而values则更加灵活.

keyTimes这个可选参数可以为对应的关键帧指定对应的时间点,

其取值范围为0到1.0,表示占总动画时间的百分比

keyTimes中的每一个时间值都对应values中的每一帧.

当keyTimes没有设置的时候,各个关键帧的时间是平分的. 

注意:CAKeyframeAnimation中timingFunction是无效的
还可以通过设置可选参数timingFunctions
为关键帧之间的过渡设置timingFunction,

如果values有n个元素,那么timingFunctions则应该有n-1个.

但很多时候并不需要timingFunctions,

因为已经设置了够多的关键帧了,

比如每1/60秒就设置了一个关键帧,那么帧率将达到60FPS,

完全不需要相邻两帧的过渡效果

(当然也有可能某两帧 值相距较大,可以使用均匀变化或者增加帧率,比如每0.01秒设置一个关键帧).

在关键帧动画中还有一个非常重要的参数,那便是calculationMode,计算模式.

其主要针对的是每一帧的内容为一个坐标点的情况,

也就是对anchorPoint 和 position 进行的动画.

当在平面坐标系中有多个离散的点的时候,

它们可以是离散的,也可以直线相连后进行插值计算,

也可以使用圆滑的曲线将他们相连后进行插值计算. calculationMode目前提供如下几种模式 

kCAAnimationLinear 
kCAAnimationDiscrete 
kCAAnimationPaced 
kCAAnimationCubic 
kCAAnimationCubicPaced

kCAAnimationLinear calculationMode的默认值,

表示当关键帧为坐标点的时候,关键帧之间直接直线相连进行插值计算; 
kCAAnimationDiscrete 离散的,就是不进行插值计算,所有关键帧直接逐个进行显示; 
kCAAnimationPaced 使得动画均匀进行,
而不是按keyTimes设置的或者按关键帧平分时间,此时keyTimes和timingFunctions无效; 

kCAAnimationCubic 对关键帧为座标点的关键帧进行圆滑曲线相连后插值计算,对于曲线的形状还可以通过tensionValues,continuityValues,biasValues来进行调整自定义,这里的数学原理是Kochanek–Bartels spline,这里的主要目的是使得运行的轨迹变得圆滑; 


kCAAnimationCubicPaced 看这个名字就知道和上一个有一定联系,

其实就是在kCAAnimationCubic的基础上使得动画运行变得均匀,就是系统时间内运动的距离相同,此时keyTimes以及timingFunctions也是无效的.



  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值