在 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文件中的所有代碼替換爲以下內容:

相關文章
相關標籤/搜索