在 iOS 上进行平滑的手绘

原网址:https://code.tutsplus.com/tutorials/smooth-freehand-drawing-on-ios--mobile-13164

博客中英文转载链接:http://blog.csdn.net/u013410274/article/details/78894413

整理的代码地址 :http://download.csdn.net/download/u013410274/10203776


该文章中文采取的直接网页翻译而来


本教程将教您如何在iOS设备上实现高级绘图算法,以实现流畅的手绘。继续阅读

触摸是用户与iOS设备交互的主要方式。这些设备预期提供的最自然和最明显的功能之一是允许用户用手指在屏幕上画画。目前App Store中有许多徒手绘制和记笔记应用程序,许多公司甚至要求客户在购买时签署iDevice。这些应用程序如何实际工作?让我们停下来思考一下“引擎盖下”是怎么回事。

当用户滚动表格视图,捏放大图片,或在绘画应用程序绘制曲线时,设备显示正在快速更新(例如,每秒60次),应用程序运行循环不断采样用户的手指的位置。在此过程中,拖动屏幕的手指的“模拟”输入必须转换为显示器上的数字点集,并且此转换过程可能构成重大挑战。在我们的绘画应用程序的背景下,我们手上有一个“数据拟合”的问题。当用户在设备上愉快地涂写时,程序员必须插入iOS中报告给我们的采样触点中丢失的模拟信息(“连接点”)。而且,这种内插必须以这样的方式发生,即对于终端用户来说,结果是连续的,自然的,平滑的笔画,就好像他正在用纸笔在笔记本上画草图一样。

本教程的目的是展示如何在iOS上实现徒手画,从一个执行直线插值的基本算法开始,并推进到一个更接近于像Penultimate这样的着名应用程序提供的质量的更复杂的算法好像创建一个工作起来的算法不够困难,我们也需要确保算法运行良好。正如我们将看到的,一个天真的绘图实现可能会导致一个具有重大性能问题的应用程序,这将使绘图繁琐,最终无法使用。

我假设你对iOS开发并不是全新的东西,所以我已经略过了创建一个新项目,向这个项目添加文件的步骤等等。希望这里没有任何困难,但是为了以防万一完整的项目代码可供您下载和玩耍。

基于“ 单一视图应用程序 ”模板启动一个新的Xcode iPad项目,并命名为“ FreehandDrawingTut ”。一定要启用自动引用计数(ARC),但取消选择故事板和单元测试。您可以使这个项目是一个iPhone或通用的应用程序,这取决于你有什么样的设备可供测试。

新项目

接下来,继续在Xcode Navigator中选择“FreeHandDrawingTut”项目,并确保只支持纵向:

只支持肖像

如果您要部署到iOS 5.x或更早版本,则可以通过以下方式更改方向支持:

我正在这样做,以保持简单,所以我们可以专注于主要的问题。

我想迭代地开发我们的代码,并以渐进的方式改进代码 - 就像你从头开始实际操作一样 - 而不是一下子把最终版本放在你的头上。我希望这种方法能让你更好地处理不同的问题。记住这一点,为了避免在同一个文件中反复删除,修改和添加代码,这可能会变得混乱和容易出错,我将采取以下方法:

  • 对于每个迭代,我们将创建一个新的UIView子类。我将发布所有需要的代码,以便您可以简单地复制并粘贴到您创建的新UIView子类的.m文件中。不会有视图子类的功能的公共接口,这意味着你将不需要触摸.h文件。
  • 为了测试每个新版本,我们需要将我们创建的UIView子类指定为当前占用屏幕的视图。我将向您展示如何使用Interface Builder首次执行此操作,详细地完成这些步骤,然后在每次编写新版本时提醒您这一步。

在Xcode中,选择File> New> File ...,选择Objective-C类作为模板,然后在下一个屏幕上命名文件LinearInterpView并将其设置为UIView的子类保存。名称“LinearInterp”是“线性插值”的缩写。为了本教程,我将命名每个我们创建的UIView子类,以强调在类代码中引入的一些概念或方法。

