[回目录]
9.7 看图
手机拍照的功能大约从2003、2004年的时候开始流行起来,那时候称得上比较高端的手机都配备了30W像素的一个小小摄像头,按一下,就能生成一张现在看起来模糊不堪的照片——其实也就只能当玩具玩玩,而且当时也仅限于拍拍照,想要像电脑那样方便地查看照片就不行了,你选择了一张小小照片之后,它就一个黑屏,上面几个小小的字“Loading...”,再花上几百毫秒到几秒的时间,才能把一张很毛糙的照片显示给你看,这是许多年前的事……iPhone一出,一切都变了。
我第一次看到iPhone可以用手指切换缩放照片的时候,我简直震惊了,我从来没想到过居然可以这样,而且速度居然还很流畅,要知道,性能差一点的电脑,做这个动作还很慢呢。于是,手机的拍照/看图功能就开始脱离“玩具”的范畴,转入了真正意义上的实用阶段。
不得不说,SoSoPi的看图功能由于存在一些严重的问题而沦为了鸡肋,这是我的遗憾。后来我一直想办法修正这些问题,但一直没成功,现在大致回顾一下。
看图功能是高度依赖系统资源的功能,比如你要显示一张320*240的图片,你需要多少资源?简单地算算,一个像素2字节(Windows Mobile的像素格式是“R5G6B5”,16bit),于是就320*240*2 = 150K,大约150K这样的缓存方可,150K貌似不大,但图片一旦大起来就很恐怖了,比如一张1600*1200的图片,大约3.66M,对于资源紧张的手机系统来说,这也许是很要命的。正是这个紧张的资源问题和一些OEM相关的碎片化问题,导致资源枯竭出现的可能性很高,(抱怨得最多的就是Samsung的i8000手机)资源枯竭的后果就是系统死机或莫名其妙“假死”。也许你想说:“这估计是资源泄漏引起的吧?”不完全排除这个可能性,但我已经仔细加仔细地看过代码,并执行了大量的白盒测试,没发现我这里面的代码存在什么资源泄漏的问题,而且这种“假死”并非出现在所有的系统上,有些手机就工作得很不错,这种“资源泄漏”也许是系统底层造成的,不是我力所能及的了。
另一种办法,就是完全自己搞一套图片加载和绘图的方案,而不要使用系统默认提供的。这是解决这个问题的终极方案,但我始终没有找到一种很好的办法,虽然我知道办法一定有,但真的不在我手里。如流畅的图片查看和缩放,其实不只是iPhone上有,Windows Mobile下有个软件叫“Resco Photo Manager”,也有这个功能,另外还有一些可以在Windows Mobile上跑的3D游戏,都表明了这个系统上的硬件的能耐,只是怎么用而已。我在网上求助过很多人,包括让老吴去找了之前“无限今日”的作者,让他提供一套用于图片渲染的代码(反正他后来也不再更新无限今日了嘛),但后来发现那套代码不能直接编译通过,还特别晦涩难懂,而且需要“破解”一个dll方可,后来也行不通。这也就是我之后所抱怨的:资料太少了,微软支持力度太弱。对比下,Android和iOS的开发者就太幸福了。
下面向大家展示下看图模块的功能:
功能大致就这些,生成缩略图,点击查看,再双击可按原图大小查看,类似iPhone的操作方式那样切换图片,还可以设为墙纸,差不多就这些。隐藏的需求是需要选择一个存放图片的目录,如果这个目录的内容发生变化,那缩略图也要跟着变化。
其实,这里面的细节还很多:图片的加载并非一件很轻松的事,尤其是机器较慢,图片较大的时候,这意味着当程序正在加载图片的时候,界面是不能及时响应用户的操作的……嗯?你说多线程?那我索性就在这里吐槽一下Windows Mobile的“假多线程”,Windows Mobile对CPU资源的管理能力是远远差于Windows的,你以为开了多线程程序就能及时响应用户的操作?——实际上根本不行,线程优先级之类的概念在Windows Mobile下形如虚设,不是“直接说不支持”就是“用起来不支持”,所以后来我采用的办法是:当鼠标(手指)按下准备操作界面的时候,就把工作线程暂停掉(用SuspendThread),在手指松开并且滚动动画完成的时候,再让工作线程动起来(用ResumeThread),这是非常非常容易出错的地方,因为按下鼠标的地方很多,工作线程可能不只有一个,请求线程挂起的地方也不唯一,请求恢复线程工作的时候也许由于模块切换,线程已经被结束……所以看图这个模块还有个bug,我一直没捕捉到,那就是有时会导致程序死锁(不是死循环),用户不得不强制结束掉SoSoPi,但貌似只有这种办法能给予用户流畅的界面响应。可见一个看起来想当然的功能的背后,不知会有多少技术细节。
再比如,图片变化通知,这个需要另开一个线程来等待一个系统队列的状态,当有任何文件发生改变(增删改都算),就会从队列中拿到消息,然后通知主线程,主线程再启动缩略图加载线程来加载图片(如果线程已经在跑,那先通过一个标志位将其停止,然后再开始)。但如图中蓝色黑体字标明的地方,有个处理细节!试想用户一下子往图片目录复制入大量的图片文件的情况,是不是会导致很多的变更消息?然后得重复地做一系列的缩略图布局计算,还有频繁地“重启”缩略图加载线程?所以我这里用了一种自创的叫“延迟执行”的小技巧:当文件发生改变的时候,不马上发送通知,而是等待1秒钟,如果这一秒钟里再有文件发生改变,那么再等待一秒钟,以此类推。这个小技巧十分地有用,在SoSoPi和皮肤编辑器里都有很多地方用到。
至于类似iPhone这样的图片切换功能是怎么弄的?这里有个示意图:
从图中可以看出来,实际被加载的图片只有3张,即“上一张”,“这张”和“下一张”,要是全部加载的话资源肯定不允许啊,那切换图片的时候,如果速度较快,岂不是来不及加载下一张而只能看到缩略图?是的,如果是这样的话,就稍等片刻,加载好了就自然会显示大图。如果机器的资源足够多的话,应该可以缓存更多的图片,但我只做到了这一步。
总体来说,这个模块我自己感觉并不满意,但限于当时所掌握的技术,我又有些束手无策,我们很缺乏图片处理的核心技术,所以图片的无级缩放/旋转都没做。再加上“资源泄漏”和“不定期死锁”的问题,我不得不明确告诉用户“不推荐使用此模块”。
9.8 阅读
早几年,我对整天低头看手机的人是很不屑的,主要是因为手机的屏幕太小,心想这样“看书”能爽么?但iPhone4一出,一切又变了,手机的屏幕从此变得无比细腻,加上便利的缩放和流畅的翻页,阅读体验迅速提升。其实,到现在我都没完全能理解为什么这么多人喜欢用手机看网络小说,而且看完后像水过鸭背一样,一点感言都没有,完全成了临时的消遣。
SoSoPi的阅读功能是一个非常典型的“看起来简单,做起来难”的东西。
阅读功能当然也毫无疑问地是老吴的主意,他十分喜欢用手机看网络小说,比较出名的那几部他应该都看过,和别的“水过鸭背”的读者不同,他有时会跟我们分享一些他的读后感,赞叹那些小说的作者水平如何如何高,但我一直都没有认真去读读他推荐的那些东西,主要是要花太多时间,我又担心自己会沉迷其中。老吴的想法很多,一开始他打算让我做一个支持不同格式的读书软件,pdf,txt,umd,doc都要支持,但后来发现除了txt格式,别的还真不太靠谱——太复杂了啊,于是就将格式支持简单地锁定为txt,以为这样就够容易了,结果……这才是刚刚开始。
最大的难度无疑就是分页!如上图。
为了较好的阅读体验,我们的程序总不能像Windows的记事本那样,右边一个滚动条,由用户拖着滚动条一点点走吧?所以需要分页。现在问题是:从第几个字起另开一页?答案是:不知道,除非你真正地把文字描绘出来。这是我花了大量时间去研究的结果,那就是只能用这种最笨的办法。为什么?你想想看:
字体类型不确定,字宽不确定,字体大小不确定,换行段落不确定,怎么知道在哪里换页?对我而言,唯一的办法就是尝试。把上图中的红色的框看作是阅读可视区域,我要尝试把一段文字显示到这个区域来,从第一个字母“I”开始,找出一个长度,正好能显示满一页。
做法:确定一个单页最大字符数2000(试验下来,字再小,一页也不会超过2000字),然后尝试用2000字(如果缓存中的字数不足2000,就拿出缓存中的所有内容)绘制在一个与目标窗口宽度一致的矩形中,得出其高度,用这个高度来估算大约多少字能正好填满窗口,即“合适字数 ≈ 测试字数*窗口高度/测试字数的高度”,然后从这个“合适字数”开始用二分法去尝试找出一个字数,多一个字就超过窗口高度,少一个字就不超过窗口高度,这样我就认为应该在这里分页了。
刚才提到了“缓存”,是的,我的做法并不是每翻一页都会去读一次文件,而是创建一个缓存,读6000个字,这样就避免了翻页的时候频繁地对文件进行读取。也许你要说:为什么不一次性读入所有文字呢?——资源问题,一个txt文件可能有好几M,Windows Mobile分配几M的内存很可能会失败的,这点跟Windows很不一样,再说程序别的地方也需要使用宝贵的内存资源呢,不能把这点点内存全部腾出来给这个txt吧。
另外还值得说说的是一个跳转的功能,比如你看书,直接从第10页跳到100页,如果只能点“下一页”的话那得点90次,多麻烦,但对于本来就没有页码概念的txt文件来说,到哪个字是第100页?——不知道啊!想知道的话只能像前面描述的那样,一页一页计算,估计要算很久,而且,最要担心的是用户稍微调一下字体大小,又得重新计算了,一个好几兆的txt文件,这样折腾下来还得了?所以后来“翻到第N页”的这种功能我们没实现,但却实现了一个大概的,按百分比跳转的功能:
按百分比就相对简单了,前提是确保文本文件为UNICODE格式,一个字占据两个字节,对于一个4M的txt文件来说,我要跳转到它的75的位置的话,那就从3M这个偏移量开始读取。
在这个模块开发差不多完成的时候,老吴突然提了一个新的要求:能方便地调整亮度。我说:“为啥?”“晚上阅读的时候感觉太亮了。”“不会吧?”可当我亲身尝试了之后发觉确实如此。调就调,这有何难?不试不知道,一试就发现这又是一个OEM相关的问题,细心想想也能知道,Windows Mobile这个操作系统不能假定你的液晶屏都是亮度可调的啊,有些估计根本没有“亮度”的概念,有些是固定亮度行不行?像现在电脑的显示器,你能在控制面板中调整其亮度吗?不行吧,你只能直接在显示器上调,因为不同的显示器,对亮度的控制的做法可能是差别很大的,而且又没有一个统一的接口。我一下被难倒了,后来,老吴想出了一个巧妙的办法:不能直接调整液晶屏的亮度,那我总可以将我的阅读版面的颜色调深一些吧?——确实是个不错的折中方案,我就这么做了,当然了,这种方式的弊端是:实际上液晶屏的亮度没变,只是看起来“暗”了,不省电的。
总体而言,阅读这个模块还是很不错的,很多用户都喜欢它,我想原因是:够简单好用。功能不一定要多强大,但要够好用。绝大多数用户就是用这个来看网络小说消遣的,这是最大的需求,所以这个程序基本上不需要什么图片啊,查找啊之类的功能,因为绝大多数读者都是从前一直往后翻,不像我们阅读什么参考书的时候需要在章节之间跳来跳去。当然了,还有些比较人性化的功能,比如亮度调整,字体大小颜色调整,更换阅读背景图,阅读进度等。
9.9 音乐
如果我没记错的话,音乐是我给SoSoPi实现的最后一个模块,这个模块的核心功能当然就是播放音频文件,支持的格式有这几种:mp3,wma,wav。也许不止这几种,但最常见的音频格式也就这几种吧。
播放这个功能对我而言是没办法自己做的,只能直接使用Windows Mobile提供的“Windows Media Player”(WMP)作为播放器,和Windows平台的类似,WMP在Windows Mobile平台下也是以一个ActiveX(以前提到的COM技术,是ActiveX技术的基础)控件的形式提供的,Windows平台下的WMP控件的使用是非常简单的,在一个支持ActiveX的窗口容器上把控件一拖上去,就什么都有了,什么“播放”、“停止”、“上一曲目”、“下一曲目”……等,运行起来,就跟WMP一模一样,感觉你瞬间就把WMP写出来了,但其实你什么都没写,你想这样的程序谁会用呢?至少,你得往上面弄点WMP没有的东西吧。
虽说跟Windows很像,但Windows Mobile下折腾WMP还是稍微麻烦一些,原因是SDK不全,文档说明少,有些地方得靠自己摸索,我一开始对这个没什么信心,因为我是直接使用Windows原生的API来开发SoSoPi的,并没有使用MFC或ATL等程序框架,要想使用ActiveX控件,很可能支持力不足,不知道行不行。后来我在网上找了一个例子,抽丝剥茧地分析了其中的代码,发现貌似可以,但需要改一点东西,于是我先自己写了一个demo,把一些阻碍我视线的无关代码都移除掉,就剩下最简的代码,看看这样做可不可行,最后认为“可行”,Okay,去做。我想说的是:这么多年下来,这一直是我用来分析问题和解决问题的最重要方法。至于具体的代码,还是有点小复杂的,这里还是略过了吧。
SoSoPi当然不可能字节显示WMP控件的界面,我只是想要它的播放功能,怎么做?很简单啊,把窗口隐藏起来不就OK了么?然后那些“播放”“停止”之类的按钮就对应到这个控件上的不同的方法不就可以了吗?
SoSoPi这里还提供了一个“关闭屏幕”的按钮,因为大多数时候,用户在听音乐的时候并不想继续看着屏幕,关掉屏幕还能省点电,小功能,但却很有用。
音乐其实应该是个不错的功能,它比默认的WMP好用多了,Windows Mobile默认的WMP连如何创建播放列表都能让人迷惑半天,很多时候,用户要的只是“给我播放这个目录下的mp3歌曲”,那些“My Videos”、“My TV”、“库”……等功能对用户来说是否有些晦涩?就像苹果的iTunes那样,功能强大啊,但用户会用它来听歌吗?一般都用简单点的winamp,qq音乐,千千静听等,对吧,我并不是说这几款软件真的简单,我只是说它们用起来简单,最常用的功能就直接摆在那里,直截了当。——但,这个模块我却并不是很满意,原因嘛,跟“看图”哪个模块差不多,有些技术上的问题解决不了,就有好几个用户跟我们反映过音乐无法播放,后来有一次我在自己的手机上刷了一个新的ROM,也出现了这种问题,其实也就是WMP控件的问题,控件创建失败,我一点办法都没有啊,只能告诉用户一个不太友好的解决方案:你换一个ROM试试看?归根到底,我们没有掌握核心技术。
9.10 日历
我差点遗忘了这个模块,这个模块实现得是比较早的,因为我相信它应该比较容易,确实,对比短信和阅读来说算简单,但也没有预想的那么简单,我也同样地过五关斩六将方可。
首先要解决的问题是农历,其实很早以前我就知道“农历≠阴历”,但我一直不知道具体细节,但在完成日历这个模块的时候,对这些历法知识,我有了深刻的认识。趁这个时候来科普一下吧:
阳历,是以太阳运动为参考依据的历法;阴历则是以月球运动为参考依据的历法。所以即便农历是阴历,那也只是一种从属关系,而不是等同关系,而事实上,农历根本不是阴历。最有力的证据是:清明是农历几月初几?——大多数人都只能翻日历,日历上标着“清明”的那天便是清明,清明是每年公历4月4日、5日或6日这三天之一,这是一个阳历节日。农历每年正月,都是冬天,农历每月十五,都是满月,这充分说明了,农历是一部阴阳历,它兼顾了太阳和月球的运动——这个观点对大多数人而言比较高端,如果哪天你跟别人提起,但他又一脸迷惑地看着你,那就放弃吧,你自己明白就是。
农历这部历法在我看来是相当了不起的,确定每个月的天数,填充二十四节气,插入闰月来协调阴阳……这些都必须借助相当精准的天文观测方可制定,坦白说,我不太知道古代中国人是如何弄的,我甚至不觉得古代中国人有能力弄出这么一部神奇的历法,它比公历复杂多了。(BTW:公历是一部阳历)
关于公历到农历的转换,网上有很多资料,但你在用这些资料前,必须得自己去验证一下,因为我在做这个模块的时候发现其实很多网上提供的代码都是不正确的,这里面并没有简单的换算公式,比如一个农历月究竟是30天还是31天,这里面并无计算公式,只能把所有的月枚举出来;二十四节气究竟是公历哪天,也无法用公式计算,一样只能全枚举(这个数据量不小);闰月究竟哪年有,同样无法用公式计算……不管你的代码是不是自己写的,反正大量的验证是少不了的了。
接下来是节日,节日我分析下来主要有三种节日:
- 公历节日,如元旦,情人节,圣诞节……能用公历几月几日确定
- 农历节日:如春节,端午节,中秋节……能用农历几月几日确定
- 周节日:如母亲节,父亲节,感恩节……能用几月第几周周几确定
除此外,还有节气和特殊节日,节气前面已提过,剩下的特殊节日基本上只有一个——复活节,这个节日没办法简单地用上述方法确定它是哪天,有兴趣的同学百度一下就知道此节日计算有多繁琐。
我用一个xml文件来保存这些节日信息,这样用户就能很轻松地增加一个节日,比如叫“我的生日”,另一好处是方便国际化,如美国的就没有十月一日这个国庆节,他们有什么节他们自己最清楚。
不知道大家有没有想过公历日期下的小文字是怎么做的,比如10月1日国庆节,同时也是“国际音乐节”和“国际老年人节”,那凭什么小文字显示“国庆节”而不是别的呢?优先级?也不成,因为国庆节的标准名称叫“中华人民共和国国庆节”,总不能显示那么多字嘛,再说了,有很多节日其实根本就不应该显示在小文字上,比如10月4日,“世界动物日”,如果把这种节日都显示出来的话,日历估计会混乱不堪了,农历根本就没法看。所以我的做法是写死在程序里的,做国际化的时候,如果不需要显示这些节日,那修改语言包文件,将其中的文本替换成空行即可。
在“日视图”中,我提供了一个“历史上的今天”,这是一个有点“呵呵”的功能,我感觉没什么用,但却花了不少时间,尤其是英文版的,我到一个英文站点去,把那些大多数都看不懂的文字,一点点拷贝下来,再整理成我想要的格式。——现在看来,SoSoPi有些功能是可以不要的,这就是其中之一。
[回目录]