Sunday, May 27, 2012
Thursday, May 24, 2012
Google Play Subcription(auto renew)訂閱型服務上線!
Google Play訂閱型服務於美國時間2012/5/24發出了技術公報,
底下簡單列出幾項重點︰
底下簡單列出幾項重點︰
- 一定要先將APP上線(published)才能使用subscription服務
- 一個APP內可含多種資費,售價一定要高於0.00元
- subcription的沙箱模式幾週後才會在開發者控制臺上線
- 價格一旦訂了,就無法更改(更改的功能也要等幾週後開發者控制臺改版才提供)
- 如果舊資費已上線,使用者也在使用了,後期如果調整資費,subcription仍會以舊資費向使用者扣款。
- subcription的訂閱期間分2種-月租型和年繳型
- 透過Google Wallet跟使用者扣款
- 每期的扣款都會寄一封e-mail通知使用者
- Google會持續地跟同一張信用卡扣款
- 關於退費,必須從Google Play-->[我的應用程式]裡去解約,不提供直接從APP做解約的動作
- 當使用者按了退款,會算到週期結束才結束subcription服務(30或31天,視大小月而定)
- 特殊狀況下,萬一使用者直接找廠商解約,還是可以透過自己的server並使用server-side API去跟Google做API方式的直接解約
- subcription只支援有支援in-app-billing的國家 (台灣也算在內,請見官方in-app-billing支援國家列表)
- Google Play app版本號必須在3.5以上,Android版號必須在2.2以上
- 當使用者移除app時,Google會發出警告通知︰這隻APP仍在subcription服務下,並會詢問是否要解除該訂閱
- subcription的退費模式遵照in-app-billing,需直接找廠商處理。
- 即使退費了,服務仍會在使用者手機上維持1個月的cycle。
- subcription一樣有30%的抽成
Friday, May 18, 2012
Saturday, May 12, 2012
NDK的相關知識
一、前言
JAVA在程式語言中,算是高階語言(人類能理解的語言)。
由於JAVA的主要用途用在商業領域,
因此底層的架構是一層又一層的,
執行的效能可想而知就不如C(中階語言)的快速。
因此才有人會說︰
如果你寫的語言越接近組合語言(低階語言,也是電腦的語言),
執行起來當然就會是最快的。
Android因為提供了NDK,
因此可以將很多C語言的專案收納進來。
當然,
Google提供了NDK的支持,
主要也是因為其自由開放的特性,
更希望大量遊戲之類的專案,
能用進來Android領域被其推廣。
在遊戲畫面裡,
每個物件的處理都需要高度的演算,
這些演算如果使用JAVA來處理,
效能很差。
因此,
如果Android想要有遊戲的支持,
NDK的支持就變成了很重要的重點。
最有名用NDK的例子,
非Angry Bird莫屬。
Angry Bird在Android裡並非使用JAVA寫的,
而是C轉過來在Android上執行的。
二、文章開始
這裡先提及幾個可能需要了解的概念。
首先,
C語言會被放在Android Project的JNI目錄底下。
JNI目錄底下的.mk檔
告訴NDK建構系統(NDK build system)你的Native Code的相關資訊。
JNI目錄底下的.c檔
C語言檔。就好像JAVA檔的副檔名叫.java一樣
JNI目錄底下的.so檔
Android若要執行Native code,
並需先將C語言編譯且封裝成Library。
這個概念就好像我們在JAVA底下,
習慣將程式碼封裝成.jar給別的專案使用一樣。
要將Native code編譯成.so檔,
就要使用make.exe。
make.exe
這個檔案是GNU Make的檔,功能是將Native code轉成library,
讓其它的程式可以使用。
make出來的檔案,
在Windows環境底下副檔名會是.dll,
而在Linux底下,副檔名就是.so了。
Wiki對於make的解釋
在軟體開發中,make是一個工具程式(Utility software),經由讀取叫做「makefile」的文件,自動化建構軟體。它是一種轉化文件形式的工具,轉換的目標稱為「target」;與此同時,它也檢查文件的依賴關係,如果需要的話,它會調用一些外部軟體來完成任務。它的依賴關係檢查系統非常簡單,主要根據依賴文件的修改時間進行判斷。大多數情況下,它被用來編譯原始碼,生成結果代碼,然後把結果代碼連接起來生成可執行文件或者庫文件。
在make前,
Android的Project需要有build.xml檔方可編譯,
要在命令提示字元底下、該專案目錄裡,打上指令︰
android update project -p . -s -t android-8此時,
專案目錄底下就會產生build.xml檔,
這時候,再去對C語言做編譯。
Android也提供了一個快捷的方式make,
就是在專案目錄裡JNI目錄裡,下指令︰
ndk-build
Android一收到ndk-build指令,
便去找系統的make.exe,
幫你把這個C語言打包成Library,
並放進專案目錄底下的libs目錄裡,
副檔名為.so。
ndk-build這個檔案放在NDK目錄底下,
一打開目錄就可以看到了。
要在JNI目錄裡執行ndk-build,
你需要先設定好環境變數。
一打開目錄就可以看到了。
要在JNI目錄裡執行ndk-build,
你需要先設定好環境變數。