正如我前面提到的,你可以保留头文件。删除LinearInterpView.m文件中的所有代码,并将其替换为以下内容:

在此代码中,我们直接处理应用程序每次触摸序列时向我们报告的触摸事件。也就是说,用户将手指放在屏幕视图上,将手指移过屏幕,最后将手指从屏幕上抬起。对于这个序列中的每个事件,应用程序向我们发送相应的消息(在iOS术语中,消息被发送到“第一响应者”;可以参考文档以获得详细信息)。

为了处理这些消息,我们实现了-touchesBegan:WithEvent:在UIView继承的UIResponder类中声明的方法和公司。我们可以编写代码来处理触摸事件,无论我们喜欢什么。在我们的应用程序中,我们要查询触摸的屏幕位置,做一些处理,然后在屏幕上画线。

这些点来自上面的代码相应的评论数字:

  1. 我们重写-initWithCoder:是因为视图是由XIB生成的,因为我们将很快建立。
  2. 我们禁用了多个触摸:我们将只处理一个触摸序列,这意味着用户一次只能用一个手指进行绘制; 在此期间放置在屏幕上的任何其他手指都将被忽略。这是一种简单化,但不一定是不合理的 - 人们通常不会一笔一笔地在纸上写字!无论如何,这会让我们不能离开太远,因为我们已经有足够的工作去做了。
  3. UIBezierPath是一个UIKit类,可让我们在由直线或某些类型的曲线组成的屏幕上绘制形状。
  4. 由于我们正在做自定义绘图,我们需要重写视图的-drawRect:方法。每次添加新的线段时,我们都会通过抚摸路径来完成此操作。
  5. 还要注意的是,虽然线宽是路径的属性,线本身的颜色是绘图上下文的属性。如果你不熟悉图形上下文,可以在Apple文档中阅读。现在,将图形上下文想象成当您重写-drawRect:方法时绘制的“画布” ,并且所看到的结果就是屏幕上的视图。我们很快就会遇到另一种绘图环境。

在构建应用程序之前,我们需要将刚刚创建的视图子类设置为屏幕视图。

  1. 在导航窗格中,单击ViewController.xib(如果你创建了一个通用的应用程序,只需进行这一步的两个视图控制器〜iPhone.xib视图控制器〜iPad.xib文件)。
  2. 当视图在界面生成器画布上显示时,点击它将其选中。在实用程序窗格中,单击“标识检查器”(窗格顶部右侧的第三个按钮)。最上面的部分说“自定义类”,这是你将设置你点击的视图的类。
  3. 现在应该说“UIView”,但我们需要改变它(你猜对了)LinearInterpView输入类的名称(只需键入“L”应使自动完成在安定地钟声)。
  4. 同样,如果您要将其作为通用应用程序进行测试,请为模板为您创建的两个XIB文件重复此确切步骤。
将视图控制器的视图类更改为我们的自定义UIView子类

现在构建应用程序。你应该得到一个闪亮的白色的视图,你可以用你的手指画。考虑到我们编写的几行代码,结果并不是太简单!当然,他们也不是很壮观。连接点的外观是相当明显的(是的,我的手写也吸)。

我们第一次尝试徒手画

确保你不仅在模拟器上而且在真实的设备上运行应用程序。

如果您在设备上使用应用程序一段时间,您一定会注意到一些事情:最终,UI响应开始滞后,而不是由于某种原因每秒获取的〜60个触点,用户界面能够进一步采样下降。由于点越来越分离,直线插值使绘图甚至比以前更“块”。这当然是不可取的。发生什么了?

让我们回顾一下我们已经做的事情:当我们绘制时,我们获取点,将它们添加到不断增长的路径中,然后在主循环的每个循环中渲染*完整*路径。所以随着路径变长,在每一次迭代中,绘图系统都有更多的绘制,最终变得太多,使得应用难以跟上。由于一切都在主线上发生,我们的绘图代码与UI代码竞争,其中包括在屏幕上对触摸进行采样。

