【如果你想了解這個點讀筆寫字App的背景,請移步這里
http://www.reibang.com/p/ee2a1bb99280 】
寫前嘮叨
現(xiàn)在開始寫的這篇佛玄,距離上次的結(jié)文的時間節(jié)點已有大半個月的跨度。最近換了新東家通贞,想著好好調(diào)節(jié)一下狀態(tài)胡野,改變之前早上萎靡的狀態(tài)。早上上班基本不再看程序師的博文蓝丙,而是回顧昨天所學(xué)&寫code&學(xué)習(xí)。中午會保證20min的休息望拖。晚上回來倒是如之前一樣渺尘,白天想著回來做點學(xué)習(xí)和總結(jié),回來卻是懶得啥也不想做说敏,所以寫東西也就一天天擱置鸥跟。
做了這些改變之后,整體感覺還是不錯的盔沫,一天下來收獲比之前要多锌雀。只是下午3.左右 整個人就很煩躁蚂夕,頭腦發(fā)熱效率低下,這種狀態(tài)一直持續(xù)到晚上腋逆。主要原因是那個時候大腦有點疲勞(我周末都是在這個點之前躺床上睡一個多小時的)婿牍,然后又只能逼著自己學(xué)習(xí),引起了厭煩的情緒惩歉。我試著用堅持的方式逼著自己調(diào)整生物鐘等脂,但是往往適得其反。我不知道大家是否會遇到這樣的狀況撑蚌,畢竟每個人的情況都會不同上遥。
如果有上面的情況的朋友,我的建議是不要逼著自己做争涌,去做感興趣的事粉楚。我自己的感覺是疲勞的時候仍逼著自己去學(xué)習(xí)往往會適得其反,引起厭煩亮垫;我也嘗試過這時候去網(wǎng)上看看博客模软、段子、新聞饮潦,不過很多時候我看著看著就不知道我看到哪了燃异,甚至有時候在那打瞌睡了。我們需要面對一個現(xiàn)實——人不可能全天都保持高效的學(xué)習(xí)狀態(tài)继蜡,既然在那段時間效率低下還不如讓自己“放假”回俐。因為如果你是個自我驅(qū)動很強的人(我相信有一部分程序員是這樣的,別的職業(yè)可能大抵如此稀并,程序員不是第三類人)仅颇,效率低下帶來的自責(zé)、煩躁等消極情緒往往會抵消那點可憐的收獲碘举。
接下的日子忘瓦,我準(zhǔn)備嘗試在那段時間里寫寫東西,做些總結(jié)來整理整理自己的邏輯殴俱。一方面寫點東西理清邏輯的話,會對自己所做東西的把握枚抵,容易地獲得自信线欲;另一方面,寫東西的時候汽摹,我常常一晃幾個小時李丰,卻由于時不時回去閱讀(ps1),總寫不了多少字逼泣。同樣效率低下趴泌,但是之后非但不消極舟舒,反而很愉悅,想到說不定會對別人有幫助甚至很興奮嗜憔,我謂之“上乘的興趣”秃励。這與打籃球不同,打得好就開心吉捶,打得不好就不舒服夺鲜,而且之類的體育運動又不方便在工作時間開展,這樣的興趣就不適合用在這里調(diào)節(jié)自我了呐舔。如果是聽音樂之類币励,總能讓你心情大的悅話,那不妨試試珊拼。另外要提醒一下的就是食呻,要注意“放假”和惰性界線的把控,別玩大了澎现。你們是如何度過每天中這段“困難時期”的仅胞?可以在下面告訴我。
ps1:我一直沒搞清楚自己為什么常常要回讀昔头,要是在古代饼问,“才思敏捷”、“一氣呵成”這樣的成語也不會因為我而發(fā)明揭斧,如果我生的好人家莱革,大概可以貢獻一個“才思不敏”的寓言故事。
前面啰嗦太多讹开,下面直接開始寫字細節(jié)的內(nèi)容吧盅视。
獲得數(shù)據(jù)并處理
本來這個App是必須配合一個硬件的設(shè)備來完成的,因為可能涉及到公司的一些利益就不介紹了旦万。設(shè)備最終會不停的給我傳回一個int型的數(shù)據(jù)闹击,這個int型可以對應(yīng)到某一頁的某個特定位置,而app要做的就是把這些int型轉(zhuǎn)換成圖片上的涂黑的像素點成艘。每隔一段時間app都會從設(shè)備讀得一個數(shù)值赏半,讀到有效值就通過Handler類發(fā)消息給畫圖類處理。當(dāng)然淆两,由于書寫是一個連續(xù)的過程断箫,讀取數(shù)據(jù)的時間間隔要設(shè)置得恰到好處,使得app能夠及時讀取到硬件獲取的數(shù)值秋冰,而不會因為硬件上覆蓋了上次未來得及被app讀取的數(shù)據(jù)仲义,造成遺漏數(shù)據(jù)的情況。另外,又要避免過密的讀寫給cpu帶來的壓力埃撵。App的暫定實現(xiàn)方式是這樣的赵颅,但我不認(rèn)為這樣方式是很好的決定,最后我會來說這個問題暂刘。
在app中饺谬,畫圖類和數(shù)據(jù)讀取類是不同的兩個類。現(xiàn)在畫圖類中繼承Handler實例化自己的內(nèi)部類MyHandler鸳惯,重載其handleMessage()方法實現(xiàn)消息處理策略商蕴。然后實例化MyHandler,把該實例傳給數(shù)據(jù)讀取類芝发,在數(shù)據(jù)讀取類中通過實例發(fā)送消息觸發(fā)消息處理函數(shù)绪商。代碼如下:
//畫圖類中
private class MyHandler extends Handler
{
@Overridepublic
void handleMessage(Message msg) {}
}
private final MyHandler PageHandler = new MyHandler();
//畫圖類是一個活動類,然后onCreate使用PageHandler實例化畫圖類把實例傳過去即可
//數(shù)據(jù)讀取類中
PageHandler.sendMessage(msg);//msg包裹讀取的數(shù)據(jù)
在MyHandler的消息處理函數(shù)中辅鲸,首先會去判斷我們獲得的一個值格郁,如果該值是筆書寫過程中獲得的值,則按照他們接受的順序存儲到數(shù)組中独悴;如果該值代表著筆抬起的動作例书,那么則將數(shù)組中的點統(tǒng)一解析成baseBtimap上的像素點。
//void handleMessage(Message msg)的實現(xiàn)
if(nIndex == nUpPenCode)
{
//沒有真正繪畫前兩值相等(初始化);你變態(tài)到不在在本上寫字戳筆尖觸發(fā)筆抬起消息刻炒,do nothing.
if(nCurScribingPage != nJustScribedPage)
{
//重寫一頁時决采,保存剛才所畫頁到本地
if(baseBitmap != null)
{
SaveForInitLoad(baseBitmap, fCurDrewPage);
baseBitmap.recycle();//由于Bitmap在內(nèi)存中占據(jù)很大的內(nèi)存,容易出現(xiàn)OOM的狀況坟奥,提醒系統(tǒng)及時回收內(nèi)存
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();到另一頁上面書寫访敌,則保存圖片到本地凉敲,重新創(chuàng)建一塊畫布。在往數(shù)組中添加數(shù)值時寺旺,由于我們書寫的連續(xù)性爷抓,不太可能一筆下來會寫到別的頁上面,所以我只需要根據(jù)要添加進數(shù)組的第一個值判斷往第幾頁上面寫就可以了阻塑。上面的else里面就做了解析int型數(shù)值蓝撇,解析成到哪一頁書寫的一個相對數(shù)值。那些加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);
}
}
如何初始化一張畫布耘沼,之前篇章已經(jīng)講過了,這里不在重復(fù)朱盐。這個畫布是哪來的呢群嗤?當(dāng)在一張白紙上書寫的時候是從資源文件里導(dǎo)入一張原始圖片,而在寫過的紙上書寫則導(dǎo)入之前保存有痕跡的圖片兵琳,然后接著在上面解析像素點狂秘。
實時顯示
通過前面的介紹,可以知道書寫內(nèi)容的實時顯示并不是一個點一個點地去響應(yīng)的躯肌,而是在筆抬起來的時候統(tǒng)一響應(yīng)之前一筆連貫的書寫者春。顯示的實現(xiàn)是在DrawAllPoints()中:
private void DrawAllPoints()
{
for(int i = 0; i < Indexs.size(); ++i)
{
DecodeToPt(Indexs.get(i));
}
Indexs.clear();
UpdateAfterDraw();//以上在內(nèi)存中寫,寫完更新顯示當(dāng)前圖片}
DecodeToPt()函數(shù)中就是數(shù)值對應(yīng)到像素點的解析了清女,解析的過程就是int型數(shù)對應(yīng)到畫板一定數(shù)目像素點的過程:
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表示每頁一行的定位點數(shù)钱烟,nXSpanPerPoint表示每個定位點對應(yīng)的像素寬,帶“Y”則對應(yīng)到Y(jié)軸的意思嫡丙。還記得我前面有篇拴袭,點一個點涂黑一個方塊的那張圖嗎?圖片像素800*800曙博,一個框100*100拥刻,框的標(biāo)號從1-64。這里的100就是nSpanPerPoint父泳。
UpdateAfterDraw()所做的事情就是將內(nèi)存上畫好圖顯示到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)//找不到業(yè)表示新的頁,則添加,重新排序尘吗;否則逝她,直接更新數(shù)據(jù)
{
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;
}
邏輯上沒有什么復(fù)雜之處睬捶,以上代碼做了3件事:正確的位置添加圖片黔宛,顯示到UI,滾動到當(dāng)前書寫頁圖擒贸。后面兩件事臀晃,一個前面篇章有講到,后面一個一句事(除了在手勢滑動的時候假想旁邊有個滑條外沒什么好說的介劫,與這里也沒什么關(guān)系)徽惋。而第一個就是下面要講的了。
有序的頁圖
要做到有序座韵,自然就需要排序了险绘。上面注釋的地方就是下面這段代碼了:
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瓣距,值分別對應(yīng)了Bitmap類型的頁圖和String類型的頁碼,這里排序之后的結(jié)果是listItem之間的有序序列代咸。代碼中讓mListItems根據(jù)給出的比較子進行比較蹈丸,而比較子中重載了compare函數(shù)。比較的規(guī)則是按照每個map中pageNo字段的String字符串相對ASCII碼的順序呐芥。也許大家看到這里似乎可以明白我為什么要把頁碼格式化成“01”這樣的格式了逻杖,當(dāng)然用“1”這樣的也是可以的∷嘉粒可以看到這里compare起作用的就是其返回值荸百,我們用“1”形式的就不能調(diào)用compare了,但是可以轉(zhuǎn)換成整數(shù)滨攻,直接比較結(jié)果返回-1管搪,0,1铡买。至于需要正序時t1>t2的情況應(yīng)該返回1還是-1更鲁,自己試一試。以前研究過比較子排序的實現(xiàn)代碼奇钞,現(xiàn)在又忘掉了澡为,如果知道的朋友麻煩在下面評論里告訴大家。
大家可以猜一猜還有哪里需要排序的景埃?應(yīng)該是無序數(shù)據(jù)需要顯示UI的時候吧媒至?機智的小伙伴會告訴我,打開app初始化記錄的時候谷徙,因為那時候要進行本地數(shù)據(jù)到UI的顯示拒啰。當(dāng)然,不知道應(yīng)該在初始化的時候的朋友也并不能說明你不機智完慧,因為你很可能不知道從本地讀取的文件數(shù)據(jù)到數(shù)組中是無序的谋旦。按理說確實應(yīng)該在初始化時getDatas后,再copy一次上面排序的算法屈尼,或者寫個函數(shù)出來分別調(diào)用册着。這里我把這個排序放到了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);
}
注意其中文件數(shù)組Arrays.sort()甲捏,這么做首先是為了符合初始化顯示的有序要求,當(dāng)我按照文件名有序讀取數(shù)據(jù)后鞭执,mListItems中存著序的頁圖司顿,可以直接顯示芒粹。另外,這么做是基于一個效率問題的考慮大溜。我們知道這里的文件并不是真正讀取到內(nèi)存中的文件的2進制數(shù)據(jù)是辕,而是一個本地文件的引用,所以這里占據(jù)的內(nèi)存是很小的猎提。而上面對mListItems排序,里面太多東西啦旁蔼,一個圖片占據(jù)那么多內(nèi)存锨苏,當(dāng)我們排序的時候內(nèi)存中需要搬動那么大的數(shù)據(jù),而且是多次搬動棺聊,太辛苦啦!當(dāng)你開始體諒計算機的時候伞租,說不定有意想不到的收獲;如果你說不讓計算機代替人做復(fù)雜的運算是體諒的話限佩,那是抬杠葵诈!他不計算叫他“計算機”干嘛?而我們知道祟同,當(dāng)大部分的數(shù)據(jù)是有序的時候作喘,放入一個數(shù)據(jù),然后讓其排序到同序(正序或逆序)晕城,代價要比排列完全無序所做的數(shù)據(jù)搬動少得多泞坦。哪個實踐者做個例子給大家證明一下就更好了,我也同求looklook(好懶啊砖顷,每次寫一篇東西要幾天)贰锁。
實現(xiàn)之后的一些感想
app是為了做出筆記本的效果,每一頁都是一張圖片滤蝠。我們希望做到的效果是動過筆的頁才顯示出來豌熄。事實上做到后面我不太認(rèn)同這樣的方式。我并不認(rèn)為這樣做的顯示效果會有多么好物咳。既然網(wǎng)格的布局(見點讀筆寫字App(2)中的圖片)可以方便地選擇進入某一頁(像現(xiàn)實中根據(jù)頁碼翻頁一樣)锣险,為什么不把所有的頁都顯示出來,干凈的頁就留出空白頁圖览闰。我們是會有一本和app配套的筆記本囱持,上標(biāo)記了頁碼,但是app也應(yīng)該清楚地告訴使用者本子的頁數(shù)焕济。
現(xiàn)在的做法不僅一定程度上增加了app的實現(xiàn)難度纷妆,而且不利于了解保存的圖片慢慢增加,何時會出現(xiàn)OOM(out of memory)的問題(我們需要保證適量的頁數(shù)即使全部頁圖讀入內(nèi)存都不會出現(xiàn)OOM)晴弃。當(dāng)我們已經(jīng)在往UI顯示前往內(nèi)存讀入了所有的頁圖掩幢,為什么不讓其常駐內(nèi)存逊拍,書寫時只在內(nèi)存中修改。我們要做的只是在打開和關(guān)閉App時做IO的讀寫际邻。
至于文章的前半部分提到關(guān)于獲取硬件數(shù)據(jù)策略的問題芯丧,感覺實際的應(yīng)用中,讀取硬件的響應(yīng)速度是很難做到適度的世曾,我不知道那些寫字最快的人的速度極限是多少缨恒,縱然天空不是他們的界線。我一直主張里面應(yīng)該設(shè)置一個差不多的值轮听,然后一些點交給一個模糊算法去生成骗露,這樣我們就不需要精確地響應(yīng)每一個點。畢竟我們寫字的一些點都是連續(xù)的血巍,有起承轉(zhuǎn)合的趨勢萧锉,有跡可循,算法也可以實現(xiàn)述寡。當(dāng)然柿隙,公司沒有做算法的同事,就一直沒人鳥我鲫凶。而我自己也只是說說禀崖,卻是行動的矮人(主要沒弄過的東西,就感覺學(xué)習(xí)曲線很陡)。感覺找一下應(yīng)該會有現(xiàn)成的開源算法吧?
想聽聽你們談?wù)勀銈兪侨绾蜗耄?/p>