美文网首页Android知识Android技术知识Android开发
Android绘图软件开发(1)-框架概述

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

作者: 阿堃堃堃堃 | 来源:发表于2017-10-30 15:46 被阅读0次

    引言

    不知道您有没有厌倦了做一个诸如学生管理、仓库管理、图书馆管理的系统?除了增删改查还是增删改查,做完后会感觉成就感很少,因为这样的系统已经遍地开花了,很难给人以新鲜感和冲击力。
    今天想讲讲自己的一个小软件,一个基于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()方法即可完美满足需要。

    结语

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

    相关文章

      网友评论

        本文标题:Android绘图软件开发(1)-框架概述

        本文链接:https://www.haomeiwen.com/subject/qaappxtx.html