Android绘图软件开发(1)-框架概述

引言

不知道您有没有厌倦了做一个诸如学生管理、仓库管理、图书馆管理的系统?除了增删改查还是增删改查,做完后会感觉成就感很少,因为这样的系统已经遍地开花了,很难给人以新鲜感和冲击力。
今天想讲讲自己的一个小软件,一个基于Android平台开发的绘图APP。这个APP加入了很多新鲜和创新的元素,不仅仅是绘图这么简单,但出于篇幅与重点考虑,本章仅讲解对于绘图软件传统功能的开发思路,且尽量脱离某一具体平台讲解(本文以Android平台为例,采用java语言描述),不过度深入实现细节,而仅给出一个可行性、维护性和扩展性都较好的开发架构。

绘图软件有什么

PS、CDR、Windows画图……相信大家对绘图软件并不陌生,三下五除二就总结出了它的基本功能,下面是我总结的:

  1. 画图形:可以画直线、曲线、折线、随笔线、圆形、椭圆、矩形、多边形等
  2. 编辑图形:可以选中、平移、缩放、旋转、拷贝、删除图形
  3. 填充图形:可以对画布上任意封闭区域填充颜色
  4. 调整颜色:提供一个调色板,改变画笔的颜色
  5. 调整画笔:提供若干风格迥异的画笔

开发思路

说实话,最初看到这么多功能,我也是一头雾水,无从下手。但仔细思考,通过归纳这些功能的特性与共性,会发现这5大功能其实分为两类:

1. 有状态功能

这些功能只有在被选中了后才能生效,且他们之间是互斥使用的。比如当选中了“画圆按钮”后,在画布上绘出的就是圆;当选中了“画矩形按钮”后,在画布上绘出的就是矩形;当选中了“平移按钮”后,就可以对画布上的任一图形进行平移;当选中了“填充按钮”后,点击画布就会填色。上节的前3大功能均属于该类。

2. 无状态功能

这些功能被触发后,随即生效。比如点击“调色板按钮”后选择画笔颜色,确认后颜色马上发生改变;点击“画笔按钮”后选择画笔样式,确认后也会立马生效。上节的后2大功能均属于该类。
划分好这两大类功能后,思路就明朗了很多,因为无状态功能不外乎就是对一些全局参数的设置,是很容易实现的。下面我们先讲下有状态功能中“画图形”是怎么实现的。

抽取图形类

画布上的每个图形都有自己独一无二的形状、所占区域、颜色、风格,因此我们可以马上抽取出图形类Pel的结构:

class Pel
{
    Path path; //形状轨迹
    Region region; //所占区域
    Paint paint; //风格与颜色
}

Path、Region、Paint三个类都是Android SDK中自带的,其中:path负责存储图形的轨迹,可通过调用它的若干绘制函数结合坐标形成;region负责存储图元所构成的区域,可由path转换得到,用处是方便选中图形;paint负责指定该图形的样式,包括了画笔风格和颜色。

存储图形

图形是有了,但它们都是相对独立的个体,我们还需要建立合适的数据结构统一管理它们。考虑到用户绘制的图形个数是没有限制的,绘制过程中涉及对图形的频繁增删,这里我们选择用一个链表List<Pel> pelList序列化存储绘制在画布上的图形,如下图所示。


获取图形坐标

图形的存储已经有了一个归宿,但要绘制出图形来,我们肯定需要知道坐标,那坐标是怎么获取到的呢?这里就需要引出图层类View,它的内部有一个onTouchEvent(MotionEvent event)的回调方法,用户对这个图层进行触摸时都会调用,且将触摸事件类型(如手指落下事件、移动事件、抬起事件等)和触摸数据(如坐标)封装进了MotionEvent对象中,下面是获取坐标的代码框架:

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:{处理移动事件};break;
        case MotionEvent.ACTION_UP:{处理抬起事件};break;
    }
    return true;
}

绘制图形

我们知道:path用来存储图形的轨迹,坐标指示了用户手指所在的位置,如果能把坐标“画”进path里面,就实现了图形的存储。所幸的是,Path类提供了这样的函数替我们转换,如画随笔线quadTo()、画矩形addRect()、画椭圆addOval()等。具体怎么实现呢?其实绘制就是围绕以上三个触摸事件展开的,下面给出事件处理的大致思路:

  • MotionEvent.ACTION_DOWN:利用Path类的moveTo()方法固定轨迹的起始点
  • MotionEvent.ACTION_MOVE:利用Path类的各种绘制方法,以当前坐标作为参数绘出图形,并刷新到画布上
  • MotionEvent.ACTION_UP:确定图形的最终轨迹,构建pel对象,存入pelList中