找不到make.exe
make是在安裝Cygwin的時候,順便安裝下來的套件,
不要到剛才的GNU make網站去抓安裝包,
因為如果是用這種方式安裝,
屆時在Cygwin裡執行make時,
Cygwin會不認識你自己安裝的make指令哦!
安裝make也蠻簡單的
只要Cygwin在安裝套件包時,
輸入make關鍵字就可以找到了。
文章未完哩....
Thursday, May 3, 2012
百萬大學堂 開發日記
一、前言 百萬大學堂在上線的2個星期內,
下載量正式破5萬人,
也持續了1個星期台灣區APP下載前10名。
在這個部落格裡,
將這隻APP開發過程的三兩事記錄下來,
跟玩家們分享。
二、文章開始
[百萬大學堂]誕生話說,
本來在餐飲業的我,
這樣子傻傻、不怕死的轉進資訊產業,
而且也因為愛死了那隻小綠人,
所以完全都沒有想的就踏入了Android領域。
這樣子奮鬥也快2年了,
我很想問我自己︰
「我的程度到哪裡了?
我的作品能被市場認同嗎?
如果沒有公司要我了,我有自己活下去的能力嗎?」在參與了一次台北GTUG(台灣Android開發者討論社群)的聚會後,
得知三星即將舉辦一場App大賽,
廣邀全台灣Android開發者進來這場比賽,
凡事獲得晉級者,
皆可拿到免費的Samsung Note和Samsung Tablet。
這是一個證明我自己的絕佳機會。
後來,
百萬大學堂沒有得到三星App競賽初評的青睞。
我︰
「可惡,本來看上了SamsungAPP大賽只要APP有晉級,
便可以拿到免費的Note和Tablet,
但是卻沒入圍。
Note那臺規格那麼奇怪,
如果有一臺Note,
至少之後的開發,
還能稍微照顧一下Note的使用者。
現在算了啦,
我們也沒錢再買Note了。
海倫,我們轉向,
把這隻APP趕快做成可運行版本,
在Google Play上架吧!」
版本更新歷史
v1.0.0 - 大學堂上架
海倫是[百萬大學堂]的視覺美編,
我(小鰻)是程式設計 + 企劃發想。
我︰
「海倫,天啊,
真的會瘋掉,
你知道嗎?
我剛才拿我剛做好的程式,
去西門町三星通路實測,
在Note上完全跑版耶!
在平板上更慘!」
海倫︰
「驚!」
我︰
「一定要想一個辦法解決Android解析度不同的問題,
不然海倫妳圖會畫死,
我程式介面也會醜爆!」
v1.0.1 - 改進調成靜音後進不了程式的BUG
在v1.0.0版上架Google Play了以後…我︰
「啊!我是豬頭嗎?
當我在設定裡,將音量調成靜音後,
遊戲進不去了!
凌晨3點,
快點修BUG!」
v1.0.2 - 能夠調整遊戲時間
海倫︰「小鰻,網友在幹譙不能調時間啦!
一直在我們的評論裡面罵髒話!」
我︰「發生什麼事?」
海倫︰
「剛才我看了一下,上線到現在2天了,
目前差不多有500人下載了,
但是網友都狂幹譙耶…」
我︰
「500了哦?那有機會追過劉子千哦!(心裡暗爽中)」
我︰
「哦,好啦,那我趕快追加一個能調整遊戲時間的版本。」
v1.0.3 - 防小白篇
「小鰻,下載量破1000了,
但是網友現在都在罵我們,
說遊戲超爛的啦!
而且都給我們1顆星,
還跟我們說題目很少…!」
我︰
「這樣不行,
大學堂是我們的孩子,
我們做爸媽的,
有義務保護好孩子。
海倫,
你趕快上去,留一篇中肯一點的評論,
至少能稍微阻止玩家一直負評下去。
我也會添加"開學典禮",
藉由開學典禮告訴玩家可以設遊戲時間、我們有意見回報的功能。
才2個人是能想出多少題目,
你至少幫我上去止血一下。」
海倫︰
「嗯。」
v1.0.4 - 添加成績單回饋
我︰「海倫喔~
我從第1版就跟你說,
玩家應該不會只想要看到成績,
畫面應該要有多一點的回饋才對,
上次跟你說的如果玩家得到100分,
小綠人會扭屁股,
如果得0分會3條線,
什麼時候才會畫給我啦!」
海倫︰
「哈哈,我選擇性忘記…」
我︰
「…」
「對了,記得喔,小綠人扭屁股的時候,
屁股要加上小叮噹的尾巴。」
海倫︰
「為什麼?那樣不會很奇怪嗎?」
我︰「我也不知道,但做就對了。」
海倫︰
「哦…」
為什麼小綠人扭屁股會有小叮噹的尾巴,
其實我到現在都還是不知道。
但是海倫畫的那個扭屁股,
我第1次看到動畫時,
連笑了5分鐘,
愛死你了,海倫~
v1.0.5 - 藍牙對戰版上線
海倫︰「小鰻,嚇到我了啦,
網友開始激增,
而且還是負評不斷!」
我︰
「有什麼辦法,
看到他們一直說題目很少,
我現在就卡在藍牙連線對戰開發,
根本就沒時間找題目啊,
你知道藍牙層一直讓我原本的遊戲當掉,
超麻煩的!
厚~
為什麼每次我都要挖洞給自己跳啦!
現在也只能趕趕趕,
快點把藍牙連線版釋出,
才有時間去找題目啦!」
海倫︰
「嗯…」
我︰
「在藍牙連線畫面,
海倫我希望用最簡單的流程,
讓玩家能夠完成藍牙連線的動作,
所以我會希望風格是簡單的,
玩家幾乎不用學習,
就能夠操作。」
海倫︰
「哦…(深思中...,其實是放空中。她自己說的)」
我︰「等等!海倫!
這2隻對抗的小綠人,
不應該只是對看,
他們中間應該要有火花!」
海倫︰
「厚~對看就有感覺了!幹嘛還要畫火花啦!」
我︰
「不行!要有火花!」
後來我把程式畫面實作出來後,
海倫她根本就超愛的,
她說電波有"死光"的感覺。
v1.0.8 - 張君亞傳奇(之所以打"張君亞",是因為"張君雅"侵權。)
我︰「海倫,
當初我們一次把主題都擺上去書架上,
玩家好像都只玩前面一、兩個單元就不玩了,
這感覺不太對哦!
是不是有什麼方法可以鼓勵他們往後玩,
而不要都只玩前面1、2個單元就跟我們說遊戲很爛、沒深度啊?」
海倫︰
「(思考中)其實是再度放空中…」
我︰
「啊!
我想到了,
尋找張君雅!
我們做一個單元,
內容是問玩家有關台灣的泡麵文化的相關問題,
主題取名為尋找張君雅。」
海倫︰
「好像還不錯唷!」
我︰
「可以把張君雅的名牌藏在其它主題的黑板或哪裡,
讓玩家去找它,
收集完後就可以開啟張君雅單元了。」
海倫︰
「不然我可以畫張君雅的笛子、書包…」
我︰
「嗯,先這樣吧…」
隔了一天,
早上6點,
睡夢中驚醒,
傳了WhatsApp給海倫︰
「海倫,
張君雅這件事情我們有一個地方不對,
我們要玩家收集的,
不應該是笛子、書包…,
應該要是筷子、湯匙和泡麵!
因為,
張君雅只吃泡麵!
這是上天賦予她的使命!」
海倫︰
「哦~。」
我︰「筷子任務題藏進我是萬事通單元裡,
問筷子的歷史。
湯匙則藏在另一個單元中,
玩家如果回答到任務題且答對,
就會獲得湯匙和筷子!」
玩家們,準備收集「筷子、湯匙和泡麵」吧!
希望這個創意,你們會喜歡哦!
張君雅一夕爆肥!!!!
話說,海倫畫給我的張君雅小妹,
本來是一個瘦子,
睡了一天醒來後︰
「海倫,那個張君亞有問題!
張君亞應該是個胖子!」
後來,海倫畫了胖子張君亞給我。
我︰
「哈哈哈,海倫你昨晚是讓張君雅吃了什麼,才一個晚上,胖那麼多!!」
三、總結
百萬大學堂,仍在陸續改版中,
海倫也持續被我逼瘋中。
未完,待續。
四、後記
聯成電腦專訪百萬大學堂和我的求學經過
給我靈感
Sunday, April 29, 2012
不要怕犯錯
我親愛的朋友︰
如果你現在看到這篇文章,
那是因為我有感而發。
我覺得每個人都應該為自己的未來負責,
所以我跟朋友成立哈工作團隊,
我們利用閒暇之餘做「屬於自己」的APP。
經營了快1年,
每隻APP的廣告營收,
1天不到10塊錢台幣,
下載量更是微乎其微,
我犯的錯是︰我天真的以為我的創意和獨特化,消費者會懂。
但是,他們跟創意發想的我們,
其實落差很大。
我做了決策工具,
這個工具很棒,
概念是從管理學的決策Matrix矩陣而來,
好幾次我在求職或做重大決策時,
因為有它,
所以在決定一件事時,
至少能有一點方向。
因此我把這隻APP實作出來,
第1版,反應還不錯,
但是也是累積了快半年,
才達到1,000下載人次。
這跟我當時做的「統一發票愛我」,
很快就達到1萬下載人次,根本無法比。
對於這個數據不死心的我,
又跟我的夥伴提議要做第2版更新。
這麼一改,
花了2個月,
界面也全換了,
並且跟我的夥伴說︰
「我們一定會大賺。」
結果,
我跌得很痛。
放到現在快半年了,
還沒到500下載人次。
應該說,
除了挑戰劉子千這隻APP以外,
別摔小安(手機摔了會叫痛)、師父開示(每天看到一則好話)、寫春聯祝福朋友、
手機遺失保鑣…
沒有一隻跌得不慘。
所以到後來,
我已經沒有勇氣再跟我夥伴說︰「這隻一定大賺」了。
在寫程式,
往往是先有一個點子,
一直這樣反反覆覆,
不知道過了N次才能成功。
在企劃一個案子也是,
一直想要獨特化,
然後遇到消費者不懂,
不死心又再次獨特化,
消費者仍然不懂,
最後只好調整自己的心態,
於是消費者懂了。
即將上線的百萬大學堂「藍牙對戰版」,
關於藍牙的串接,
這隻APP應該被我開了又關不下1000次,
藍牙開開關關一天也至少100次…
整個程式的分層架構刪掉又建3次,
動畫模組修改了4次,
但這些東西,
都是消費者看不到的努力,
也是我一次次不斷犯錯、修改後的結果。
很開心,
百萬大學堂上線1週,
即破台灣區萬人下載,
現在在台灣總排行榜第16名。
我親愛的朋友,
不要怕犯錯。
因為你的成功,
都是因為你犯錯而獲得到的寶貴經驗。
千萬不要怕犯錯。
小鰻 2012/4/30
如果你現在看到這篇文章,
那是因為我有感而發。
我覺得每個人都應該為自己的未來負責,
所以我跟朋友成立哈工作團隊,
我們利用閒暇之餘做「屬於自己」的APP。
經營了快1年,
每隻APP的廣告營收,
1天不到10塊錢台幣,
下載量更是微乎其微,
我犯的錯是︰我天真的以為我的創意和獨特化,消費者會懂。
但是,他們跟創意發想的我們,
其實落差很大。
我做了決策工具,
這個工具很棒,
概念是從管理學的決策Matrix矩陣而來,
好幾次我在求職或做重大決策時,
因為有它,
所以在決定一件事時,
至少能有一點方向。
因此我把這隻APP實作出來,
第1版,反應還不錯,
但是也是累積了快半年,
才達到1,000下載人次。
這跟我當時做的「統一發票愛我」,
很快就達到1萬下載人次,根本無法比。
對於這個數據不死心的我,
又跟我的夥伴提議要做第2版更新。
這麼一改,
花了2個月,
界面也全換了,
並且跟我的夥伴說︰
「我們一定會大賺。」
結果,
我跌得很痛。
放到現在快半年了,
還沒到500下載人次。
應該說,
除了挑戰劉子千這隻APP以外,
別摔小安(手機摔了會叫痛)、師父開示(每天看到一則好話)、寫春聯祝福朋友、
手機遺失保鑣…
沒有一隻跌得不慘。
所以到後來,
我已經沒有勇氣再跟我夥伴說︰「這隻一定大賺」了。
在寫程式,
往往是先有一個點子,
然後去做,
做完之後發現不對,
於是砍掉重練,
練了又不對,
又砍掉再練,一直這樣反反覆覆,
不知道過了N次才能成功。
在企劃一個案子也是,
一直想要獨特化,
然後遇到消費者不懂,
不死心又再次獨特化,
消費者仍然不懂,
最後只好調整自己的心態,
於是消費者懂了。
即將上線的百萬大學堂「藍牙對戰版」,
關於藍牙的串接,
這隻APP應該被我開了又關不下1000次,藍牙開開關關一天也至少100次…
整個程式的分層架構刪掉又建3次,
動畫模組修改了4次,
但這些東西,
都是消費者看不到的努力,
也是我一次次不斷犯錯、修改後的結果。
很開心,
百萬大學堂上線1週,
即破台灣區萬人下載,
現在在台灣總排行榜第16名。
我親愛的朋友,
不要怕犯錯。
因為你的成功,
都是因為你犯錯而獲得到的寶貴經驗。
千萬不要怕犯錯。
小鰻 2012/4/30
Tuesday, April 24, 2012
Friday, April 20, 2012
開啟Eclipse右側灰色標註
有時候我們想要看一個特定對象在一個Class中的所在位置,
開啟的方式是[windows]-->[Preferences]-->左邊的區塊選擇[Java]-->[Editor]-->[Make Occurrences],
然後將Make occurrences of the selected element in the current file選項打勾即可。
註︰Mac版是從上方工具列的Eclipse裡,找到Preference
開啟的方式是[windows]-->[Preferences]-->左邊的區塊選擇[Java]-->[Editor]-->[Make Occurrences],
然後將Make occurrences of the selected element in the current file選項打勾即可。
註︰Mac版是從上方工具列的Eclipse裡,找到Preference
Tuesday, April 17, 2012
Sunday, April 15, 2012
從BluetoothChat範例程式學藍芽連線原理
撰寫時間︰2012/04/15 16:06
更新時間︰2012/04/14 10:34
文章更新次數︰1
一、前言
Bluetooth藍芽裝置在手機界已經存在很久了,也早就成為連低階Android手機都有的基本配備。
官方也提供了一個利用藍芽連線互相對話的範例程式,
讓我們能快速地了解藍芽在Android中的使用方式。
二、文章開始
首先,
我們先建立一個觀念︰
藍芽一定分成2個端點,
分別為被動的Server端和主動的Client端。
底下是BluetoothChat的Sample Code程式流程︰
=========現在在BluetoothChat.java底下==============
![]() |
| BluetoothChat.java是這個範例程式的主頁面,可以在跟已連線的藍芽設備對話。 |
1.在onCreate()時,呼叫
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();一起來看看官方文件裡怎麼說BluetoothAdapter
BluetoothAdapter represents the local Bluetooth adapter (Bluetooth radio).
The BluetoothAdapter is the entry-point for all Bluetooth interaction.
Using this, you can discover other Bluetooth devices, query a list of bonded (paired) devices, instantiate a BluetoothDevice using a known MAC address, and create a BluetoothServerSocket to listen for communications from other devices.
BluetoothAdapter是區域藍芽接口(藍芽廣播)。BluetoothAdapter也是所有藍芽交易互動的啟始點。用這個接口,我們可以偵測區域內有哪些其它的藍芽裝置、查詢已配對過的藍芽列表、用已知的MAC地址建立一個BluetoothDevice實體、建立一個BluetoothServerSocket來監聽是否有其它藍芽裝置傳來的通訊…等。2-1.得到一個BluetoothAdapter實體之後,
在onStart()裡,
如果沒有啟動藍芽,則要求使用者開啟藍芽。
指令是︰
if (!mBluetoothAdapter.isEnabled()) {
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
// Otherwise, setup the chat session
}else {
if (mChatService == null) setupChat();
}
2-2.透過setupChat()建立起基本的對話視窗和BluetoothChatService背景服務,並把主Thread的Handler傳給Service以供日後傳回message。
mChatService = new BluetoothChatService(this, mHandler);
3.在onResume()裡,也做一樣的事,如果檢查沒有開啟藍芽BluetoothChatService背景服務,則再次開始該服務。
if (mChatService.getState() == BluetoothChatService.STATE_NONE) {
// Start the Bluetooth chat services
mChatService.start();
}
=========現在進入BluetoothChatService.java裡==============
程式碼才剛開出來,
馬上就看到這個Service的程式架構中,塞了3個執行緒,
分別為︰
(1)AcceptThread
(2)ConnectThread
(3)ConnectedThread
馬上來看看它們在Service裡,分別擔任什麼樣的任務︰
// Cancel any thread attempting to make a connection
if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
// Cancel any thread currently running a connection
if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}
// Start the thread to listen on a BluetoothServerSocket
if (mAcceptThread == null) {
mAcceptThread = new AcceptThread();
mAcceptThread.start();
}
1.在onStart()中,檢查如果ConnectThread和ConnectedThread存在,則將他們關掉。
2.啟動一個AcceptThread(現在的流程是在藍芽開啟中的狀態,開啟了一個AcceptThread待命)。
這個AcceptThread存在的目的,是因為程式先假設每臺裝置都有可能想要跟它做藍芽連線。
來看一下這個程式一啟動後就執行的AcceptThread裡面做了些什麼︰
/**
* This thread runs while listening for incoming connections. It behaves
* like a server-side client. It runs until a connection is accepted
* (or until cancelled).
*/
private class AcceptThread extends Thread {
// The local server socket
private final BluetoothServerSocket mmServerSocket;
public AcceptThread() {
BluetoothServerSocket tmp = null;
// Create a new listening server socket
try {
tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
} catch (IOException e) {
Log.e(TAG, "listen() failed", e);
}
mmServerSocket = tmp;
}
public void run() {
if (D) Log.d(TAG, "BEGIN mAcceptThread" + this);
setName("AcceptThread");
BluetoothSocket socket = null;
// Listen to the server socket if we're not connected
while (mState != STATE_CONNECTED) {
try {
// This is a blocking call and will only return on a
// successful connection or an exception
socket = mmServerSocket.accept();
} catch (IOException e) {
Log.e(TAG, "accept() failed", e);
break;
}
// If a connection was accepted
if (socket != null) {
synchronized (BluetoothChatService.this) {
switch (mState) {
case STATE_LISTEN:
case STATE_CONNECTING:
// Situation normal. Start the connected thread.
connected(socket, socket.getRemoteDevice());
break;
case STATE_NONE:
case STATE_CONNECTED:
// Either not ready or already connected. Terminate new socket.
try {
socket.close();
} catch (IOException e) {
Log.e(TAG, "Could not close unwanted socket", e);
}
break;
}
}
}
}
if (D) Log.i(TAG, "END mAcceptThread");
}
public void cancel() {
if (D) Log.d(TAG, "cancel " + this);
try {
mmServerSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of server failed", e);
}
}
}
我們看到了在AcceptThread裡面,埋入了一個BluetoothServerSocket。看一下Bluetooth Android官方對它的解釋︰
BluetoothServerSocket represents an open server socket that listens for incoming requests
(similar to a TCP
BluetoothServerSocket是一個開放式的server socket,用來監聽任何傳進來的請求(原理類似TCP ServerSocket)。為了讓2隻Android devices能夠連線,其中一隻裝置必須開啟server socket。當遠端的藍芽裝置向手上這隻裝備請求連線後,這隻裝置上的BluetoothServerSocket會回傳一個accepted的BluetoothSocket給呼叫那一方。ServerSocket). In order to connect two
Android devices, one device must open a server socket with this class. When a
remote Bluetooth device makes a connection request to the this device, the
BluetoothServerSocket will return a connected BluetoothSocket when the
connection is accepted.因此我們知道,上面程式碼中
BluetoothSocket socket = mmServerSocket.accept();就是應證了BluetoothServerSocket會吐BluetoothSocket出來這件事。
回到一開始呼叫AcceptThread.start()的那個時間點,
也就是說,
程式在一啟動時,
都先要求使用者開啟藍芽,
然後隨時準備接收別臺藍芽裝置會傳送連線請求的事件。
我們取到了BluetoothSocket後,
看看這個BluetoothSocket能做些什麼。
首先,
在官方技術文件提到︰
BluetoothSocket represents the interface for a Bluetooth socket (similar to a TCP
BluetoothSocket是一個Bluetooth socket的接口(原理類似TCP Socket)。這個連結點允許APP透過InputStream和OutpusStream互相交換資料。Socket). This is the connection point that allows
an application to exchange data with another Bluetooth device via InputStream
and OutputStream.因此我們得知,
BluetoothSocket可以讓我們做到資料交換的功能。
因為在Service onStart()呼叫AcceptThread.start()後,
馬上將藍芽狀態設定成setState(STATE_LISTEN);
因此,在switch迴圈中,
程式執行了connected()函式。
這段程式碼如下︰
case STATE_LISTEN:
case STATE_CONNECTING:
// Situation normal. Start the connected thread.
connected(socket, socket.getRemoteDevice());
break;
馬上來看看connected()函式做了哪些事
/**
* Start the ConnectedThread to begin managing a Bluetooth connection
* @param socket The BluetoothSocket on which the connection was made
* @param device The BluetoothDevice that has been connected
*/
public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) {
if (D) Log.d(TAG, "connected");
// Cancel the thread that completed the connection
if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
// Cancel any thread currently running a connection
if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}
// Cancel the accept thread because we only want to connect to one device
if (mAcceptThread != null) {mAcceptThread.cancel(); mAcceptThread = null;}
// Start the thread to manage the connection and perform transmissions
mConnectedThread = new ConnectedThread(socket);
mConnectedThread.start();
// Send the name of the connected device back to the UI Activity
Message msg = mHandler.obtainMessage(BluetoothChat.MESSAGE_DEVICE_NAME);
Bundle bundle = new Bundle();
bundle.putString(BluetoothChat.DEVICE_NAME, device.getName());
msg.setData(bundle);
mHandler.sendMessage(msg);
setState(STATE_CONNECTED);
}
為了避免重覆連線,先檢查有沒有已存在的ConectThread、ConnectedThrad和AcceptThread。
如果有,一律先關掉。
然後,啟動ConnectedThread,
並將MESSAGE_DEVICE_NAME用handler(mHandler,還記得我們前面有提到在BluetoothChat.java傳了一個主Thread的Handler給Service嗎?)傳訊的方式,
將Client端的裝置資料傳回BluetoothChat.java,
讓Server端知道是誰在跟它做連結。
前面提到,
一旦取得了BluetoothSocket之後,
就可以開始執行互相傳遞資料的工作了 。
這個被啟動的ConnectedThread就是在做資料互傳的監聽工作,
我們看看ConnectedThread做了些什麼
/**
* This thread runs during a connection with a remote device.
* It handles all incoming and outgoing transmissions.
*/
private class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;
public ConnectedThread(BluetoothSocket socket) {
Log.d(TAG, "create ConnectedThread");
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
// Get the BluetoothSocket input and output streams
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "temp sockets not created", e);
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
Log.i(TAG, "BEGIN mConnectedThread");
byte[] buffer = new byte[1024];
int bytes;
// Keep listening to the InputStream while connected
while (true) {
try {
// Read from the InputStream
bytes = mmInStream.read(buffer);
// Send the obtained bytes to the UI Activity
mHandler.obtainMessage(BluetoothChat.MESSAGE_READ, bytes, -1, buffer)
.sendToTarget();
} catch (IOException e) {
Log.e(TAG, "disconnected", e);
connectionLost();
break;
}
}
}
/**
* Write to the connected OutStream.
* @param buffer The bytes to write
*/
public void write(byte[] buffer) {
try {
mmOutStream.write(buffer);
// Share the sent message back to the UI Activity
mHandler.obtainMessage(BluetoothChat.MESSAGE_WRITE, -1, -1, buffer)
.sendToTarget();
} catch (IOException e) {
Log.e(TAG, "Exception during write", e);
}
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect socket failed", e);
}
}
}
是的!有看到嗎?
ConnectedThread正在用BluetoothSocket取得InputStream和OutputStream,
並透過旗下的write()和read()在做2隻藍芽裝置的溝通!
現在剩下ConnectThread還沒有去理解了,
查了一下ConnectThread被start的時間是發生在一開始對話頁面的menu鍵中!
原來ConnectThread的目的是要主動連接其它已開啟藍芽的裝置。
當使用者點擊Connect a device時,
會啟動ConnectThread,
開始尋找附近的藍芽裝置,
並對對方發出連線訊號,
對方監聽到你的配對要求後,
對方手機裡原本程式就開啟中的AcceptThread便答應你的請求,
然後開啟ConnectedThread,
並利用連結成功後得到的BluetoothSocket和你做藍芽傳輸溝通。
主動連線端是Client端,被動接收端是Server端,
就好像精子與受精卵…
三、總結
在這裡我把整個程式流程重覆敍述一次︰
在連線的一開始,兩隻手機的程式一開始都先建立一個AcceptThread
(因為誰都不知道誰最後會成為被動接收的Server端,誰又是主動的Client端),然後都跟RFCOMM頻道索取這隻app專屬的BluetoothServerSocket實體。
Server方做了些什麼︰
用BluetoothServerSocket這個實體去等待Client端用ConnectThread發出的請求連線事件,
連線若成功會得到這次藍芽溝通專用的BluetoothSocket。
Client方做了些什麼︰
Client端執行ConnectThread。
1.Client端在與Server方連線(Connect a deivce)之前,
會先取得到Server端的身份證MAC address,
並用該address得到Server端的BluetoothDevice實體。
2.Client端藉由自己的MY_UUID和Server端的BluetoothDevice實體,
從RFCOMM頻道拿到這次藍芽溝通專用的BluetoothSocket。
兩方在這個時候都拿到這次藍芽溝通專用的BluetootheSocket,
也都在此時知道了對方的BluetoothDevice實體(知道對方的身份)。
這時候雙方都同時開啟ConnectedThread,
彼此利用BluetoothSocket互相做資料傳輸。
註︰資料傳輸利用 InputStream和OutputStream。
Thursday, April 12, 2012
串接 Google Play In-app-billing 易犯的錯誤
撰寫時間︰2012/04/13 11:41
更新時間︰2012/11/05 11:04
文章更新次數︰11
一、前言
之前提到,
臺灣目前雖然無法購買付費型APP,
但卻可以使用In-app-billing機制來獲利。
Google Play的In-app-billing機制很完善,
因此在機制底下的規矩也很多。
新手在串接時,
可能因此發生了一堆奇奇怪怪的錯誤。
這邊我把遇到的問題跟大家分享,
這些都是我很寶貴的出錯經驗。
二、文章開始
我在串接Google Play in-app-billing時發生過的問題及錯誤如下︰
1.關於應用程式產品內ID值的問題
雖然Google Play In-app-billing Document已經很明確的教導我們ID值的命名規則是︰
(1)應用程式產品內ID沒有照規則走
產品IDs是以唯一性的方式跨越應用程式命名空間。產品ID必須啟始字元必須為小寫或數字,而且組成的字串也都只能有小寫(a-z)、數字(0-9)、下
底線(_)和小數點(.)。以"android.test"開頭的產品ID命名被保留,任何以android.test開頭的命名方式皆不可用。附帶一
提,當您在建立產品ID後,是無法修改的,而且您也無法重覆使用同一組產品ID。
有時候為了搶時間,這個ID值沒有照規範走就定義在程式碼中並上傳成草稿APK至Google Play Publisher。
直到要在Publisher後臺添加 應用程式產品內ID值 時,才發現自己沒有遵照規範。
因此程式碼內的 應用程式產品內ID值 又要再改一次,再重新上傳一個新的草稿APK。
更糟的是︰
如果你的APP有做package name控管,那麼你的package name一定會超出控管。
因為同樣的package name的APP,即使你從後臺刪除又重新上傳,Google Play都會把它們當成是同一隻APP。這使得你package name需要重新命名才能完成上傳,因而package name已經不是你原本希望的名稱了。
(2)設定Publisher後臺 應用程式產品內ID值 的[受管理]與[不受管理]分類時請小心!
由於 應用程式產品內ID值 分成受管理和不受管理類,
這個值如果沒有設定好就儲存或發佈,
後來發現設定錯了,
即使刪除,
都不能再在同一個APP內設定同一個產品ID了。
這很麻煩,
因為程式還要為了這個不小心的錯誤,
重新改程式碼中對應的ID值、重新上傳草稿APK、重新測試…
2.點擊購買流程,iap視窗彈出「這個版本的應用程式還無法用付款功能。」
這算是一個新手錯誤。如果查看一下LOG,收到的LOG應該是
RESULT_DEVELOPER_ERROR官方文件對這個LOG的定義如下︰
此回應指出您的APP試圖發送iap請求,但是APP的AnddroidManifest.xml裡卻沒有宣告
com.android.vending.BILLING權限。也可能是因為應用程式沒有正確的被簽署,或者您發送了一個非正確格式的請求,像是忘了傳
Bundle的key值或者是使用了一個無法被識別的請求類型。
官方文件曾經說,要測試iap內容,有幾個條件需要實作︰(1)手機的primary account要設成test account如果不是,請重設為原廠設定,這是官方建議我們的。(2)test account要設定在Google Play Publisher的編輯個人資料的測試帳戶底下。因為是測試iap,因此我們上傳的草稿apk不用被發佈,只要儲存即可。但是 應用程式內產品ID 一定要被發佈。但是這樣要怎麼找的到這些沒有被發佈的應用程式內ID呢?答案就是要將手機的account設到這邊當成test account。(3)androidManifest.xml裡要宣告<uses-permission android:name="com.android.vending.BILLING" />而且重點是,不僅要宣告這行,還要把這行放在<manifest>和<application>中間。
(4)請確認裝置上的版編、版號、keystore與上傳的草稿APK的版編、版號、keystore一致
因為我們不能用debug.keystore上傳app,因此,在做內部測試時,
請確認線上的草稿app的keystore是和裝置上要測試的版本的keystore是一致的。
官方文件Testing In-app Billing單元有提到︰
上傳您的草稿APP至發佈網站。您無需發佈您的APP才能執行端點測試和真實產品ID的消費。您僅需以草稿的方式將您的APP上傳。然而,
您必須將您的APP簽署上那把你平時釋出APP時,專用的金鑰。而且,您上傳的APP版號必須和你裝置上要執行測試的APP的版號一致。如果想知道關於如
何上傳APP至Android市集,請見 Uploading
applications。
我曾經將草稿APK上傳了,但是就是一直回應我這個版本的應用程式還無法用付款功能。我將這行AndroidManifest.xml裡的宣告改變位置,
然後將package name和
應用程式內產品ID 全部重新定義過,也都無法解決,
最後才發現我裝置上的APK的版編版號沒有和線上的『已發佈』APK一致。
這些細節如果沒仔細去看,
真的會很折磨人。
註︰
2012/11/05
有時候為了不要讓服務直接上線(還在內測階段),會上一隻假的APK檔上架Google Play來測試裝置上DEBUG模式的真實APK。如果上傳的假APK裡Android Manifest屬性和手上實測的DEBUG模式真實APK裡的Android Manifest屬性差太多,可能也會造成即使線上和手上裝置版本號一致,但仍show出此版本無法購買的問題。此時建議直接再上傳一個新版號做測試即可。
2012/11/05
有時候為了不要讓服務直接上線(還在內測階段),會上一隻假的APK檔上架Google Play來測試裝置上DEBUG模式的真實APK。如果上傳的假APK裡Android Manifest屬性和手上實測的DEBUG模式真實APK裡的Android Manifest屬性差太多,可能也會造成即使線上和手上裝置版本號一致,但仍show出此版本無法購買的問題。此時建議直接再上傳一個新版號做測試即可。
3.payload很好用,但是使用它是有條件的。
我們知道在request Purchase時,可以附一個payload給Google Play,
屆時如果交易成功,
傳回來的Json裡面會傳回這個payload。
因此這個payload變得很好用,
因為我們可以拿這個值來做虛擬幣加值的依據之類的。
但,
這個payload只會在正式金流交易下回傳過來,
如果你請求購買的
應用程式內產品ID 是android.test.purchased之類的,很抱歉,
傳回來的Json是不會附帶這個payload的。
4.不要將你的購買成功後的程式動作放在Response_OK後面。
@Override
public void onRequestPurchaseResponse(RequestPurchase request,
ResponseCode responseCode) {
if (Consts.DEBUG) {
Log.d(TAG, "331 "+request.mProductId + ": " + responseCode);
}
//付費成功不是在這裡處理。這裡只是一般購買請求Google Play是否答應的接收處
if (responseCode == ResponseCode.RESULT_OK) {
if (Consts.DEBUG) {
Log.d(TAG, "335 purchase was successfully sent to server");
}
// MainActivity.addMoney();//不是在這裡加值
// logProductActivity(request.mProductId, "sending purchase request");
} else if (responseCode == ResponseCode.RESULT_USER_CANCELED) {
if (Consts.DEBUG) {
Log.d(TAG, "340 user canceled purchase");
}
// logProductActivity(request.mProductId, "dismissed purchase dialog");
} else {
if (Consts.DEBUG) {
Log.d(TAG, "346 purchase failed");
}
// logProductActivity(request.mProductId, "request purchase returned " + responseCode);
}
}
在官方的iap教學文檔中,我們實作了PurchaseObserver。
這個PurchaseObserver被呼叫的時間點,
是在Google Play對iap購買交易有完成的成功回應時,
在ResponseHandler.java呼叫purchaseResponse(), 找我們實作的PurchaseObserver底下去執行程式相關的動作。
但是,
ResponseCode.RESULT_OK只是代表我們可以執行iap購買,
不代表交易成功了。
還記得in-app-billing交易流程圖嗎?
ResponseCode.RESULT_OK只是這個交易flow的第2條而已。
真正表示你交易成功會發生在第7點︰PURCHASE_STATE_CHANGED。
因此交易成功要在onPurchaseStateChange()函式裡實作,
程式如下︰
@Override
public void onPurchaseStateChange(PurchaseState purchaseState, String itemId,
int quantity, long purchaseTime, String developerPayload) {
if (Consts.DEBUG) {
Log.d(TAG, "286 onPurchaseStateChange() itemId: " + itemId + " " + purchaseState);
}
// if (developerPayload == null) {
// logProductActivity(itemId, purchaseState.toString());
// } else {
// logProductActivity(itemId, purchaseState + "\n\t" + developerPayload);
// }
//購買完成會呼叫這裡
if (purchaseState == PurchaseState.PURCHASED) {
Log.w(TAG, "297 onPurchaseStateChange: PURCHASED");
// Toast.makeText(IapPage.this, R.string.purchased_success, Toast.LENGTH_SHORT).show();
MainActivity.addMoney(purchaseTime, developerPayload);
// mOwnedItems.add(itemId);
}
5.老王不能自己買瓜
剛開始串接IAP的新手有時候會遇到「找不到項目」的錯誤,
看LOG回覆的錯誤訊息,並且參照Reference通常都能找出錯誤原因,
我曾遇過的錯誤是因為
RESULT_ERROR,查了一下In-app Billing Reference後,發現問題是因為我自己是販售者,我仍然又用販售者的身份去購買in-app Billing的商品,自己跟自己買東西,這件事在Google Wallet是不被允許的。 因此,如果你現在用Test Account購買商品,記得手機登入的Primary Account不能跟販售者的account相同。 但是,如果你是第1次為這隻APP發佈in-app-billing商品時,
Google是需要花時間去處理的(我的實測是2個小時候才找的到該商品,因為中午吃了一個便當,今天吃7-11的義大利麵)。
所以,你發佈出去不代表馬上就能找到應用程式內商品,
等一下他們吧!
註︰最近Google好像不提供不發佈APK、僅發佈iab商品測試消費了,
三、結論
就是這些了。 這些經驗花了我半年 + 卡住數次得來。
它們都是我在串接Google Play iap時常遇到的錯誤。
如果之後還有機會遇到任何的錯誤,
我會把經驗分享上來。
(當然會希望不要再有了!!)
只希望之後大家在串接時,
不要再犯我犯過的錯了。
四、附註
1.官方的Sample Code主程式中unregister observer時的時間點錯了(範例文件將unregister放在onStop()去執行),
因為我們不知道系統什麼時候會讓Activity進入onStop()狀態,
造成原本應該監聽iap後續動作的observer被系統終止了。
這會造成交易完成後,我們實作的observer有時候會無法順利被呼叫。
最好是移到onDestroy()再去執行ResponseHandler.unregister(myPurchaseObserver);
SurfaceView 的onSurfaceDestroy()的系統呼叫時機
一開始以為surfaceDestroy()這個SurfaceView.callback實作函式一定會在Activity onPause()呼叫後被執行,結果發現不然。
基本上SurfaceView的callback函式與Activity5大進程之間的運行過程為︰
onCreate()-->onStart()-->onResume()-->surfaceCreated()-->onPause()-->surfaceDestroy()-->onStop-->onDestroy()
如果不能掌握surfaceDestroy()被呼叫的時間點,
就很難確保下次該Activity有沒有重新執行surfaceCreate(),
這會造成程式執行上很重大的問題…
查了一下倒底surfaceDestroy()的呼叫時間點為何?
後來在Android Developer看到了一段話︰
原來SurfaceView的運作時機是建構在Activity的View是否有"黏(attached)"到window上判定。
我們知道,
window是Android最底層的視窗管理元件(看一下我之前PO的這篇),
view是後來才黏上去的產物,
當系統判定當下的Activity的view"擺脫(detached)"了window後,
SurfaceView的surfaceDestroy()函式才會被呼叫。
想半天為什麼Intent另一個Activity出來時,
畫SurfaceView的這個Activity為什麼onPause()後沒有呼叫surfaceDestroy(),
原來是這個原因=.=||
基本上SurfaceView的callback函式與Activity5大進程之間的運行過程為︰
onCreate()-->onStart()-->onResume()-->surfaceCreated()-->onPause()-->surfaceDestroy()-->onStop-->onDestroy()
如果不能掌握surfaceDestroy()被呼叫的時間點,
就很難確保下次該Activity有沒有重新執行surfaceCreate(),
這會造成程式執行上很重大的問題…
查了一下倒底surfaceDestroy()的呼叫時間點為何?
後來在Android Developer看到了一段話︰
The way SurfaceView works is that its Surface is created when the view
is attached to a window, and destroyed when it is detached. There is
no provision for you to do destroy it at other points, nor is that a
good idea because if the system ever needs to show that part of your
window you will end up with a big hole in the window (where the
SurfaceView exists, but without a Surface to display).
is attached to a window, and destroyed when it is detached. There is
no provision for you to do destroy it at other points, nor is that a
good idea because if the system ever needs to show that part of your
window you will end up with a big hole in the window (where the
SurfaceView exists, but without a Surface to display).
原來SurfaceView的運作時機是建構在Activity的View是否有"黏(attached)"到window上判定。
我們知道,
window是Android最底層的視窗管理元件(看一下我之前PO的這篇),
view是後來才黏上去的產物,
當系統判定當下的Activity的view"擺脫(detached)"了window後,
SurfaceView的surfaceDestroy()函式才會被呼叫。
想半天為什麼Intent另一個Activity出來時,
畫SurfaceView的這個Activity為什麼onPause()後沒有呼叫surfaceDestroy(),
原來是這個原因=.=||
標籤:
Android,
SurfaceView
Sunday, March 11, 2012
談談Android的螢幕解析度
撰寫時間︰2012/03/12 13:15
更新時間︰2012/03/23 11:54
文章更新次數︰3
一、前言
Android手機有多種解析度,
視覺或美工在設計版面時,
倒底該怎麼做,
才能讓每種不同規格的Android手機,
表現出其最大效能?
二、文章開始
讓我們先從Android的圖形資源檔說起。
左邊這張圖是Android置放圖片的資料夾,
如果以手機來說,
至少可以分成4種資料夾,
分別為drawable、drawable-hdpi、drawable-mdpi和drawable-ldpi。
因為這樣的資料夾分類,
讓每種解析度的手機,
能聰明的依照它們自己的螢幕密度(density),
找到他們需要的圖檔應該要在哪個目錄取得。關於這4個資料夾的使用方式,
以及該放什麼圖片進去,
我會在稍後提及。
註︰
需要先在AndroidMenifest.xml裡將Any density設為true,
手機才會依自己的螢幕密度(density)去找適當的drawable資料夾,
否則會一律從drawable這個預設目錄去取得圖形資源。(如果又沒有設drawable資料夾,
則會從其它的圖形資料夾下找到該圖形資源,
然後依照density的比例去自動縮放。
如果你現在看不懂我說的,
無妨。
因為等一下我還會再說明。)
![]() |
| Android官方技術文件中提供目前Android的解度析有以上這些規格(但實際上有更多) |
小鰻當時也花了很久的時間在摸索,
當然,要怎麼知道手機的density,
Google很多人都有教了,
不在此特做說明。
以欄位來看,
第1欄屬於ldpi的手機規格至少有︰
QVGA(240x320)、WQVGA400、WQVGA432、WVGA800、WVGA854...。
Android Design Guide建議我們在處理每種解析度的圖片問題時,
可以使用底下2種方式︰
1.先從mdpi的規格去製圖,然後再使用photoshop之類的圖片編修軟體,向上調整尺寸和向下調整。
2.從最大尺寸去製圖(假設你想開發手機App,可以從Galaxy Nexus的density320開始繪製),然後再向下縮小。
Android的最基本尺寸規格是Medium Density(160) - mdpi,
任何其它的Android尺寸,
都是由mdpi去向上或向下延伸換算出來的,
因此我個人是比較推薦使用第1種 - 從mdpi去調整圖形大小的方法來製圖。
photoshop是Google很推薦的繪製工具,
因為使用這套軟體,
可以自由的拉大和縮小圖形也不會失真(以專案檔的方式來說的話)。
如同我前面所說,
如果要設計手機版面,
至少會有4種drawable資料夾,
這裡解釋一下這4個資料夾的用法。
1.drawable
drawable資料夾相當於drawable - nodpi,如果手機從這個資料夾抓出圖片,
那麼系統繪製圖片的方式,
是"圖片有多大就畫多大",
以1:1的方式去繪製。
所以,如果使用這個資料夾,
通常還要在程式裡做很多螢幕解析度的換算,
不然會遇到圖片在小手機,
根本完全跑版、超出手機螢幕大小的問題。
2.drawable - mdpi
mdpi(density160)是Android標準的規格尺寸,任何其它dpi,都是從mdpi去演算及延伸的,
強列建議先由mdpi去排版製圖,
然後再去調整其它的解析度。
3.drawable - hdpi
前面說到,Android的標準尺寸是mdpi(density160),
而hdpi則是density240,
240(hdpi density) /160(mdpi density) = 1.5。
這樣的比例換算下來,
螢幕顆粒密度比原本的mdpi還要精密,
因此,
你會發現圖片在mdpi中看是正常大小,
但到hdpi的手機來看,
圖片被明顯縮小了。
因此,
這個資料夾裡的圖片,
你必須畫得比drawable - mdpi的圖檔還大。
大幾倍?
答案是...150%
這個值就是剛才240/160=1.5算出來的值,
你必須將mdpi的那張圖,
用photoshop放大150%另存新檔,
放在drawable - hdpi來用,
才能顯示跟drawable - mdpi一樣的畫面大小。
3.drawable - ldpi
ldpi的Density為120,120/160 = 0.75,
因此,
使用photoshop將剛才mdpi的尺寸,
縮小成75%,
放進drawable - ldpi目錄,
將能讓呈現出來的畫面,
跟mdpi的畫面一樣。
![]() |
| 如果要製作不同解析度的icon,你需要對每一種dpi去客製化,才能在每種手機上呈現一樣的結果。 |
如果你問我︰
「天啊!
我一定要花那麼多的工夫在製做不同尺寸的解析度嗎?」
這個答案是可選擇的。
why?
如果你今天開發的App,
畫面只繪製mdpi,
那有沒有辦法在ldpi和hdpi正常顯示呢?
答案是...可以。
把drawable目錄刪掉,
剩下其它3個目錄(drawable - ldpi、drawable - mdpi、drawable - hdpi),
且將繪好的mdpi圖形,
丟進drawable - mdpi,
如果程式發現現在使用的手機,
是ldpi或hdpi時,
系統會自動去換算放大或縮小,
也不會讓原本設計給mdpi的圖片尺寸,
在hdpi或ldpi中跑版。
但...這就牽扯到視覺(美工)人員最在意的品質問題。
品質是視覺(美工)人員的生命,
他們的職責就是替產出的作品負責,
畫面上的每1 mm,
都是比自己的小孩還要親的小孩。
當然,
也是有視覺(美工)人員是比較...隨遇而安的心態啦...。
讓系統去自動縮放圖片大小,
壞處有下︰
1.如果手機解析度比mdpi還大(如hdpi),
那麼這張圖將被拉大,
畫面將沒那麼精美了。
2.如果手機解析度比mdpi還小(如ldpi),
那麼這張圖將被縮小,
看起來似乎沒什麼問題,
但你要知道,
使用者手上拿的這種ldpi手機,
通常都是效能極差的Android手機(我們稱它為Android低階機,講好聽叫入門機),
以台灣來說,
通常都是電信業者綁門號,
0元就能帶回家的Android手機。
這種品質的手機,
你在開發時,
會發現它的所有硬體支援度都比中高階的Android手機還差。
如果iPhone開發工程師已經在開發下一款產品時,
老闆為了要旗下的產品支援所有Android手機,
你會發現你還在調校這些低階手機。
現在,
你把mdpi大小的圖片塞進這隻效能極差的ldpi手機中,
因為效能極差,
沒多久,
你就會遇到記憶體不足(我前面的文章有提到)的問題!
三、結論
無論你是視覺(美工)人員或者是Android程式設計師,不管是螢幕畫面還是手機效能,
最好都還是替不同的螢幕解析度去客製化不同的圖片大小,
一來控制畫面品質,
二來也能確保程式穩定度。
這些細心,
每一種規格的Android手機使用者都能依各自的可用資源看到你呈現出來的產品,
何樂而不為?
四、備註
我開發過的手機和平板規格︰1.Nexus one(800x480, density 240)
2.Nexus S(800x480, density 240)
3.Galaxy Nexus(1280x720, density 320)
4.HTC Hero(480x320, density 160)
5.Sony Arc(854x480, density 240)
6.遠傳小精靈(320x240, density 120)
7.HTC野火機1代(320x240, density 120)
8.Samsung SII(800x480, density 240)
9.HTC Flyer平板(1024x600, density 160)
10.Samsung Tab 7.7(1024x600, density 170)
11.HTC Desire(800x480, density 240)
12.HTC Incredible(800x480, density 240)
13.HTc Sensation(960x540, density....)
還有更多...
以上如果規格或Density有錯,
煩請告知。
感謝
其它導讀︰
1.處理多螢幕解析度問題-PartII(外文)
標籤:
Android,
density,
Drawable,
resolution,
screen
Tuesday, March 6, 2012
Android刷機工具 Odin
刷Kernal的方式
如果kernal是.zip,使用recovery mode刷入;如果Kernal是.tar,則用Odin,不要勾選Re-partition(不分區),放入PDA中直接刷!
小技巧
- 刷Rom前、後最好都雙Wipe(進入Recovery mode執行Wipe Data和Wipe cache partition)
- 先刷完Rom後再刷kernal
- 通常kernal都是放進PDA裡刷
Monday, March 5, 2012
[技術公報]Android App打破50MB的上傳限制了
Android Apps Break the 50MB Barrier
Posted by Tim Bray on 05 March 2012
at
5:00 PM
Android applications have historically been limited to a maximum
size of 50MB. This works for most apps, and smaller is usually
better — every megabyte you add makes it harder for your users to
download and get started. However, some types of apps, like high-quality
3D interactive games, require more local resources.
Android app先前將最大檔案容量限制在50MB,這個方案在許多的App上運作的很好 - 因為你所編譯並產出的每megabyte都會讓你的使用者需要花時間下載後才能啟動。然而,有一些類型的App,像是高畫質的3D互動遊戲,卻需要更大量的本地端資源。
So today, we’re expanding the Android app size limit to 4GB.
因此,我們今天將Android App的容量限制提高至4GB。
The
size of your APK file will still be limited to 50MB to ensure secure
on-device storage, but you can now attach expansion files to your APK.
APK檔容量其實仍然限制在50MB,這是為了確保裝置儲存的安全性。但是你現在還可以添加擴充檔至您的APK中。 - Each app can have two expansion files, each one up to 2GB, in whatever format you choose.每個App有2個擴充檔,每一個擴充檔最高是2GB,沒有檔案格式上的限制。
- Android Market will host the files to save you the hassle and cost of file serving.Android市集會將這2個充檔保留在Android Server中,一方面省去了你開發上的麻煩,也節省你的開發成本。
- Users will see the total size of your app and all of the downloads before they install/purchase.使用者會在他們安裝/購買前,看到你App的總量和總下載次數。
On
most newer devices, when users download your app from Android Market,
the expansion files will be downloaded automatically, and the refund
period won’t start until the entire download completes. On older
devices, your app will download the expansion files the first time it
runs, via a downloader library which we’ve provided below.
在大多數新型裝置中,當使用者從Android市集下載了您的App,擴充檔也會跟著被自動下載,此時,15分鐘的退費期間將不會被算在內,15分鐘的限制將會在全部的檔案都下載完成後才會開始計算。然而,在一些舊有的裝置中,你的App會在下載並執行App的第1次啟動後,才開始透過我們底下提供的downloader函式庫下載擴充檔。
While
you can use the two expansion files any way you wish, we recommend that
one serve as the initial download and be rarely if ever updated; the
second can be smaller and serve as a “patch carrier,” getting versioned
with each major release.
雖然您可以依照你希望的方式來使用這2個擴充檔,我們仍建議您將其中一個擴充檔用在初始化下載,若是要更新App,盡量不要用它。另一個擴充槽則可設計成被置放較小的檔案,提供"更新補丁(patch carrier)",用來做每個版本主要更新的內容。Helpful Resources有用的資源
In order to
make expansion file downloading as easy as possible for developers,
we're providing sample code and libraries in the Android SDK Manager.
為了能簡單且有效的讓開發者使用擴充檔這個資源,我們在Android SDK Manager提供了簡單的範例程式和函式庫。 - In the Google Market Licensing package, an updated License Verification Library (LVL). This minor update mostly adds the ability to obtain expansion file details from the licensing server.在Google市集的許可套件中,一個更新的許可驗証函式庫(LVL)。能過這個小更新取得擴充檔的詳細資訊。
- From the Google Market APK Expansion package, the downloader service example. The library makes it relatively simple to implement a downloader service in your application that follows many of our best practices, including resuming downloads and displaying a progress notification.從Google市集APK擴充套件來的downloader服務範例。函式庫非常容易從downloader服務去實作,因為這是從我們很多很棒的實作中去完成的 - 功能包含了續傳下載和訊息通知欄的下載進度顯示。
Because
many developers may not be used to working with one or two large files
for all of their secondary content, the example code also includes
support for using a Zip file as the secondary file. The Zip example
implements a reasonable patching strategy that allows for the main
expansion file to “patch” the APK and the patch file to “patch” both the
APK and the main expansion file by searching for asset files in all
three places, in the order patch->main->APK.
由於許多的開發者也許不會用他們次要的擴充檔來放置1~2個大型檔案,因此範例程式用zip格式來做次要檔的放置。zip範例的實作是合理的策略,允許主要的擴充檔以"patch" APK的方式來擴充,(恕刪)...........順序是ptch-->main-->APK。Expansion File Basics擴充檔基本概念
Expansion
files have a specific naming convention and are located in a specific
place for each app. As expansion files are uploaded to the publisher
site, they are assigned a version code based upon the version of the APK
that they are associated with. The naming convention and location are
as follows:
擴充檔在每一個app中,都擁有特定的命名慣例和置放的位置。擴充檔被更新至publisher網站,也被指定到相關連的某個version code以上的apk中。命名慣例和位置依照︰Location:<shared-storage>/Android/obb/<package-name>/
Filename:[main|patch].<expansion-version>.<package-name>.obb
Example:/sdcard/Android/obb/com.example.myapp/main.5.com.example.myapp.obb
Expansion files are stored in shared storage. Unlike APK files, they can be read by any application.
擴充檔被儲存在分享空間。它們不像APK檔,因此可以被任何的APP自由的讀取。Downloading and Using the Expansion Files下載並使用擴充檔
When
the primary activity for the app is created, it should check to make
sure the expansion files are available. The downloader library provides
helper functions (for example the “Helpers” class in the code below) to
make this easy.
當主要activity被建立後,應該要檢查並確保是否有可用的擴充檔。downloader函式庫提供了一個helper功能(範例程式中被命名為"Helper"類,下方的程式中也可看到),讓開發者方便使用。boolean expansionFilesDelivered() {
// get filename where main == true and version == 3
String fileName = Helpers.getExpansionAPKFileName(this, true, 3);
// does the file exist with FILE_SIZE?
if (!Helpers.doesFileExist(this, fileName, FILE_SIZE, false))
return false;
}
return true;
}
}

