上篇博客《iOS可視化動態繪制八種排序過程》可視化了一下一些排序的過程,本篇博客就來聊聊圖的東西。在之前的博客中詳細的講過圖的相關內容,比如《圖的物理存儲結構與深搜、廣搜》。當然之前寫的程序是比較抽象的。上篇博客我們以可視化的方式看了一下各種排序的過程,今天博客中我們就來可視化的看一下圖的相關部分,今天我們要畫的圖是無向圖,並且每個點到其他點都有直接的連線。今天我們就基於此圖來做一些事情。當然本篇博客在畫圖時我們使用的是Bezier曲線來畫的,因為之前也聊過關於Bezier的相關東西,所以今天就不對Bezier做過多贅述了。
今天的博客我們有易到難大致分為三個部分。第一部分我們會畫出相應的圖,並該圖是可以對每個點進行拖動的,在拖動的過程中,我們對其進行重繪。第二部分會取消拖動,使用UIView自帶的動畫來讓其自己變換,當然本部分你也可以使用Timer或者GCD的TimerSource讓其運動。第三部分則是第二部分的升級,再第二部分的基礎上我們稍作改進,此部分我們使用的是DispatchSourceTimer來讓每個點進行運動的。在第三部分我們讓局部范圍的點進行連線,也就是在運動的過程中,我們需要找出在當前點的規定范圍內有哪些點,然后將這些點進行連接。
上述這三部分的內容下方會詳細的進行介紹,並會附有相應的運行結果圖。接下來就進入我們的主題部分。
一、圖的繪制
在本篇博客的第一部分我們要按照要求先把圖給繪制出來,我們會隨機的生成幾個坐標點,然后在這些坐標點上添加上View,然后再將這些坐標點使用Bezier進行連接。當然,在連接時我們使用的是鄰接矩陣來記錄的每兩點之間的關系。在繪制的過程中,我們會隨機的為每個點每條邊分配顏色。
當相應的圖繪制好后,我們需要為每個點添加上Move事件,在對每個點進行拖動時,我們會及時的重新繪制整個圖的關系。下方就是我們本部分要實現內容的運行效果,如下所示:
如果理解了數據結構中圖的構建,實現上述效果,並不困難。解析來我們就來看一下實現上述效果的核心代碼。
1、圖的節點View的封裝
首先我們來封裝上述圖的節點View,當然此節點View的封裝比較簡單。核心就在於給每個節點View添加一個TouchesMoved事件,然后在TouchesMoved事件執行時,將觸摸的移動點設置成當前View的Center即可。這樣我們就可以拖動每個節點View了。在拖動節點View時,我們還需要將拖動的事件回調到節點View的父視圖上,讓父視圖知道當前用戶拖動的是哪個View。接下來我們就來看一下節點View的核心代碼。
下方這段代碼的上一部分就是我們定義的一個閉包類型,用來將節點View的觸摸事件回調給父視圖。該閉包類型需要傳一個參數,該參數就是當前View的Tag, 這樣父視圖就知道當前用戶拖動的是哪個節點了。
而randomColor()函數則是用來負責隨機生成顏色的,上面每次顏色的變化都是使用的下方這個函數所隨機生成的UIColor對象。
下方這段就是節點View的TouchesMoved事件,在該事件中我們獲取到當前用戶觸摸移動的坐標點,然后將該點賦值給當前節點View的Center,然后調用更新父視圖的閉包回調對象即可。如下所示:
2、圖View的封裝
接下來我們要實現畫圖的View了,也就是上述節點View的父視圖了。父視圖主要負責的工作內容就是創建上述的節點View,然后使用Bezier將每個節點進行連接即可。當然,在用戶拖動相應的View的時候,需要對當前圖進行重繪。
下方這個方法就是往父視圖上添加相應的節點視圖,在節點視圖初始化后,要設置一個閉包回調,該回調用來移動后圖的重繪。在該閉包回調中,我們會調用drawLine()方法。當然在創建節點View時,我們也創建了相應的BezierPath的對象。每個節點對應一個BezierPath對象,用來繪制該節點所連節點的線。具體代碼如下所示:
我們整個圖的關系是存儲在鄰接矩陣中的,所以我們要對鄰接矩陣進行創建,在重繪時要對該鄰接矩陣進行初始化。下方就是該鄰接矩陣創建和初始化的代碼,關於鄰接矩陣的內容在此就不做過多贅述了,具體內容請參考之前的博客。
節點View和鄰接矩陣的准備工作完成后,接下來就是畫線的工作了。下方就是畫線的核心代碼,在畫線之前我們要先將相應的BezierPath對象上的點移除掉,然后再添加上新的點,最后就是進行重繪了。在往BezierPath對象上添加點時,我們要將節點的關系在鄰接矩陣中進行記錄。如果兩個點之間已經畫完線了,那么鄰接矩陣上的內容我們設置為true,未畫線的節點之間則是false。具體代碼如下所示。
在上述方法調用setNeedsDisplay()方法后,就會執行View的draw()方法,我們就在此方法中進行線條的繪制。當然下方的代碼比較簡單,在此就不做過多贅述了。
上述這些代碼就是本部分所展示的效果圖核心代碼,完整示例請移步本篇博客末尾的github分享鏈接。
二、圖的自動變換
上一部分是我們手動的拖動讓創建的圖進行變換的,接下來我們對上述代碼進行改造一下,使其自動的進行變換。在點自動移動時,如果碰到屏幕的邊界,我們讓其反彈接着進行移動。下方就是我們本部分要實現的效果。
當然有了第一部分作為基礎,我們實現本部分的效果並不復雜。我們需要做的事情是隨機生成每個節點所移動的方向。然后判斷移動時是不是超出屏幕范圍,如果超出屏幕范圍我們就要對運動方向進行修正,讓其往反方向進行移動。本部分我們只需要修改節點View,而節點View的父視圖不做修改。
下方這段代碼片就是為了讓其自動變換所實現的方法。下方的這兩個方法會替換掉第一部分的TouchesMoved方法。下方的randomIncrement()方法用來生成當前View的x坐標和y坐標的偏移量。x的偏移量為1則表示往右運動,-1表示往左運動。y的偏移量為1則往下運動,-1則是往上運行。
下方的changePoint()就是根據x和y的偏移量不斷修改當前節點View的坐標的方法。為了簡單,此處使用了UIView自帶的Animate來實現的。在修改x和y坐標的值時要判斷是否超出屏幕邊距,如果超出屏幕邊界就往反方向移動。為了讓點一直運動下去,我們需要不斷的調用changePoint()方法,如下所示。當然每調用一次changePoint()方法,我們就需要調用一下重繪的回調。具體代碼如下所示。
三、特定區域內畫圖
接下來我們要做的就是繼續在上述內容中做一些東西。在節點自動運動的過程中,我們不把所有的點都連接起來,本部分要做的事情是當點運動時,我們以改點為中心划定個區域,如果有其他點在該區域內,我們就將該區域內的點進行連接。如果點在運動的過程中超出了划定的范圍,那么我們就去除之前畫的線。效果如下所示:
本部分主要修改的內容是節點View的父視圖,核心就是要計算當前點與周圍點的距離,如果該距離小於我們規定的距離的話,那么我們就畫線,否則就不畫線。下方代碼片段就是本部分的核心代碼。主要就是往貝塞爾上添加點時進行距離的判斷。下方的countDistance()函數就是用來計算兩點之間直線距離的函數,在areaPoints()中調用了該函數來確定當前區域中的點。核心代碼如下所示:
四、點擊新增節點
本部分也將在上述部分的代碼上進行更新。該部分要做的事情是點擊屏幕,往屏幕上添加新的節點。這一點在上述基礎上實現是比較簡單的。只需給節點的父View添加上新的節點即可。下方就是第四部分要實現的效果,每點擊一次屏幕,就會在屏幕點擊的地方生成一個節點,該節點就會運動。具體效果如下所示。
要想實現上述效果,下方是我們修改的代碼片段。就是給父視圖添加了一個TouchesEnded事件,在點擊的地方生成一個節點View即可。具體如下所示:
本篇博客Demo的github分享地址為:https://github.com/lizelu/FlyOver