画板听起来很难,其实做起来很简单。主要很多人不明白思路,所以觉得难以入手。
场景:实现清除、撤销、重做、照片、保存、颜色、线宽等功能的画板。
一、功能开发流程
1.1. UI使用storyboard进行快速搭建
1.2. 自定义绘图区域
画板功能相对来说比较聚合,所有的功能最终都是通过drawRect
绘制到画板上的,同时为了方便其他项目使用,我们尽量把功能都集成在一个画板上。
画板使用手势计算路径会更加合理高效,所以我们使用滑动手势来绘制。
DBDrawboardView.m
1 | @implementation DBDrawboardView |
1.3. 基本线条绘制
线条的绘制使用贝塞尔曲线,只需要两个点就可以连成一条直线。当两个点之间的距离很小的时候(颗粒度)就会觉得线条很自然。
第一步,先在手势开始的时候创建一个贝塞尔曲线(每次手势只会创建一次),并记录起点;
第二步,手势状态发生改变时,把手势移动的位置点拼接到曲线路径上,最后渲染就行了。
DBDrawboardView.m
1 | @interface DBDrawboardView () |
通过上面的简单几行代码,就可以绘制出基本线条了。但是每次只能绘制一条连续的线条,正常画板是可以绘制不同状态多个线条的,其实只需要添加一个路径栈就行。
1.4. 路径栈
使用栈来保存每一条路径,等需要绘制的时候,从栈底依次取出。OC中最适合的容器栈就是数组了。
1 | // 保存历史栈 |
1.4和1.3的diff:
1 | @property (nonatomic, strong) NSMutableArray *pathArray; |
这样,整个画板的基础功能就算完成了,只需要把其他高级功能完善。
1.5. 设置颜色和线宽
颜色和线宽在自定义的画板view内部实现后,只需要对外提供接口。由于UIBezierPath
不支持设置线条颜色,所以我们需要新建一个子类扩展一个颜色属性。
DBDrawboardView
1 | // DBDrawboardView.h |
1.6. 橡皮擦
橡皮擦是真的没有什么要说的,就是一个和画板背景色一样的路径。会Photoshop的同学应该不陌生,毕竟橡皮擦掉的东西也是可以撤回的。
1 | // 橡皮擦 |
1.7. 清空
把当前路径栈元素清空,重新绘制。
1 | [self.pathArray removeAllObjects]; |
1.8. 撤回和重做
同样可以对比Photoshop的历史记录面板,撤回的时候可以重做,重做后还可以撤回,重做的思路和撤回基本一致,一个操作的是当前路径栈,一个操作的移除栈。但是撤回或重做的时候,不管层级关系是怎样的,只要有任何影响画板重绘的操作,重做都会失效。
撤回
把路径栈栈顶的路径去除,同时把该路径添加到移除栈中,重新绘制。
1 | if (self.pathArray.count != 0) { |
重做
把移除栈中的栈顶路径移除,并压入到当前栈,重新绘制。
1 | if (self.removeArray.count != 0) { |
但是仅仅这样做还是有问题的,上面提到其他任何操作都会影响重做是否有效。所以我们需要记录下每个功能的状态,更新重做功能的时效性。并且橡皮擦之后有清空操作,画板颜色如果没有调整,会看不到绘制的内容。
1.9. 优化
新建一个记录画板不同绘制状态的变量。每次操作都记录对应状态,然后及时更新画板就行了。
DBDrawboardView
1 | // DBDrawboardView.h |
1.10. 保存
把画板内容截屏保存到本地相册。
注意:相册权限需要配置。
1 | // 保存 |
1.11. 添加照片
照片可以缩小、平移、旋转等形变操作,使用自定义view装载图片,并把形变操作直接应用到这个view上会更好。
如果直接把图片添加到画板上,将来绘制图片的时候图片就会和画板一样大,不是我们要的效果。如果我们把图片先放到一个和画板尺寸一样的透明view上,在这个透明view上进行形变操作,最后把透明view渲染到画板上就可以解决问题了。
1.11.1. 自定义装载图片的view
DBImageContainerView
1.11.2. 渲染
在画板视图中绘制添加的图片DBImageContainerView.m
1 | // 添加照片 |
二、完整代码及效果
ViewController.m
1 |
|
DBDrawboardView
1 | // DBDrawboardView.h |
DBImageContainerView