你会被原谅的,认为有一种方法可以在屏幕上显示已经存在的内容。不幸的是,这是我们需要摆脱纸上笔的类比的地方,因为图形系统默认情况下不是那样工作的。虽然凭借我们接下来要写的代码,但我们间接地要实施“借鉴”方法。

虽然有几件事情我们可能会试图解决我们的代码的性能,但我们只是实现一个想法,因为事实证明,这足以满足我们目前的需求。

创建一个新的UIView子类像之前,将其命名为CachedLIView(LI的是提醒我们我们还在做大号 inear  nterpolation)。删除CachedLIView.m的所有内容,并将其替换为以下内容:

保存之后,请记住将XIB中的视图对象的类更改为CachedLIView!

当用户将他的手指放在屏幕上画画时,我们从一条没有点或线的新路径开始,并且像前面一样向它添加线段。

再次提到评论中的数字:

  1. 我们另外在内存中维护与我们的画布(即在屏幕视图)上相同大小的(离屏)位图图像,其中我们可以存储我们迄今为止绘制的内容。
  2. 每当用户提起手指时(通过-touchesEnded:WithEvent发送信号),我们将屏幕上的内容绘制到此缓冲区中。
  3. drawBitmap方法创建一个位图上下文 - UIKit方法需要一个“当前上下文”(画布)来绘制。当我们进入-drawRect:这个环境时,会自动提供给我们,并反映我们在屏幕视图中绘制的内容。相反,位图上下文需要被显式地创建和销毁,并且绘制的内容驻留在存储器中。
  4. 通过以这种方式缓存之前的图形,我们可以摆脱之前的路径内容,并以这种方式避免路线变得太长。
  5. 现在每次drawRect:调用,我们首先将内存缓冲区的内容绘制到我们的视图中(在设计上)具有完全相同的大小,因此对于用户,我们保持连续绘制的幻觉,只是以不同于以前的方式。

虽然这不是完美的(如果我们的用户在不举起手指的情况下继续绘画,那会怎样?),这对于本教程的范围来说已经足够了。鼓励你自己试验,找到更好的方法。例如,您可以尝试周期性地缓存图形,而不是仅当用户举起手指时。碰巧,这个离屏缓存过程为我们提供了后台处理的机会,如果我们选择实施它的话。但是我们不打算在本教程中这样做。尽管你被邀请自己尝试!

现在让我们把注意力转移到使图画“看起来更好”。到目前为止,我们已经用直线段连接相邻的触点。但通常当我们徒手画画的时候,我们的自然中风有一个自由流动的曲线(而不是块状和刚性的)。我们尝试用曲线而不是线段插入我们的点是有道理的。幸运的是,UIBezierPath类让我们绘制它的同名曲线:贝塞尔曲线。

什么是贝塞尔曲线?在不调用数学定义的情况下,贝塞尔曲线由四个点定义:一条曲线通过的两个端点和两个“控制点”,它们有助于定义曲线在其端点处必须接触的切线(技术上这是一条三次贝塞尔曲线,但为简单起见,我将它简称为“贝塞尔曲线”)。

一个三次贝塞尔曲线

贝塞尔曲线允许我们绘制各种有趣的形状。

立方贝齐尔有能力做的有趣的形状

我们现在要尝试的是对四个相邻接触点的序列进行分组,并在Bezier曲线段内插入点序列。为了保持笔画的连续性,每一对相邻的贝塞尔段将共享一个共同的端点。

你现在知道演习。创建一个新的UIView子类并将其命名为BezierInterpView将以下代码粘贴到.m文件中:

