Android繪圖軟件開發(fā)(1)-框架概述

引言

不知道您有沒有厭倦了做一個(gè)諸如學(xué)生管理、倉庫管理、圖書館管理的系統(tǒng)?除了增刪改查還是增刪改查,做完后會(huì)感覺成就感很少,因?yàn)檫@樣的系統(tǒng)已經(jīng)遍地開花了,很難給人以新鮮感和沖擊力。
今天想講講自己的一個(gè)小軟件,一個(gè)基于Android平臺(tái)開發(fā)的繪圖APP。這個(gè)APP加入了很多新鮮和創(chuàng)新的元素,不僅僅是繪圖這么簡單,但出于篇幅與重點(diǎn)考慮,本章僅講解對(duì)于繪圖軟件傳統(tǒng)功能的開發(fā)思路,且盡量脫離某一具體平臺(tái)講解(本文以Android平臺(tái)為例,采用java語言描述),不過度深入實(shí)現(xiàn)細(xì)節(jié),而僅給出一個(gè)可行性、維護(hù)性和擴(kuò)展性都較好的開發(fā)架構(gòu)。

繪圖軟件有什么

PS、CDR、Windows畫圖……相信大家對(duì)繪圖軟件并不陌生,三下五除二就總結(jié)出了它的基本功能,下面是我總結(jié)的:

  1. 畫圖形:可以畫直線、曲線、折線、隨筆線、圓形、橢圓、矩形、多邊形等
  2. 編輯圖形:可以選中、平移、縮放、旋轉(zhuǎn)、拷貝、刪除圖形
  3. 填充圖形:可以對(duì)畫布上任意封閉區(qū)域填充顏色
  4. 調(diào)整顏色:提供一個(gè)調(diào)色板,改變畫筆的顏色
  5. 調(diào)整畫筆:提供若干風(fēng)格迥異的畫筆

開發(fā)思路

說實(shí)話,最初看到這么多功能,我也是一頭霧水,無從下手。但仔細(xì)思考,通過歸納這些功能的特性與共性,會(huì)發(fā)現(xiàn)這5大功能其實(shí)分為兩類:

1. 有狀態(tài)功能

這些功能只有在被選中了后才能生效,且他們之間是互斥使用的。比如當(dāng)選中了“畫圓按鈕”后,在畫布上繪出的就是圓;當(dāng)選中了“畫矩形按鈕”后,在畫布上繪出的就是矩形;當(dāng)選中了“平移按鈕”后,就可以對(duì)畫布上的任一圖形進(jìn)行平移;當(dāng)選中了“填充按鈕”后,點(diǎn)擊畫布就會(huì)填色。上節(jié)的前3大功能均屬于該類。

2. 無狀態(tài)功能

這些功能被觸發(fā)后,隨即生效。比如點(diǎn)擊“調(diào)色板按鈕”后選擇畫筆顏色,確認(rèn)后顏色馬上發(fā)生改變;點(diǎn)擊“畫筆按鈕”后選擇畫筆樣式,確認(rèn)后也會(huì)立馬生效。上節(jié)的后2大功能均屬于該類。
劃分好這兩大類功能后,思路就明朗了很多,因?yàn)闊o狀態(tài)功能不外乎就是對(duì)一些全局參數(shù)的設(shè)置,是很容易實(shí)現(xiàn)的。下面我們先講下有狀態(tài)功能中“畫圖形”是怎么實(shí)現(xiàn)的。

抽取圖形類

畫布上的每個(gè)圖形都有自己獨(dú)一無二的形狀、所占區(qū)域、顏色、風(fēng)格,因此我們可以馬上抽取出圖形類Pel的結(jié)構(gòu):

class Pel
{
    Path path; //形狀軌跡
    Region region; //所占區(qū)域
    Paint paint; //風(fēng)格與顏色
}

Path、Region、Paint三個(gè)類都是Android SDK中自帶的,其中:path負(fù)責(zé)存儲(chǔ)圖形的軌跡,可通過調(diào)用它的若干繪制函數(shù)結(jié)合坐標(biāo)形成;region負(fù)責(zé)存儲(chǔ)圖元所構(gòu)成的區(qū)域,可由path轉(zhuǎn)換得到,用處是方便選中圖形;paint負(fù)責(zé)指定該圖形的樣式,包括了畫筆風(fēng)格和顏色。

存儲(chǔ)圖形

圖形是有了,但它們都是相對(duì)獨(dú)立的個(gè)體,我們還需要建立合適的數(shù)據(jù)結(jié)構(gòu)統(tǒng)一管理它們??紤]到用戶繪制的圖形個(gè)數(shù)是沒有限制的,繪制過程中涉及對(duì)圖形的頻繁增刪,這里我們選擇用一個(gè)鏈表List<Pel> pelList序列化存儲(chǔ)繪制在畫布上的圖形,如下圖所示。


