热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

POP绘图库Asana/Drawsana源代码看看

iOS绘制就是采集点,贝塞尔曲线得到形状,绘图上下文去渲染出来AsanaDrawsana图形库,设计的挺好他可以画多种图形,

iOS 绘制就是采集点,贝塞尔曲线得到形状,绘图上下文去渲染出来

Asana/Drawsana 图形库,设计的挺好

他可以画多种图形,画线、文本、橡皮擦、五角形、矩形、箭头、角度,

他支持多种操作,撤销上一步、还原上一步,平移选择的已渲染图形

他的实现,大量使用了协议

设计: 主要看数据结构

可以分为三个层次,行为的处理 ( 采集点的传递 ) ,图形的绘制, 呈现的视图( 最开始采集点, 最后的渲染呈现 )

图形的绘制

Shape 协议,决定了可看 ( 可渲染 ),可点击

ShapeSelectable 协议,增加了形状区域和仿射变换。

最基础的形状,Shape

有一个 id、 类型的区分,

渲染出来,可否响应点击事件,

更改绘图设置 ( 画线的颜色、画线的填充色、画线的宽度 )


public protocol Shape: AnyObject, Codable {var id: String { get set }static var type: String { get }func render(in context: CGContext)func hitTest(point: CGPoint) -> Boolfunc apply(userSettings: UserSettings)
}

通用的形状协议 ShapeSelectable

下面 3 个协议,添加功能

ShapeWithBoundingRect 协议继承自 Shape, 添加了区域

public protocol ShapeWithBoundingRect: Shape {var boundingRect: CGRect { get }
}

ShapeWithTransform 协议继承自 Shape, 添加了仿射变换


public protocol ShapeWithTransform: Shape {var transform: ShapeTransform { get set }
}

最终形状通用的协议为 ShapeSelectable,

他继承自上面两个协议


public protocol ShapeSelectable: ShapeWithBoundingRect, ShapeWithTransform {
}

具体的形状,以角度形状为例 AngleShape

角度这个形状,需要三个点确定


class AngleShape: ShapeSelectable{public var a: CGPoint = .zeropublic var b: CGPoint = .zeropublic var c: CGPoint = .zero// ... // 实现通用的形状信息, id 、种类、线宽等
}

这里还有一个协议, ShapeWithThreePoints

该协议拿到了三个点,可算出三个点决定的矩形区域

extension AngleShape: ShapeWithThreePoints{}public protocol ShapeWithThreePoints {var a: CGPoint { get set }var b: CGPoint { get set }var c: CGPoint { get set }var strokeWidth: CGFloat { get set }
}

ShapeWithThreePoints 三点形状协议,统一处理了三个点形状的区域;

这个库的形状,大部分是 ShapeWithTwoPoints , 2 点形状协议,统一处理了 2 点形状的区域,

椭圆、星星、矩形、线段,都是 2 点形状

行为的处理,这个库是 tool

工具 tool 是形状 shape 的进一步封装

当前使用工具的通用模版

工具的通用模版 DrawingTool 包含如下信息