If the file does not exist, fire up the downloader service with
如果擴充檔沒有放在SD卡的指定位置,可以使用DownloaderClientMarshaller.startDownloadServiceIfRequired().
The downloader will perform an LVL check against the server. This
check will deliver the names of the files, file sizes, and the file
URLs.DownloaderClientMarshaller.startDownloadServiceIfRequired()來啟動downloader服務。downloader會針對server執行LVL檢驗,檢驗會傳遞檔案名稱、檔案大小和檔案的URLs。
Once that check has been completed, it will begin
downloading the files. You don’t have to use our download solution, but
you might want to because we:
一旦檢查機制完成,就會開始下載檔案。也許你不需要使用我們提供的下載方案,但是您可能還是會用到,因為我們提供了︰- Include a notification UI that provides progress and estimated completion time in layouts customized for ICS and pre-ICS devices訊息通知介面,能在ICS和ICS版本之前的裝置提供了進度條和下載時間估算。
- Resume large files safely提供安全性的續傳檔案。
- Handle redirection with appropriate limits重新定位apk的適量限制。
- Run in the background as a service提供背景服務下載。
- Pause and resume downloads when WiFi is not available當WiFi失效時,提供暫停和續傳的功能。
Enjoy!
We can’t wait to see what kinds of things developers do with this! For
more information about how to use expansion files with your app, read
the APK Expansion Files developer guide.
好好享用吧!我們迫不及待想看到開發者使用這些功能!更多擴充檔的資訊,請閱讀開發者導引的APK Expansion Files單元。[This post wasn’t actually written by anyone, but bashed out by a posse of engineering and product-management people. Heavy bashers included Dan Galpin, Ilya Firman, Andy Stadler, Michael Siliski, and Ellie Powers.]
Saturday, February 18, 2012
官方提供的Action Bar向下相容元件
撰寫時間︰2012/02/18 18:25
更新時間︰2012/02/20 125:35
文章更新次數︰2
![]() |
| Action Bar是Google在Android3.0後 力推的Android特色元件之一。 開發者能利用此元件 將簡潔實用的功能提供給使用者。 |
Android 3.0的新元件「Action Bar」何時才能向下相容(在pre-HoneyComb執行)?
前天在Android Resource裡看到有如何將Action Bar向下相容的方法。
但是在Android SDK資料夾裡的Sample卻找不到,
只好將Sample Code頁面裡的內容,
一頁一頁在Eclipse中貼成一個新的Android專案,
花了一個早上在三星辦公室的時間,
才將這個專案建立完成。
檔案在此,請用Eclipse import Archive File的方式匯入。
匯入方式如下︰
標籤:
Action Bar,
Android
Friday, February 17, 2012
[Android雜談] - Activity、Window、View的關係
剛才在查Window在Android中的定義,
無意中爬文到此篇文章。
解釋的還不錯。
以下文章轉載自花郎部落格
一、首先說說View和ViewGroup吧
Android中的所有UI類都是建立在View和ViewGroup的這兩個類的基礎上。
所有View的子類稱為“Widget”,所有ViewGroup的的子類稱為“Layout”。
View和ViewGroup之間採用了組合設計模式,可以使得“部分 - 整體”同等對待。
ViewGroup的是佈局容器類的最上層,佈局容器裡面又可以有View和ViewGroup。
當我們運行程式的時候,用了一setContentView()方法,Activity其實不是顯示視圖(直觀上感覺是它),實際上Activity調用了PhoneWindow的setContentView()方法,然后加載視圖,將視圖放到這個Window上,而 Activity其實構造的時候初始化的是Window (PhoneWindow),Activity其實是個控制單元,即可視的人機交互界面。
打個比喻:
Activity是一位工人,它控制Window;
Window是一面顯示畫面,用來顯示信息;
View就是要顯示在顯示畫面上的信息,這些View都是層層重疊在一起(通過infalte()和addView())放到Window顯示畫面上的。
而LayoutInfalter就是用來生成View的 一個工具,XML佈局文件就是用來生成View的原料。
再來說說代碼中具體的執行流程
setContentView(R.layout.main)其實就是下面内容。
(註釋掉本行執行下面的代碼可以更直觀)
getWindow().setContentView(LayoutInflater.from(this).inflate(R.layout.main, null))
即運行程式後,Activity會調用PhoneWindow的setContentView()來生成一個Window,而此時的 setContentView就是那個最底層的View。然後通過LayoutInflater.infalte()方法加載佈局生成View對象並通過 addView()方法添加到Window上,(一層一層的疊加到Window上)
所以,Activity其實不是顯示視圖,View才是真正的顯示視圖。
註:一個Activity構造的時候只能初始化一個 Window(PhoneWindow),另外這個PhoneWindow有一個”ViewRoot”,這個”ViewRoot”是一個View活 ViewGroup,是最初始的視圖,然後通過addView方法將View一個個層疊到ViewRoot上,這些層疊的View最終放在Window 這個載體上面
無意中爬文到此篇文章。
解釋的還不錯。
以下文章轉載自花郎部落格
一、首先說說View和ViewGroup吧
Android中的所有UI類都是建立在View和ViewGroup的這兩個類的基礎上。
所有View的子類稱為“Widget”,所有ViewGroup的的子類稱為“Layout”。
View和ViewGroup之間採用了組合設計模式,可以使得“部分 - 整體”同等對待。
ViewGroup的是佈局容器類的最上層,佈局容器裡面又可以有View和ViewGroup。
二、LayoutInflater,LayoutInflater.inflate()是什麼意思?
LayoutInflater是一個用來實例化XML佈局文件為View對象的類
LayoutInflater.infalte(R.layout.test,null)用來從指定的XML資源中填充一个新的View。當我們運行程式的時候,用了一setContentView()方法,Activity其實不是顯示視圖(直觀上感覺是它),實際上Activity調用了PhoneWindow的setContentView()方法,然后加載視圖,將視圖放到這個Window上,而 Activity其實構造的時候初始化的是Window (PhoneWindow),Activity其實是個控制單元,即可視的人機交互界面。
打個比喻:
Activity是一位工人,它控制Window;
Window是一面顯示畫面,用來顯示信息;
View就是要顯示在顯示畫面上的信息,這些View都是層層重疊在一起(通過infalte()和addView())放到Window顯示畫面上的。
而LayoutInfalter就是用來生成View的 一個工具,XML佈局文件就是用來生成View的原料。
再來說說代碼中具體的執行流程
setContentView(R.layout.main)其實就是下面内容。
(註釋掉本行執行下面的代碼可以更直觀)
getWindow().setContentView(LayoutInflater.from(this).inflate(R.layout.main, null))
即運行程式後,Activity會調用PhoneWindow的setContentView()來生成一個Window,而此時的 setContentView就是那個最底層的View。然後通過LayoutInflater.infalte()方法加載佈局生成View對象並通過 addView()方法添加到Window上,(一層一層的疊加到Window上)
所以,Activity其實不是顯示視圖,View才是真正的顯示視圖。
註:一個Activity構造的時候只能初始化一個 Window(PhoneWindow),另外這個PhoneWindow有一個”ViewRoot”,這個”ViewRoot”是一個View活 ViewGroup,是最初始的視圖,然後通過addView方法將View一個個層疊到ViewRoot上,這些層疊的View最終放在Window 這個載體上面
Monday, February 13, 2012
Android Push Notification推播機制(7)-[實例分享]以Chrome to Phone為例
撰寫時間︰2012/02/13 13:35
更新時間︰2012/02/04 15:52
文章更新次數︰1
以下內容由Google I/O 2010 - Building push applications for Android 翻譯擷取
一、前言
在Android Market和Chrome的瀏覽器中,都可以看到一個叫Chrome to Phone的程式。
它的使用方式大概就是
當使用者正在瀏覽Google Map、或在看某個網站、甚至是某段YouTube影片,
可以在Chrome上,按下"Chrome to Phone"按鈕,
這些網址或資訊,
馬上被手機端的Chrome to Phone程式接收,
並在手機上繼續享受剛才在桌機上的使用體驗,
繼續看剛才看到一半的影片。
這就是C2DM機制被實作的例子之一。
二、文章開始
我們一起看看Chrome to Phone的操作方式︰
1.首先,使用者在Chrome瀏覽器按下了"Chrome to Phone"按鈕
2.訊息被傳遞到App Engine
3.App Engine將訊息通知到C2DM
4.C2DM會將訊息通知手機
使用者可以拉下他們的Notification bar去看訊息,當然我們也可以將Notification Bar的內容關閉,去做背景程式更新。
5.接著,手機就會開啟瀏覽器,去找出App Server裡的最後一則訊息為何。
下一篇︰
Android Push Notification推播機制(8)-問答篇
相關文章︰
Android Push Notification推播機制(1)-簡介篇Android Push Notification推播機制(2)-實務篇
Android Push Notification推播機制(3)-collapse key
Android Push Notification推播機制(4)-[狀況1]手機在飛航模式下
Android Push Notification推播機制(5)-Attenuation
Android Push Notification推播機制(6)-[狀況2]手機在待機下
官方技術公報 - Android Cloud To Device Messaging
標籤:
Android,
C2DM,
Chrome to Phone
Subscribe to:
Posts (Atom)


































