美文网首页Android开发经验谈Android开发移动开发
点读笔写字App(3)——画布写字细节

点读笔写字App(3)——画布写字细节

作者: Crawl_W | 来源:发表于2016-06-27 18:08 被阅读188次

    【如果你想了解这个点读笔写字App的背景,请移步这里
    http://www.jianshu.com/p/ee2a1bb99280

    写前唠叨

    现在开始写的这篇,距离上次的结文的时间节点已有大半个月的跨度。最近换了新东家,想着好好调节一下状态,改变之前早上萎靡的状态。早上上班基本不再看程序师的博文,而是回顾昨天所学&写code&学习。中午会保证20min的休息。晚上回来倒是如之前一样,白天想着回来做点学习和总结,回来却是懒得啥也不想做,所以写东西也就一天天搁置。

    做了这些改变之后,整体感觉还是不错的,一天下来收获比之前要多。只是下午3.左右 整个人就很烦躁,头脑发热效率低下,这种状态一直持续到晚上。主要原因是那个时候大脑有点疲劳(我周末都是在这个点之前躺床上睡一个多小时的),然后又只能逼着自己学习,引起了厌烦的情绪。我试着用坚持的方式逼着自己调整生物钟,但是往往适得其反。我不知道大家是否会遇到这样的状况,毕竟每个人的情况都会不同。

    如果有上面的情况的朋友,我的建议是不要逼着自己做,去做感兴趣的事。我自己的感觉是疲劳的时候仍逼着自己去学习往往会适得其反,引起厌烦;我也尝试过这时候去网上看看博客、段子、新闻,不过很多时候我看着看着就不知道我看到哪了,甚至有时候在那打瞌睡了。我们需要面对一个现实——人不可能全天都保持高效的学习状态,既然在那段时间效率低下还不如让自己“放假”。因为如果你是个自我驱动很强的人(我相信有一部分程序员是这样的,别的职业可能大抵如此,程序员不是第三类人),效率低下带来的自责、烦躁等消极情绪往往会抵消那点可怜的收获。

    接下的日子,我准备尝试在那段时间里写写东西,做些总结来整理整理自己的逻辑。一方面写点东西理清逻辑的话,会对自己所做东西的把握,容易地获得自信;另一方面,写东西的时候,我常常一晃几个小时,却由于时不时回去阅读(ps1),总写不了多少字。同样效率低下,但是之后非但不消极,反而很愉悦,想到说不定会对别人有帮助甚至很兴奋,我谓之“上乘的兴趣”。这与打篮球不同,打得好就开心,打得不好就不舒服,而且之类的体育运动又不方便在工作时间开展,这样的兴趣就不适合用在这里调节自我了。如果是听音乐之类,总能让你心情大的悦话,那不妨试试。另外要提醒一下的就是,要注意“放假”和惰性界线的把控,别玩大了。你们是如何度过每天中这段“困难时期”的?可以在下面告诉我。
    ps1:我一直没搞清楚自己为什么常常要回读,要是在古代,“才思敏捷”、“一气呵成”这样的成语也不会因为我而发明,如果我生的好人家,大概可以贡献一个“才思不敏”的寓言故事。

    前面啰嗦太多,下面直接开始写字细节的内容吧。

    获得数据并处理

    本来这个App是必须配合一个硬件的设备来完成的,因为可能涉及到公司的一些利益就不介绍了。设备最终会不停的给我传回一个int型的数据,这个int型可以对应到某一页的某个特定位置,而app要做的就是把这些int型转换成图片上的涂黑的像素点。每隔一段时间app都会从设备读得一个数值,读到有效值就通过Handler类发消息给画图类处理。当然,由于书写是一个连续的过程,读取数据的时间间隔要设置得恰到好处,使得app能够及时读取到硬件获取的数值,而不会因为硬件上覆盖了上次未来得及被app读取的数据,造成遗漏数据的情况。另外,又要避免过密的读写给cpu带来的压力。App的暂定实现方式是这样的,但我不认为这样方式是很好的决定,最后我会来说这个问题。

    在app中,画图类和数据读取类是不同的两个类。现在画图类中继承Handler实例化自己的内部类MyHandler,重载其handleMessage()方法实现消息处理策略。然后实例化MyHandler,把该实例传给数据读取类,在数据读取类中通过实例发送消息触发消息处理函数。代码如下:

    //画图类中
    private class MyHandler extends Handler
    {
      @Overridepublic 
      void handleMessage(Message msg) {}
    }
    private final MyHandler PageHandler = new MyHandler();
    //画图类是一个活动类,然后onCreate使用PageHandler实例化画图类把实例传过去即可
    
    //数据读取类中
    PageHandler.sendMessage(msg);//msg包裹读取的数据
    

    在MyHandler的消息处理函数中,首先会去判断我们获得的一个值,如果该值是笔书写过程中获得的值,则按照他们接受的顺序存储到数组中;如果该值代表着笔抬起的动作,那么则将数组中的点统一解析成baseBtimap上的像素点。

    //void handleMessage(Message msg)的实现
    
    if(nIndex == nUpPenCode)
    {   
      //没有真正绘画前两值相等(初始化);你变态到不在在本上写字戳笔尖触发笔抬起消息,do nothing.
      if(nCurScribingPage != nJustScribedPage)
      {
          //重写一页时,保存刚才所画页到本地
          if(baseBitmap != null)
          {
             SaveForInitLoad(baseBitmap, fCurDrewPage);
             baseBitmap.recycle();//由于Bitmap在内存中占据很大的内存,容易出现OOM的状况,提醒系统及时回收内存
             baseBitmap = null;
          }
          CreateCurCanvas();//note:等会会在下面贴代码
          nJustScribedPage = nCurScribingPage;
      }
      if(!Indexs.isEmpty())
      {
         DrawAllPoints();
      }
    }else
    {
       if(Indexs.isEmpty())
          nCurScribingPage = (nIndex-1) / nPointNumPerPg + 1;
       Indexs.add((nIndex - 1) % nPointNumPerPg + 1);
    }
    

    先看handleMessage中if语句前半分支中添加if(nCurScribingPage != nJustScribedPage)的判断,如果还在同一页书写,则跳过CreateCurCanvas(),直接在原来baseBitmap上DrawAllPoints();到另一页上面书写,则保存图片到本地,重新创建一块画布。在往数组中添加数值时,由于我们书写的连续性,不太可能一笔下来会写到别的页上面,所以我只需要根据要添加进数组的第一个值判断往第几页上面写就可以了。上面的else里面就做了解析int型数值,解析成到哪一页书写的一个相对数值。那些加1减1的计算实际上是为了处理边缘值的问题,这种先加1,完了再减1的处理在处理边缘问题的时候很常见,下面我们还会见到。

    private void CreateCurCanvas()
    {
       fnCurDrewPage = DrewContent_Path + "/" +
             FormatPageNo(nCurScribingPage) + ".png";
       fCurDrewPage = new File(fnCurDrewPage);
       if(fCurDrewPage.exists())
       {
          BitmapFactory.Options opts = new BitmapFactory.Options();
          opts.inMutable = true;
          baseBitmap = BitmapFactory.decodeFile(fnCurDrewPage, opts);
          canvas = new Canvas(baseBitmap);
       }else
       {
          BitmapFactory.Options opts = new BitmapFactory.Options();
          opts.inMutable = true;
          baseBitmap = BitmapFactory.decodeResource(getResources(),
               R.drawable.page_background, opts);
          canvas = new Canvas(baseBitmap);
       }
    }
    

    如何初始化一张画布,之前篇章已经讲过了,这里不在重复。这个画布是哪来的呢?当在一张白纸上书写的时候是从资源文件里导入一张原始图片,而在写过的纸上书写则导入之前保存有痕迹的图片,然后接着在上面解析像素点。

    实时显示

    通过前面的介绍,可以知道书写内容的实时显示并不是一个点一个点地去响应的,而是在笔抬起来的时候统一响应之前一笔连贯的书写。显示的实现是在DrawAllPoints()中:

    private void DrawAllPoints()
    {
       for(int i = 0; i < Indexs.size(); ++i)
       {
          DecodeToPt(Indexs.get(i));
       }
       Indexs.clear();
       UpdateAfterDraw();//以上在内存中写,写完更新显示当前图片}
    

    DecodeToPt()函数中就是数值对应到像素点的解析了,解析的过程就是int型数对应到画板一定数目像素点的过程:

    private void DecodeToPt(int nCode)
    {
        int nColumn = (nCode-1)/nXPointNum + 1;
        int nRow = (nCode-1)%nXPointNum + 1;
        int y = (nColumn-1)*nYSpanPerPoint + 1;
        int x = (nRow-1)*nXSpanPerPoint + 1;
        canvas.drawRect(x, y, x+nXSpanPerPoint-1, y+nYSpanPerPoint-1, paint);
    }
    

    nXPointNum表示每页一行的定位点数,nXSpanPerPoint表示每个定位点对应的像素宽,带“Y”则对应到Y轴的意思。还记得我前面有篇,点一个点涂黑一个方块的那张图吗?图片像素800*800,一个框100*100,框的标号从1-64。这里的100就是nSpanPerPoint。
    UpdateAfterDraw()所做的事情就是将内存上画好图显示到IU。

    private void UpdateAfterDraw(){
       getDatas(DrewContent_Path);
       Map<String, Object> listItem = new HashMap<>();
       listItem.put("image", baseBitmap);
       listItem.put("pageNo", FormatPageNo(nCurScribingPage));//File name represents for page No.
       int nPosOfItem = getItemPos(mListItems, FormatPageNo(nCurScribingPage));
       if(nPosOfItem == -1)//找不到业表示新的页,则添加,重新排序;否则,直接更新数据
       {
          mListItems.add(listItem);
          //按页从大到小排序
       }else
          mListItems.set(nPosOfItem, listItem);
       msimpleAdapter.notifyDataSetChanged();
       int nPosAfterUpdate = mListItems.indexOf(listItem);
       gridView.smoothScrollToPosition(nPosAfterUpdate);
    }
    
    //通过map中一个字段的value获取该map在list中的位置,找不到返回-1
    private int getItemPos(List<Map<String, Object>> items, String s)
    {
       for (int i = 0; i < items.size(); ++i)
       {
          if ((items.get(i).get("pageNo")).equals(s))
             return i;
       }
       return -1;
    }
    

    逻辑上没有什么复杂之处,以上代码做了3件事:正确的位置添加图片,显示到UI,滚动到当前书写页图。后面两件事,一个前面篇章有讲到,后面一个一句事(除了在手势滑动的时候假想旁边有个滑条外没什么好说的,与这里也没什么关系)。而第一个就是下面要讲的了。

    有序的页图

    要做到有序,自然就需要排序了。上面注释的地方就是下面这段代码了:

    Collections.sort(mListItems, new Comparator<Map<String, Object>>(){
          @Override
          public int compare(Map<String, Object> stringObjectMap, Map<String, Object> t1)
          {
             String str1 = (String) stringObjectMap.get("pageNo");
             String str2 = (String) t1.get("pageNo");
             if(str1 != null)
                return str1.compareTo(str2);
             return 0;
          }
      });
    

    这里给大家回顾一下mListItems中放着listItem,listItem是什么——是Map<String, Object>,其中放了两个键值对,键都是String,值分别对应了Bitmap类型的页图和String类型的页码,这里排序之后的结果是listItem之间的有序序列。代码中让mListItems根据给出的比较子进行比较,而比较子中重载了compare函数。比较的规则是按照每个map中pageNo字段的String字符串相对ASCII码的顺序。也许大家看到这里似乎可以明白我为什么要把页码格式化成“01”这样的格式了,当然用“1”这样的也是可以的。可以看到这里compare起作用的就是其返回值,我们用“1”形式的就不能调用compare了,但是可以转换成整数,直接比较结果返回-1,0,1。至于需要正序时t1>t2的情况应该返回1还是-1,自己试一试。以前研究过比较子排序的实现代码,现在又忘掉了,如果知道的朋友麻烦在下面评论里告诉大家。

    大家可以猜一猜还有哪里需要排序的?应该是无序数据需要显示UI的时候吧?机智的小伙伴会告诉我,打开app初始化记录的时候,因为那时候要进行本地数据到UI的显示。当然,不知道应该在初始化的时候的朋友也并不能说明你不机智,因为你很可能不知道从本地读取的文件数据到数组中是无序的。按理说确实应该在初始化时getDatas后,再copy一次上面排序的算法,或者写个函数出来分别调用。这里我把这个排序放到了getDatas中:

    File[] allFiles = file.listFiles();
    if (allFiles == null)
    {
       return ;//if file not a dir return null.need to test condition whit none!
    }
    Arrays.sort(allFiles, new Comparator<File>()
    {
       @Override
       public int compare(File file, File t1)
       {
          return file.getName().compareTo(t1.getName());
       }
    });
    for (File f : allFiles)
    {
        //拼出图片和页码到listItem,listItem就是存了两个键值对的 
        //Map<String, Object>,还记得是啥吗?刚刚说过
        mListItems.add(listItem);
    }
    

    注意其中文件数组Arrays.sort(),这么做首先是为了符合初始化显示的有序要求,当我按照文件名有序读取数据后,mListItems中存着序的页图,可以直接显示。另外,这么做是基于一个效率问题的考虑。我们知道这里的文件并不是真正读取到内存中的文件的2进制数据,而是一个本地文件的引用,所以这里占据的内存是很小的。而上面对mListItems排序,里面太多东西啦,一个图片占据那么多内存,当我们排序的时候内存中需要搬动那么大的数据,而且是多次搬动,太辛苦啦!当你开始体谅计算机的时候,说不定有意想不到的收获;如果你说不让计算机代替人做复杂的运算是体谅的话,那是抬杠!他不计算叫他“计算机”干嘛?而我们知道,当大部分的数据是有序的时候,放入一个数据,然后让其排序到同序(正序或逆序),代价要比排列完全无序所做的数据搬动少得多。哪个实践者做个例子给大家证明一下就更好了,我也同求looklook(好懒啊,每次写一篇东西要几天)。

    实现之后的一些感想

    app是为了做出笔记本的效果,每一页都是一张图片。我们希望做到的效果是动过笔的页才显示出来。事实上做到后面我不太认同这样的方式。我并不认为这样做的显示效果会有多么好。既然网格的布局(见点读笔写字App(2)中的图片)可以方便地选择进入某一页(像现实中根据页码翻页一样),为什么不把所有的页都显示出来,干净的页就留出空白页图。我们是会有一本和app配套的笔记本,上标记了页码,但是app也应该清楚地告诉使用者本子的页数。

    现在的做法不仅一定程度上增加了app的实现难度,而且不利于了解保存的图片慢慢增加,何时会出现OOM(out of memory)的问题(我们需要保证适量的页数即使全部页图读入内存都不会出现OOM)。当我们已经在往UI显示前往内存读入了所有的页图,为什么不让其常驻内存,书写时只在内存中修改。我们要做的只是在打开和关闭App时做IO的读写。

    至于文章的前半部分提到关于获取硬件数据策略的问题,感觉实际的应用中,读取硬件的响应速度是很难做到适度的,我不知道那些写字最快的人的速度极限是多少,纵然天空不是他们的界线。我一直主张里面应该设置一个差不多的值,然后一些点交给一个模糊算法去生成,这样我们就不需要精确地响应每一个点。毕竟我们写字的一些点都是连续的,有起承转合的趋势,有迹可循,算法也可以实现。当然,公司没有做算法的同事,就一直没人鸟我。而我自己也只是说说,却是行动的矮人(主要没弄过的东西,就感觉学习曲线很陡)。感觉找一下应该会有现成的开源算法吧?

    想听听你们谈谈你们是如何想?

    相关文章

      网友评论

        本文标题:点读笔写字App(3)——画布写字细节

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