public protocol DrawingTool: AnyObject {// 正在进行var isProgressive: Bool { get }// 工具名称var name: String { get }// 用户手指点击func handleTap(context: ToolOperationContext, point: CGPoint)// 用户手指刚滑动// 形状的拖拽刚开始 / 图形的绘制刚开始func handleDragStart(context: ToolOperationContext, point: CGPoint)// 用户手指滑动正在进行// 形状的拖拽继续 / 图形的绘制继续func handleDragContinue(context: ToolOperationContext, point: CGPoint, velocity: CGPoint)// 用户手指滑动结束了// 结束形状的拖拽 / 结束图形的绘制func handleDragEnd(context: ToolOperationContext, point: CGPoint)// 用户手指滑动取消了// 取消形状的拖拽 / 取消图形的绘制func handleDragCancel(context: ToolOperationContext, point: CGPoint)// 修改绘图设置,绘图的颜色func apply(context: ToolOperationContext, userSettings: UserSettings)}

当前使用工具的功能模版

使用工具 tool 的功能模版,

有两点形状的工具,有 3 点形状的工具,有画线形状的工具 ( pen ),

有选择工具 …

这里的例子是 2 点形状的工具,DrawingToolForShapeWithTwoPoints

他里面有一个属性, 正在画的形状 shapeInProgress,

该形状通过两个点决定

pen class DrawingToolForShapeWithTwoPoints: DrawingTool {public typealias ShapeType = Shape & ShapeWithTwoPointsopen var name: String { fatalError("Override me") }// shapeInProgress 通过下面的 makeShape() 方法创建public var shapeInProgress: ShapeType?open func makeShape() -> ShapeType {fatalError("Override me")}// ...// 实现了 DrawingTool 协议的方法
}

当前使用的具体工具

因为前面的协议,定义的比较完善,

大量通用的代码,放到了协议和父类里面,

所以具体绘制的工具,实现比较简洁

// 画线工具
public class LineTool: DrawingToolForShapeWithTwoPoints {public override var name: String { return "Line" }public override func makeShape() -> ShapeType { return LineShape() }
}// 箭头工具
public class ArrowTool: DrawingToolForShapeWithTwoPoints {public override var name: String { return "Arrow" }public override func makeShape() -> ShapeType {let shape = LineShape()shape.arrowStyle = .standardreturn shape}
}// 矩形工具
public class RectTool: DrawingToolForShapeWithTwoPoints {public override var name: String { return "Rectangle" }public override func makeShape() -> ShapeType { return RectShape() }
}// ...

呈现的视图


最开始采集点,通过自定制手势

重写触摸方法,采集到点


class ImmediatePanGestureRecognizer: UIGestureRecognizer {// 开始触摸override func touchesBegan(_ touches: Set, with event: UIEvent) {// ...}// 画线中/ 拖拽视图中override func touchesMoved(_ touches: Set, with event: UIEvent) {// ...}// 结束触摸override func touchesEnded(_ touches: Set, with event: UIEvent) {// ...}}

最后的渲染呈现

绘图需要两张 UIImage,

画一笔,把之前绘制的 UIImage 渲染出来,把正在画的那一笔渲染出来,得到绘制的视图,也就是第二张 UIImage

这里用了三张 UIImage, 一张最终版本 persistentBuffer,

把之前绘制的 UIImage 渲染出来 , transientBuffer

得到绘制的视图,也就是第二张 UIImage , transientBufferWithShapeInProgress

public class DrawsanaView: UIView {private var persistentBuffer: UIImage?private var transientBuffer: UIImage?private var transientBufferWithShapeInProgress: UIImage?

对应的处理代码,

这里是一个画线/平移操作的绘制匿名函数, updateUncommittedShapeBuffers,


let updateUncommittedShapeBuffers: () -> Void = {self.transientBufferWithShapeInProgress = DrawsanaUtilities.renderImage(size: self.drawing.size) {// 把之前绘制的 UIImage 渲染出来self.transientBuffer?.draw(at: .zero)// 把正在画的那一笔渲染出来self.tool?.renderShapeInProgress(transientContext: $0)}// 得到绘制的视图 transientBufferWithShapeInProgress// 得到绘制的视图, 呈现self.drawingContentView.layer.contents = self.transientBufferWithShapeInProgress?.cgImage// 正在进行,就更新if self.tool?.isProgressive == true {self.transientBuffer = self.transientBufferWithShapeInProgress}}

实现


  • POP, 使用协议,能够避免大量的重复代码

  • 如果面向过程写,好理解写。逻辑都在一坨。因为同样的代码,到处拷贝,不好维护,容易出错

例子,角度的绘制

角度形状,有 3 个点

public class AngleShape: ShapeWithThreePoints, ShapeWithStrokeState, ShapeSelectable{public static let type: String = "Angle"public var id: String = UUID().uuidStringpublic var a: CGPoint = .zeropublic var b: CGPoint = .zeropublic var c: CGPoint = .zero}

角度的画线方法


public func render(in context: CGContext) {// 开始绘制transform.begin(context: context)// 绘图效果的设置context.setLineCap(capStyle)context.setLineJoin(joinStyle)context.setLineWidth(strokeWidth)context.setStrokeColor(strokeColor.cgColor)if let dashPhase = dashPhase, let dashLengths = dashLengths {context.setLineDash(phase: dashPhase, lengths: dashLengths)} else {context.setLineDash(phase: 0, lengths: [])}// 画角度的两根线context.move(to: a)context.addLine(to: b)context.move(to: b)context.addLine(to: c)context.strokePath()// 画中间的角度弧形,和角度文字renderInfo(in: context)// 结束绘制transform.end(context: context)}

调用下面的,绘制中间的角度弧形,和角度文字


private func renderInfo(in context: CGContext) {// 计算开始角,和结束角if a == c {return}let center = bvar startAngle = atan2(a.y - b.y, a.x - b.x)var endAngle = atan2(c.y - b.y, c.x - b.x)if 0

调用,绘制角度大小的文字


private func renderDegreesInfo(in context: CGContext, startAngle: CGFloat, endAngle: CGFloat) {// 得到角度的大小,富文本let radius: CGFloat = 44let fontSize: CGFloat = 14let font = UIFont.systemFont(ofSize: fontSize)let string = NSAttributedString(string: "\(degreesBetweenThreePoints(pointA: a, pointB: b, pointC: c))°", attributes: [NSAttributedString.Key.font: font,NSAttributedString.Key.foregroundColor: strokeColor])// 计算出,摆放角度大小文本的位置let normalEnd = startAngle

通过三个点,计算出角度的大小

private func degreesBetweenThreePoints(pointA: CGPoint, pointB: CGPoint, pointC: CGPoint) -> Int {// 邻边let a = pow((pointB.x - pointA.x), 2) + pow((pointB.y - pointA.y), 2)// 邻边let b = pow((pointB.x - pointC.x), 2) + pow((pointB.y - pointC.y), 2)// 对边let c = pow((pointC.x - pointA.x), 2) + pow((pointC.y - pointA.y), 2)if a == 0 || b == 0 {return 0}return Int(acos((a + b - c) / sqrt(4 * a * b) ) * 180 / CGFloat.pi)}

操作可撤销,可还原的实现

绘制视图 DrawsanaView,里面有一个操作栈 operationStack

public class DrawsanaView: UIView {public lazy var operationStack: DrawingOperationStack = {return DrawingOperationStack(drawing: drawing)}()}

操作栈 DrawingOperationStack 的实现

这个类,里面有两个操作的数组,

作为栈使用,操作后进先出,

一般修改,都是改后面的

里面有三个方法,

撤销功能和还原功能,好理解

剩下的添加操作, func apply(operation

每次画完一笔,添加进 undoStack, 等待用户去操作

public class DrawingOperationStack {public private(set) var undoStack = [DrawingOperation]()var redoStack = [DrawingOperation]()// 添加操作public func apply(operation: DrawingOperation) {guard operation.shouldAdd(to: self) else { return }undoStack.append(operation)redoStack = []operation.apply(drawing: drawing)delegate?.drawingOperationStackDidApply(self, operation: operation)}/// 撤销操作@objc public func undo() {guard let operation = undoStack.last else { return }operation.revert(drawing: drawing)redoStack.append(operation)undoStack.removeLast()delegate?.drawingOperationStackDidUndo(self, operation: operation)}/// 恢复操作@objc public func redo() {guard let operation = redoStack.last else { return }operation.apply(drawing: drawing)undoStack.append(operation)redoStack.removeLast()delegate?.drawingOperationStackDidRedo(self, operation: operation)}
}

添加操作的逻辑

每个工具,触摸完成/ 使用完成,就添加一次操作


public func handleDragEnd(context: ToolOperationContext, point: CGPoint){// ...// 添加操作context.operationStack.apply(operation: AddShapeOperation(shape: shape))// ...}

这个库,挺有特色的。功能强大,就要考虑到方方面面,绕来绕去

推荐阅读
  • Java实现文本到图片转换,支持自动换行、字体自定义及图像优化
    本文详细介绍了如何使用Java实现将文本转换为图片的功能,包括自动换行、自定义字体加载、抗锯齿优化以及图片压缩等技术细节。 ... [详细]
  • golang常用库:配置文件解析库/管理工具viper使用
    golang常用库:配置文件解析库管理工具-viper使用-一、viper简介viper配置管理解析库,是由大神SteveFrancia开发,他在google领导着golang的 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 本文详细介绍了Java编程语言中的核心概念和常见面试问题,包括集合类、数据结构、线程处理、Java虚拟机(JVM)、HTTP协议以及Git操作等方面的内容。通过深入分析每个主题,帮助读者更好地理解Java的关键特性和最佳实践。 ... [详细]
  • 深入解析Spring启动过程
    本文详细介绍了Spring框架的启动流程,帮助开发者理解其内部机制。通过具体示例和代码片段,解释了Bean定义、工厂类、读取器以及条件评估等关键概念,使读者能够更全面地掌握Spring的初始化过程。 ... [详细]
  • 在Elasticsearch中,映射(mappings)定义了索引中字段的结构,类似于传统数据库中的表结构。虽然Elasticsearch支持字段的增删,但直接修改字段类型是不允许的。本文介绍了一种通过创建新索引并迁移数据的方式来改变字段类型的方法。 ... [详细]
  • 本文详细探讨了KMP算法中next数组的构建及其应用,重点分析了未改良和改良后的next数组在字符串匹配中的作用。通过具体实例和代码实现,帮助读者更好地理解KMP算法的核心原理。 ... [详细]
  • 本文详细介绍了Java中org.neo4j.helpers.collection.Iterators.single()方法的功能、使用场景及代码示例,帮助开发者更好地理解和应用该方法。 ... [详细]
  • 本文详细介绍如何使用Python进行配置文件的读写操作,涵盖常见的配置文件格式(如INI、JSON、TOML和YAML),并提供具体的代码示例。 ... [详细]
  • 本文详细介绍了如何构建一个高效的UI管理系统,集中处理UI页面的打开、关闭、层级管理和页面跳转等问题。通过UIManager统一管理外部切换逻辑,实现功能逻辑分散化和代码复用,支持多人协作开发。 ... [详细]
  • 在本教程中,我们将深入探讨如何使用 Python 构建游戏的主程序模块。通过逐步实现各个关键组件,最终完成一个功能完善的游戏界面。 ... [详细]
  • Android开发技巧:实现带描边的圆角图片
    本文介绍了一种在Android应用中实现带描边的圆角图片的方法。通过使用BitmapShader类,开发者可以轻松地为图片添加圆角和描边效果,提升应用的视觉体验。 ... [详细]
  • Android 自定义指南针视图实现
    本文介绍了如何在Android应用中自定义绘制指南针视图,包括方位角的计算、不同方向的颜色区分以及视图随手势移动的功能实现。 ... [详细]
  • nsitionalENhttp:www.w3.orgTRxhtml1DTDxhtml1-transitional.dtd ... [详细]
  • 当前,许多屏幕截图应用程序支持任意形状的截图功能。这引发了一个技术问题:如何高效地判断一个像素点是否位于指定的曲线或形状内部?本文将深入探讨这一问题,并提供一种简洁有效的解决方案。 ... [详细]
author-avatar
mobiledu2502895693
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有