獲取圖形坐標(biāo)

圖形的存儲(chǔ)已經(jīng)有了一個(gè)歸宿,但要繪制出圖形來,我們肯定需要知道坐標(biāo),那坐標(biāo)是怎么獲取到的呢?這里就需要引出圖層類View,它的內(nèi)部有一個(gè)onTouchEvent(MotionEvent event)的回調(diào)方法,用戶對(duì)這個(gè)圖層進(jìn)行觸摸時(shí)都會(huì)調(diào)用,且將觸摸事件類型(如手指落下事件、移動(dòng)事件、抬起事件等)和觸摸數(shù)據(jù)(如坐標(biāo))封裝進(jìn)了MotionEvent對(duì)象中,下面是獲取坐標(biāo)的代碼框架:

public boolean onTouchEvent(MotionEvent event)
{
    float x = event.getX();
    float y = event.getY();
    switch (event.getAction())
    {
        case MotionEvent.ACTION_DOWN:{處理落下事件};break;
        case MotionEvent.ACTION_MOVE:{處理移動(dòng)事件};break;
        case MotionEvent.ACTION_UP:{處理抬起事件};break;
    }
    return true;
}

繪制圖形

我們知道:path用來存儲(chǔ)圖形的軌跡,坐標(biāo)指示了用戶手指所在的位置,如果能把坐標(biāo)“畫”進(jìn)path里面,就實(shí)現(xiàn)了圖形的存儲(chǔ)。所幸的是,Path類提供了這樣的函數(shù)替我們轉(zhuǎn)換,如畫隨筆線quadTo()、畫矩形addRect()、畫橢圓addOval()等。具體怎么實(shí)現(xiàn)呢?其實(shí)繪制就是圍繞以上三個(gè)觸摸事件展開的,下面給出事件處理的大致思路:

  • MotionEvent.ACTION_DOWN:利用Path類的moveTo()方法固定軌跡的起始點(diǎn)
  • MotionEvent.ACTION_MOVE:利用Path類的各種繪制方法,以當(dāng)前坐標(biāo)作為參數(shù)繪出圖形,并刷新到畫布上
  • MotionEvent.ACTION_UP:確定圖形的最終軌跡,構(gòu)建pel對(duì)象,存入pelList中

擴(kuò)展更多功能

其實(shí)實(shí)現(xiàn)“畫圓”并不難,實(shí)現(xiàn)“畫矩形”也不難,難的是要實(shí)現(xiàn)上面“所有的”有狀態(tài)功能,這該如何是好?有兩種方案:

1. 用狀態(tài)標(biāo)志實(shí)現(xiàn)

相信大家會(huì)說,這很簡單嘛:既然上面說了這些有狀態(tài)功能間是互斥使用的,那么就給他們一人一個(gè)狀態(tài)標(biāo)志,當(dāng)用戶點(diǎn)擊進(jìn)入某一有狀態(tài)功能時(shí),將當(dāng)前狀態(tài)置為該標(biāo)志,然后用戶觸摸屏幕時(shí),會(huì)觸發(fā)onTouchEvent函數(shù),這時(shí)獲得坐標(biāo)后,用if語句判斷當(dāng)前是哪種狀態(tài)(是畫圓?畫矩形?平移圖形?縮放圖形?……),然后每個(gè)外層if語句里面再進(jìn)一步用switch語句判斷當(dāng)前的觸摸事件類型,最后針對(duì)不同事件進(jìn)行不同處理,完事。
這…好吧,為什么我隱隱地感到一絲不安…如果有狀態(tài)功能很少,用這種方法尚且還行(實(shí)際上也很繁瑣),但如果功能很多,比如10個(gè),可以計(jì)算下總共有多少個(gè)條件分支:10(判狀態(tài))+10*3(判事件)=40!如果說那10條“判狀態(tài)”的分支還算有實(shí)際意義的話,那另外30條“判事件”的分支簡直就是過度冗余和重復(fù)了。
這種實(shí)現(xiàn)方法的弊端是顯而易見的:

  • 代碼量大:條件判斷語句很多,代碼很冗余
  • 容易出錯(cuò):硬編碼,人工地定義狀態(tài),人工地進(jìn)行條件判斷,人工對(duì)應(yīng)他們的關(guān)系,一不留神就出錯(cuò)了
  • 可讀性不好:連續(xù)40個(gè)條件分支,每個(gè)分支下面又有對(duì)應(yīng)的處理語句,總之根本沒法讀
  • 可維護(hù)性不好:同上,代碼太多太復(fù)雜,如果想要修改一個(gè)功能,要先用ctrl+f搜狀態(tài)標(biāo)志,再搜這個(gè)狀態(tài)下的事件標(biāo)志,再修改,眼前信息量很大,不好維護(hù)
  • 可擴(kuò)展性不好:如果要新加個(gè)有狀態(tài)功能,需要找到那一堆條件分支,在最后補(bǔ)上一個(gè)else if,里面再加個(gè)switch,最后針對(duì)不同事件給出不同處理,擴(kuò)展工作量很大。
    嗯,所以這個(gè)實(shí)現(xiàn)方法注定是不可取的,是有違開發(fā)規(guī)范和初衷的。下面我介紹一種自己想的方法,若有不足歡迎大家指正。