扩展更多功能

其实实现“画圆”并不难,实现“画矩形”也不难,难的是要实现上面“所有的”有状态功能,这该如何是好?有两种方案:

1. 用状态标志实现

相信大家会说,这很简单嘛:既然上面说了这些有状态功能间是互斥使用的,那么就给他们一人一个状态标志,当用户点击进入某一有状态功能时,将当前状态置为该标志,然后用户触摸屏幕时,会触发onTouchEvent函数,这时获得坐标后,用if语句判断当前是哪种状态(是画圆?画矩形?平移图形?缩放图形?……),然后每个外层if语句里面再进一步用switch语句判断当前的触摸事件类型,最后针对不同事件进行不同处理,完事。
这…好吧,为什么我隐隐地感到一丝不安…如果有状态功能很少,用这种方法尚且还行(实际上也很繁琐),但如果功能很多,比如10个,可以计算下总共有多少个条件分支:10(判状态)+10*3(判事件)=40!如果说那10条“判状态”的分支还算有实际意义的话,那另外30条“判事件”的分支简直就是过度冗余和重复了。
这种实现方法的弊端是显而易见的:

  • 代码量大:条件判断语句很多,代码很冗余
  • 容易出错:硬编码,人工地定义状态,人工地进行条件判断,人工对应他们的关系,一不留神就出错了
  • 可读性不好:连续40个条件分支,每个分支下面又有对应的处理语句,总之根本没法读
  • 可维护性不好:同上,代码太多太复杂,如果想要修改一个功能,要先用ctrl+f搜状态标志,再搜这个状态下的事件标志,再修改,眼前信息量很大,不好维护
  • 可扩展性不好:如果要新加个有状态功能,需要找到那一堆条件分支,在最后补上一个else if,里面再加个switch,最后针对不同事件给出不同处理,扩展工作量很大。
    嗯,所以这个实现方法注定是不可取的,是有违开发规范和初衷的。下面我介绍一种自己想的方法,若有不足欢迎大家指正。
2. 用继承和多态实现

上面那种方法的思考角度本质还是面向过程,它关注的焦点是如何一步一步先后地去实现,这种过程是鼠目寸光的,必然有失对全局的考虑。而既然采用的是java语言,那我们就要充分利用它面向对象的特点,将关注的焦点转换为一个个的对象。
由于这些有状态功能都是一类,所以它们之间必然存在共同的属性与操作,而剩下的就是它们各自特有的属性与操作了。一旦我们定义好了共性的东西(接口、基类),就只用专注于去实现特性的东西(接口实现、子类覆写)了,从而轻松完成开发,不仅如此,还兼顾了程序的可维护性和扩展性。这就是程序的模块化设计的好处。
那么问题来了?有状态功能间的共性是什么?特性又是什么?很简单,你想,无论是画圆,还是画矩形,或是平移图形,不外乎上面提到的手指落下、手指移动、手指抬起这三个事件(这就是共性部分),我们要分别为这些有状态功能分别编写3种事件的处理代码,这些处理代码是互不相同、独一无二的(这就是特性部分)。
既然提到共性,没错,马上想到的就是继承。我们很容易抽象出一个触摸的基类Touch,它定义三个公共方法down()、move()、up(),再定义若干公共属性如x、y、eventType等,再由具体的“子类Touch”去继承这个基类Touch,在公共方法中实现自己的特性操作,其关系如下面类图所示。



上面只是利用继承搭好了若干类及他们的关系,但落实到具体实现上,还需要借助多态。多态最妙的一点就是:指向子类对象的基类引用可以调用子类覆写过的方法,什么意思呢,也就是上面方案1庞杂的条件分支可以神奇地简写成这样了:

//声明一个全局的Touch对象
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); //传递坐标
    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语句都没有写,那就是因为我们把条件判断都交给多态去处理了,又由于子类touch继承了基类touch,当前处于哪种状态,当前touch对象的类别就自带了含义和区分的功能,当子类new给touch的时候,touch已然“记住”了当前状态是哪个,然后再判断下触摸事件类型,对应调用当前子类touch的down()、move()、up()方法即可完美满足需要。

结语

先就写这么多啦。本人水平有限,加上第一次写这种技术文章,思路难免有点混乱,若有不足的地方恳请大家批评指正哈。下面一章我会继续深入讲解“编辑图形”功能的设计与实现,今天就先到这里吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容