概念:
1.首先要明白所有iOS中见到的所有东西,都是因为通过UIView内部的图层——CALayer对象;
并且,UIView显示到屏幕上时,会调用【 deawRect: 】方法进行绘图。
即:
》UIView负责:交互、响应;
》CALayer负责:展示、绘图;
但——系统提供的可能无法满足需求,那么我们就需要到Quartz2D(自定义UI控件)了。
2.图形上下文CGContextRef三个作用:
》路径
》状态
》目标
3.drawRect:方法
注意:
》与View相关的【图形上下文】只能在 drawRect:方法中取得,但重写 drawRect:方法需要一个UIView类,控制器类UIViewController中无法得到 drawRect:,往往需要自行创建一个;
》drawRect不能手动调用,有可能获取不到正确的上下文;
》drawRect的调用时机:
①、在View第一次显示时;
②、重绘:调用【 view的setNeedsDisplay 】或【setNeedsDisplayInRect:】的时候;
步骤:
一、搭建UI界面
二、画笔功能:
1、重写drawRect方法,需创建一个UIView类—— MayView,指定sb (storyboard) 中绘图区View的class为 MayView;
2、在MayView类中创建一个可变数组用于存储所有的路径
//存储路径的数组:路径不只有一条
@property (nonatomic, strong) NSMutableArray *allPaths;
并且进行懒加载
#pragma mark - Getter & Setter
//懒加载
- (NSMutableArray *)allPaths {
if (_allPaths == nil) {
_allPaths = [NSMutableArray array];
}
return _allPaths;
}
3、思考:每一条线都有什么状态,怎么修改?想画出一条线,怎么做?
a》注意:路径的颜色的设置需要在 drawRect:方法中,并且因为每一条线都是独立的,我们可能为每一条线设置不同的颜色等状态。我们需要为每一个路径,创建独立的对象,让它成为一个独立的存在,并且拥有颜色属性,那么我们可以通类对象来设置颜色属性。因此,我们需要创建一个路径类;
@interface MayPath : UIBezierPath
//每条路径自己的颜色状态
@property (nonatomic, strong) UIColor *pathColor;
@end
b》画一条线:至少分两步走:点击 和 移动,所以需要调用touchesBegan方法和touchesMove方法
3-1. 调用方法
在MayView类中引入头文件
#import "MayPath.h"
在“touchesBegan方法”里面创建一条路径,并且添加到全局的路径数组中(为什么每次都创建而不是用全局的?因为画板不可能只点击一次,所以我们要为每一次的点击都创建一个路径)
//开始点击
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent *)event {
// 1.创建路径
//每次点击时候都创建一条MayPath对象路径
MayPath * path = [MayPath bezierPath];
// 将创建好的路径添加到路径数组中
[self.allPaths addObject:path];
3-2.这一步我们要在MayView.h文件中创建一个lineWidth属性。(后面会将它与sb中的slider控件关联)
//线宽(设为公开属性为外部调用,与slider控件关联)
@property (nonatomic, assign) CGFloat lineWidth;
回到.m文件,设置线宽,将定于好的lineWidth赋值过来
// 2.设置线宽
[path setLineWidth:self.lineWidth];
3-3.我们需要回到控制器类ViewController中,引入自定义MayView类;
将MayView作为属性传入
#import "MayView.h"。
@interface ViewController ()
//拿到自定义view(画板),以拿到画板的属性
@property (weak, nonatomic) IBOutlet MayView *paintView;
@end
然后,为sb中的slider控件拖一条方法连线,并且在属性栏中更改它的值
//设置线宽与UISlider关联
- (IBAction)setLineWidth:(UISlider *)sender {
self.paintView.lineWidth = sender.value;
}
这样,lineWidth的值会随着UISlider控件的变化而变化;
同时我们需要在viewDidLoad中设置线宽的初始值,不设置,一开始没有点击UISlider,那么默认值为0,会无法显示。
- (void)viewDidLoad {
[super viewDidLoad];
//设置线宽的初始值
self.paintView.lineWidth = 1;
}
3-4.回到MayView.m中
// 连接处的样式
[path setLineJoinStyle:kCGLineJoinRound];
// 收尾样式
[path setLineCapStyle:kCGLineCapRound];
3-5.设置颜色
像lineWidth属性一样,我们将lineColor设置为公开的属性。然后设置路径的颜色为lineColor
@property (nonatomic, strong) UIColor * lineColor;
// 将颜色存储在对象属性中
path.pathColor = self.lineColor;
在控制器类中为颜色按钮拖线,三个按钮控件连接同一个方法。将按钮的背景颜色传给view的lineColor;
//设置颜色
- (IBAction)setLineColor:(UIButton *)sender {
self.paintView.lineColor = sender.backgroundColor;
}
回到MayView.m中
- (void)drawRect:(CGRect)rect {
// 设置颜色
// [[UIColor blackColor] setStroke];
// 渲染(遍历数组中所有路径,进行绘制)
for (MayPath * path in self.allPaths) {
//为路径渲染相应的颜色
[path.pathColor setStroke];
//渲染路径
[path stroke];
}
}
3-6.开始描线
// 3.设置起点
// 3-1.获取点击对象
UITouch * touch =[touches anyObject];
// 3-2.获取点击的位置
CGPoint currentBeganPoint = [touch locationInView:touch.view];
// 4.设置起点
[path moveToPoint:currentBeganPoint];
//每次单击都有效果
[path addLineToPoint:currentBeganPoint];
//重绘
[self setNeedsDisplay];
}
//点击移动- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent *)event {
// 1.获取点击对象
UITouch * touch = [touches anyObject];
// 2.获取点击位置
CGPoint currentMovedPoint = [touch locationInView:touch.view];
// 3.将点击起点处 连线 到当前点(取得起点:就是数组中的最后一个)
[[self.allPaths lastObject] addLineToPoint:currentMovedPoint];
// 4.重绘/渲染 view
[self setNeedsDisplay];
}
三、清屏、撤销、橡皮 功能
在MayView.h声明方法,在.m中实现,在控制器类中为三个按钮拖线并且分别调用各自的方法
//清屏
- (void)clearScreen {
//将路径数组清空
[self.allPaths removeAllObjects];
// 重绘 view
[self setNeedsDisplay];
}
//撤销
- (void)goBack {
//清除路径最后一条
[self.allPaths removeLastObject];
// 重绘 view
[self setNeedsDisplay];
}
//橡皮
- (void)erase{
self.lineColor = self.backgroundColor;
}
四、保存到相册功能
4-1.在MayView.h声明方法,在.m中实现
4-2.图片类型的上下文:其实就是改目标。开启图片类型的上下文:(默认是view,但——只要你开启了图片类型的上下文,那么渲染的目标就是内部的UIImage对象
4-3.以后比较常用的开启图片上下文的方法:
UIGraphicsBeginImageContextWithOptions(大小,不透明,缩放)
大小:确定后里面图片就是这个大小
缩放:设置为0就是根据你的屏幕设置它的点倍数
4-4.有开启图片上下文就要有关闭;UIGraphicsEndImageContext
4-5.小技巧
和绘图有关的方法都是CGContext开头
与上下文有关的方法是UIGraphics开头
//保存
- (void)saveToPhoto {
//开启图片类型的上下文
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
//图层内容给与到上下文
[self.layer renderInContext:context];
//获取图片
UIImage * img = UIGraphicsGetImageFromCurrentImageContext();
//将图片保存到相册
UIImageWriteToSavedPhotosAlbum(img, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);
// 关闭图片类型上下文
UIGraphicsEndImageContext();
}
注意:保存到相册的那个方法UIImageWriteToSavedPhotosAlbum:它内部假如要调用一个方法只能是(image:didFinishSavingWithError:contextInfo)。官方文档有说明;
- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo {
if (error) {
NSLog(@"保存失败");
}
else NSLog(@"保存成功");
}
在控制器类中为保存按钮拖线并且调用方法
//保存
- (IBAction)beSave:(UIBarButtonItem *)sender {
//弹出提示框
UIAlertController * alert = [UIAlertController alertControllerWithTitle:@"将保存到相册" message:@"请输入图片的名字:" preferredStyle:UIAlertControllerStyleAlert];
[alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
}];
[self presentViewController:alert animated:YES completion:nil];
// 添加按钮
UIAlertAction * cancel = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil];
UIAlertAction * confirm = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
// 文件保存名字
UITextField * tf = alert.textFields[0];
self.paintView.imgName = [NSString stringWithFormat:@"%@.png",tf.text];
[self.paintView saveToPhoto];
}];
[alert addAction:cancel];
[alert addAction:confirm];
}