【如果你想了解这个点读笔写字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);//按页从大到小排序}elsemListItems.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>>(){@Overridepublic 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>()
{@Overridepublic 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)——画布写字细节相关推荐

  1. 点读笔写字App(1)——从Drawable中获取图片画图

    [如果你想了解这个点读笔写字App的背景,请移步这里 http://www.jianshu.com/p/ee2a1bb99280 ] 直到这篇文章的时候,我并不知道在android App运行的过程中 ...

  2. 计算机写字的好处,喜欢写字的十大好处!

    原标题:喜欢写字的十大好处! 书法作为国人最普遍.最广泛喜欢的国粹,不仅裨益身心,而且作为文化,成为了国人精神气象和文化风骨,越来越成为全民喜欢的一项"运动".习得一手好字,是多么 ...

  3. 西门子S7-1200二轴V80伺服写字案例程序运动控制 写字机自动化机械控制,高速脉冲

    西门子S7-1200二轴V80伺服写字案例程序运动控制 写字机自动化机械控制,高速脉冲 已组态好X轴 Y轴 有效行程200mm 实现功能:点动,抬笔落笔,紧急停止,定位 写字偏移 自动写字 默认字为: ...

  4. 行李箱app开发的功能细节有哪些?

    行李箱app开发的功能细节之一就是距离显示,直观显示箱子和手机的实际距离,行李箱app上显示一目了然,放心安全.还有就是自动称重功能,智能行李箱会通过感应来进行跟随功能,不管你上楼梯还是上坡都能轻松跟 ...

  5. html 手写字效果,canvas画布实现手写签名效果的示例代码

    最近项目中涉及到移动端手写签名的功能需求,将实现代码记录于此,供小伙伴们参考指摘哦~ HTML代码: 手写区 清除 确定 CSS样式: .mSign_signMark_box{padding: 15p ...

  6. 安卓APP设计规范和设计细节

    我们在进行安卓APP设计时,需要好好调整之前的设计规范和设计细节.根据目前流行的安卓手机的系统体验来完成我们的安卓APP设计规范.应该说这是整理出最全面的安卓app设计规范. 1.安卓app设计规范之 ...

  7. 计算机的记事本和写字板的功能,写字板和记事本的异同

    大家好,我是时间财富网智能客服时间君,上述问题将由我为大家进行解答. 记事本和写字板都属于应用程序,区别有: 1.文件扩展名的区别:记事本其存储文件的扩展名为点txt,文件属性没有任何格式标签或者风格 ...

  8. Android 天气APP(九)细节优化、必应每日一图

    上一篇:Android 天气APP(八)城市切换 之 自定义弹窗与使用 重新定位.必应每日一图 新版------------------- 一.封装定位 二.重新定位 三.必应每日一图 ① 添加必应接 ...

  9. pythonturtle写字_python用turtle写字

    怎么用python的turtle库画出这个图案,要代码? 怎样在python里让海龟画图抬笔落笔?雨纷纷,旧故里草木深,小编听闻,你独守一个人. 用penup() 和 pendown()方法 海龟编辑 ...

最新文章

  1. 语义分割:基于openCV和深度学习(一)
  2. 10 个最值得 Python 新人练手的有趣项目 | 赠书
  3. unity球体添加光源_Unity渲染路径——光源种类
  4. 小程序-wepy学习
  5. 深度学习(七)caffe源码c++学习笔记
  6. java 日期计算类_java日期计算工具类【包含常用的日期计算方法】
  7. 数组中子数组求最大和
  8. a8处理器相当于骁龙几_天玑820相当于骁龙什么处理器?天梯图秒懂联发科天玑820性能排名...
  9. 学习笔记:CentOS7学习之二十二: 结构化命令case和for、while循环
  10. 【POJ 3279】【开关问题】Fliptile【暑期 No.5】
  11. 躺着赚钱|闲鱼自动发货脚本|自动化|Auto.js
  12. leetcode题解持续更新
  13. arduino 土壤温湿度传感器_arduino测量土壤湿度自动浇水提醒 - 全文
  14. Docker真实应用场景案例解析——ASSA ABLOY
  15. 高中计算机应用面试教资真题,2019下半年高中信息技术教师资格证面试试题(精选)第四批...
  16. 解析LDO的五大作用,这里有你意想不到的答案
  17. Vue3必会技巧-自定义Hooks
  18. 《Unity 5.x游戏开发实战》一1.9 添加一个水平面
  19. 【竞赛项目详解】二手车交易价格预测(附源码)
  20. 使用Qt通过Post发送Json格式数据

热门文章

  1. 《匆匆那年》的你,还记得吗?数学中的那些有(hui)趣(se)的定理(7)——牛鞭效应
  2. DM3E,雷赛步进驱动器
  3. 2020春季线上PAT甲级比赛经验(必看!!!)、155题目分类
  4. Mac版Excel怎样添加数据分析
  5. IT技术网站论坛大搜集
  6. CV脱坑指南(二):ResNet·downsample详解
  7. phpbb风格模板_[原创]使用PHPBB搭建自己的论坛系统
  8. vs studio2019解决下载慢的问题
  9. C++小游戏——恋爱指数测试器O(∩_∩)O
  10. 引流、回流、截留、一个普通的早餐店如何做到日盈利2000的?