2. 用繼承和多態(tài)實(shí)現(xiàn)

上面那種方法的思考角度本質(zhì)還是面向過程,它關(guān)注的焦點(diǎn)是如何一步一步先后地去實(shí)現(xiàn),這種過程是鼠目寸光的,必然有失對(duì)全局的考慮。而既然采用的是java語言,那我們就要充分利用它面向?qū)ο蟮奶攸c(diǎn),將關(guān)注的焦點(diǎn)轉(zhuǎn)換為一個(gè)個(gè)的對(duì)象。
由于這些有狀態(tài)功能都是一類,所以它們之間必然存在共同的屬性與操作,而剩下的就是它們各自特有的屬性與操作了。一旦我們定義好了共性的東西(接口、基類),就只用專注于去實(shí)現(xiàn)特性的東西(接口實(shí)現(xiàn)、子類覆寫)了,從而輕松完成開發(fā),不僅如此,還兼顧了程序的可維護(hù)性和擴(kuò)展性。這就是程序的模塊化設(shè)計(jì)的好處。
那么問題來了?有狀態(tài)功能間的共性是什么?特性又是什么?很簡單,你想,無論是畫圓,還是畫矩形,或是平移圖形,不外乎上面提到的手指落下、手指移動(dòng)、手指抬起這三個(gè)事件(這就是共性部分),我們要分別為這些有狀態(tài)功能分別編寫3種事件的處理代碼,這些處理代碼是互不相同、獨(dú)一無二的(這就是特性部分)。
既然提到共性,沒錯(cuò),馬上想到的就是繼承。我們很容易抽象出一個(gè)觸摸的基類Touch,它定義三個(gè)公共方法down()、move()、up(),再定義若干公共屬性如x、y、eventType等,再由具體的“子類Touch”去繼承這個(gè)基類Touch,在公共方法中實(shí)現(xiàn)自己的特性操作,其關(guān)系如下面類圖所示。



上面只是利用繼承搭好了若干類及他們的關(guān)系,但落實(shí)到具體實(shí)現(xiàn)上,還需要借助多態(tài)。多態(tài)最妙的一點(diǎn)就是:指向子類對(duì)象的基類引用可以調(diào)用子類覆寫過的方法,什么意思呢,也就是上面方案1龐雜的條件分支可以神奇地簡寫成這樣了:

//聲明一個(gè)全局的Touch對(duì)象
Touch touch = null;
//畫圓按鈕
public void onDrawOvalBtn(View view)
{
    touch = new DrawOvalTouch();
}
//畫矩形按鈕
public void onDrawRectBtn(View view)
{
    touch = new DrawRectTouch();
}
......
//平移圖形按鈕
public void onDragPelBtn(View view)
{
    touch = new DragPelTouch();
}
......
public boolean onTouchEvent(MotionEvent event)
{
    float x = event.getX();
    float y = event.getY();
    touch.setPoint(x,y); //傳遞坐標(biāo)
    switch (event.getAction())
    {
        case MotionEvent.ACTION_DOWN:touch.down();break;
        case MotionEvent.ACTION_MOVE:touch.move();break;
        case MotionEvent.ACTION_UP:touch.up();break;
    }
    return true;
}

怎么樣,是不是很神奇,為什么能簡化這么多呢,甚至一條if語句都沒有寫,那就是因?yàn)槲覀儼褩l件判斷都交給多態(tài)去處理了,又由于子類touch繼承了基類touch,當(dāng)前處于哪種狀態(tài),當(dāng)前touch對(duì)象的類別就自帶了含義和區(qū)分的功能,當(dāng)子類new給touch的時(shí)候,touch已然“記住”了當(dāng)前狀態(tài)是哪個(gè),然后再判斷下觸摸事件類型,對(duì)應(yīng)調(diào)用當(dāng)前子類touch的down()、move()、up()方法即可完美滿足需要。

結(jié)語

先就寫這么多啦。本人水平有限,加上第一次寫這種技術(shù)文章,思路難免有點(diǎn)混亂,若有不足的地方懇請大家批評(píng)指正哈。下面一章我會(huì)繼續(xù)深入講解“編輯圖形”功能的設(shè)計(jì)與實(shí)現(xiàn),今天就先到這里吧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容