正如在线评论所指出的那样,主要的变化是引入了一些新变量来跟踪贝塞尔曲线中的点,并修改-(void)touchesMoved:withEvent:了每四个点绘制一个贝塞尔曲线的方法(实际上每三个点,就应用程序向我们报告的情况而言,因为我们为每一对相邻的贝塞尔分段共享一个端点)。

您可能会在这里指出,在我们有足够的点来完成最后的Bezier段之前,我们忽略了用户举起手指并结束触摸顺序的情况。如果是这样,你会是对的!虽然在视觉上这并没有太大的区别,在某些重要的情况下,它确实如此。例如,尝试绘制一个小圆圈。它可能不完全关闭,在一个真正的应用程序中,你想要在-touchesEnded:WithEvent方法中适当地处理这个虽然我们在这里,但我们也没有特别注意触摸取消的情况。touchesCancelled:WithEvent实例方法处理这个。看看官方文档,看看是否有任何特殊情况需要在这里处理。

那么,结果是什么样的?我再次提醒您在建立之前在XIB中设置正确的课程。

如果有的话,稍作改进

呵呵。这似乎不是一个很大的改进,是吗?我认为这可能比直线插值稍好一些,或许这只是一厢情愿的想法。无论如何,没有什么值得吹嘘的。

以下是我认为正在发生的事情:虽然我们不费力地用平滑的曲线段插入四个点的每个序列,但是我们没有努力使曲线段平滑过渡到下一个曲线段,所以有效地仍然有最终结果的问题。

那么我们能做些什么呢?如果我们要坚持我们在最后一个版本中开始的方法(即使用贝塞尔曲线),则需要考虑两个相邻贝塞尔分段的“交点”的连续性和平滑性。在相应的控制点(第一段的第二控制点和第二段的第一控制点)的终点处的两个切线似乎是关键; 如果这两个切线都具有相同的方向,则曲线在交叉点处将会更平滑。

切线是两个贝塞尔段的交界处

如果我们将公共端点移动到连接两个控制点的线路上?在不利用关于接触点的附加数据的情况下,最好的一点似乎是考虑到连接两个控制点的线的中点,并且我们对于两个切线的方向所强加的要求将得到满足。我们来试试吧!

移动交点以使分段间过渡平滑

创建一个UIView子类(再次),并命名为SmoothedBIView。将.m文件中的所有代码替换为以下内容:

上面讨论的算法的关键是在-touchesMoved:WithEvent:方法中实现。内联评论应该帮助您将讨论与代码链接起来。

那么,从视觉上来说,结果如何呢?记得用XIB做这件事。

一点也不差!

令人高兴的是,这一次有了实质性的改善。考虑到我们修改的简单性,它看起来相当不错(如果我自己说的话)。我们对前一次迭代的问题的分析以及我们提出的解决方案也进行了验证。

我希望你发现这个教程是有益的。希望你能就如何改进代码开发你自己的想法。如前所述,您可以合并的最重要的(但简单的)改进之一是更加优雅地处理触摸序列的结束。

另一个我忽略的情况是处理一个触摸序列,其中包括用户用手指触摸视图,然后将其提起而不用移动它 - 有效地在屏幕上敲击。用户可能期望在这个视图上绘制一个点或小的曲线,但是对于我们当前的实现,没有任何反应,因为除非我们的视图接收到-touchesMoved:WithEvent:消息,否则我们的绘图代码不会启动您可能需要查看UIBezierPath类文档以查看您可以构建的其他类型的路径。

如果你的应用比我们在这里做了更多的工作(在一个值得送货的绘图应用中,它会!),设计它使得非UI代码(特别是离屏缓存)在后台线程中运行在多核设备(iPad 2以上)上有显着的不同。即使在像iPhone 4这样的单处理器设备上,性能也应该得到改善,因为我预计处理器会分配缓存工作,毕竟每隔几个主循环周期只发生一次缓存工作。

我鼓励你展开自己的编程技巧,并使用UIKit API来开发和改进本教程中实现的一些想法。玩得开心,谢谢你的阅读!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值