引言
不知道您有没有厌倦了做一个诸如学生管理、仓库管理、图书馆管理的系统?除了增删改查还是增删改查,做完后会感觉成就感很少,因为这样的系统已经遍地开花了,很难给人以新鲜感和冲击力。
今天想讲讲自己的一个小软件,一个基于Android平台开发的绘图APP。这个APP加入了很多新鲜和创新的元素,不仅仅是绘图这么简单,但出于篇幅与重点考虑,本章仅讲解对于绘图软件传统功能的开发思路,且尽量脱离某一具体平台讲解(本文以Android平台为例,采用java语言描述),不过度深入实现细节,而仅给出一个可行性、维护性和扩展性都较好的开发架构。
绘图软件有什么
PS、CDR、Windows画图……相信大家对绘图软件并不陌生,三下五除二就总结出了它的基本功能,下面是我总结的:
- 画图形:可以画直线、曲线、折线、随笔线、圆形、椭圆、矩形、多边形等
- 编辑图形:可以选中、平移、缩放、旋转、拷贝、删除图形
- 填充图形:可以对画布上任意封闭区域填充颜色
- 调整颜色:提供一个调色板,改变画笔的颜色
- 调整画笔:提供若干风格迥异的画笔
开发思路
说实话,最初看到这么多功能,我也是一头雾水,无从下手。但仔细思考,通过归纳这些功能的特性与共性,会发现这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()方法即可完美满足需要。
结语
先就写这么多啦。本人水平有限,加上第一次写这种技术文章,思路难免有点混乱,若有不足的地方恳请大家批评指正哈。下面一章我会继续深入讲解“编辑图形”功能的设计与实现,今天就先到这里吧。