2014年12月9日 星期二

【Android】CheckBox的「帽子把戲」(如何自製「單選選項」)

這篇算是白癡教學。只要把基礎讀完的人應該都可以順利看完。

如果(讀者)要準備製作的是一張從頭到尾都手工打造、每個選項都有自己獨特的ID和對應資料的選單,這篇文章講的技巧沒什麼意義。

但這裡講的是「ListView這類用緩存記憶體大量複製出來的UI」,而且操作的是JSONArray這類的資料時,可能會需要的技巧(或說是心得會比較恰當)。

另外...如果不知道什麼是CheckBox的人也沒有必要繼續浪費時間看下去。



說明一下題目。

雖然Android就有預設「單選群組」給大家使用,但「萬一不夠用」,或「某些特性跟自己的需求相違背」時,該怎麼辦?

我沒辦法解釋或說明「那會是什麼樣的狀況」,那是碰到了就知道的狀況.......

一般來說CheckBox是用來製作「多選選項」、或「勾選式True/False」的元件。(有一種「滑動式切換True/False」的元件。)

如果每個CheckBox都是獨一無二的,則只要「輸出結果」時將全部的CheckBox都集合起來,用迴圈判讀它的Check狀態就好,但如果是用迴圈/緩存記憶體大量複製的,則無法這樣做,資料的操作必須要在按下「CheckBox」當下的瞬間進行修改。

再說明清楚一點:

假設功能要求這裡有個長整數(Integer)的矩陣A,每「格」矩陣對應內有二或多個以上CheckBox的CheckBox群組,CheckBox群組內的CheckBox依序對應「1」、「2」....以此類推的值,(CheckBox1被勾選時,某數值為1,CheckBox2被勾選時,某數值為2,.......某數值不可能同時又是1、又是2,所以CheckBox1和CheckBox2必須為單選項,)被選取的CheckBox則會把自己對應的數值寫入群組對應的矩陣格內。

如果矩陣A的長度永遠固定,那就表示CheckBox群組的數目也永遠固定,所以只要用固定格式的介面即可。最終要輸出答案時,只要把所有CheckBox群組用固定的順序判讀即可。

但如果矩陣A的長度會隨機變化,那狀況就很複雜了.........(先不提選項有多少種,光是題目數目不一定,這就是個「菜鳥的挑戰」了。)

以ListView為例,如果用全緩存的方式管理生成畫面,會發生「當這個選項滑出畫面外時,系統會把這個選項的介面圖形回收掉,」(不懂技術的人肯定完全不懂這是怎麼回事。意思就是說「這個欄位如果滑出畫面外,可能會被系統消除掉、以節省記憶體......,當它划回營幕時,系統再把它重新生出來。」)所以如果用「判讀介面上CheckBox選項狀態」的方式來決定答案值,會發生取不到CheckBox的狀況。

碰到這種案例時,就不能「讀取CheckBox的選項狀態」來決定數值,必須要在選項被圈選的當下就決定數值.......這就要使用介面「OnCheckedChangedListener」。

這只是狀況之一,隨機決定長度的資料難度完全不同。




首先,下面這算是個用匿名函數式去宣告的一個OnCheckedChangeListener。(怕有人沒用過,所以說明一下。CompoundButton buttonView是指設定了這個Listener的CheckBox,所以即使多個CheckBox設定同一個Listener,Listener也不會弄錯,──這是種設計函數的基本技巧。boolean isChecked則是前面這個buttonView的選取狀態,true表示被勾選,false則是無勾選、或勾選取消。)

OnCheckedChangeListener l1 = new OnCheckedChangeListener() {
  
  @Override
  public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
   // TODO Auto-generated method stub
   
  }
 };
請不要用這個方式製作接下來的OnCheckedChangeListener!

請正規的實作一個類別後再宣告。


結果大概如下!這個類別預設的是操作JSONObject。
class CheckListenr implements OnCheckedChangeListener{
  ...//省略建構子
  
  JSONObject js;
  CompoundButton lastView = null;

  @Override
  public void onCheckedChanged(CompoundButton buttonView,
    boolean isChecked) {
   // TODO Auto-generated method stub
                        //!!!!0#  將這個CheckBox對應的值取出
   int tag = (Integer) buttonView.getTag();
   if(!isChecked){//!!!!A#
    ....
    ....
                                //!!!!3#  
                                lastView = null;
   }
   else{
    if(lastView != null){
                                        //!!!!2#  將上一次選取的CheckBox取消勾選
     lastView.setOnCheckedChangeListener(null);
     lastView.setChecked(false);
     lastView.setOnCheckedChangeListener(this);
                                        //!!!!2#End
    }
                                //!!!!1#  將這次勾選的選項設為「上一次勾選的項目」
    lastView = buttonView;
    ...
                                ...
   }
  }
  
 }

假設眼前有三個CheckBox,分別是CheckA、CheckB、CheckC,則將它們對應的數值設為Tag,譬如CheckA為A、CheckB為B、CheckC為C,──可以看到「!!!!0#」下面對應的那行。(我這裡的做法是用isChecked為true時填入這個值,如果為false則填入統一的「X」。各位也可以選擇用設定兩個Tag,isChecked為true時取Tag1,false則取Tag2。這是很基本的變化技巧.......不是講給熟手聽得。)

「!!!!1#」、「!!!!2#」和「!!!!3#」就是將這些CheckBox做成單選項的關鍵。

「!!!!A#」的地方將整個邏輯切割開來,分為「勾選」和「取消勾選」......不難懂(吧)。

如果是第一次勾選這個群組(提醒:群組是指都設定了這個Listener、並且被指定要修改同一個數值的CheckBox)、或群組內的勾選被取消的狀況時,「!!!!2#」區段的程式碼並不會被執行,有CheckBox被勾選後、使用者又決定改勾選別的CheckBox時,「!!!!2#」就會被呼叫。

這地方是我稱這個技巧為「帽子把戲」的原因。注意看我將「上一次勾選的CheckBox取消勾選(設定為false)」前後,個做了一次「將Listener設為Null」和「將這個Listener設定給CheckBox」。

這有可能造成無窮迴圈,或至少會造成「無法將選項正確的反勾選」。因為如果CheckBox有設定CheckChangedListener時改變它的勾選狀態,則這個Listener會被呼叫執行,然後「取消勾選後」,也就是「!!!!A#」這個地方如果為false的區段會被執行,包含「!!!!3#」。

一般情況下這其實不會有問題!可是......我無法精確說明何時會是問題,因為「取消勾選(群組內的任何一個選項)」跟「被觸發設為false」兩者是不一樣的!(簡單來說,如果「取消勾選」時對應的動作如果在「被觸發設為false」時不可以被執行會出錯時,這就是我這裡再講的狀況。)

所以就假設「萬一出問題好了」,單純的將「上一次勾選的CheckBox」設為「false」會導致邏輯判斷出錯,解法非常簡單,再執行「被觸發設為false」前,將Listener解除就好。

2014年9月30日 星期二

【Java】for(;;)和for(:)的效能差異

【我測效能的方式一向是土法煉鋼。】



宣告一個長度在三千以上的矩陣,然後用兩種不同的for迴圈來讀取、操作內容,效能會差距多少?

注意!矩陣長度一定至少要在三千以上,一萬是比較保險的。(對矩陣個別元素的操作或運算內容也別太複雜。)

注意!還可以在for(:)的迴圈內容加個長整數,然後讓長整數一起跟著遞增,讓兩個迴圈的差異變得比較小。

答案是........




一千倍!

for(:)迴圈的速度是for(;;)迴圈的一千倍以上!




但了解技術內容的人都知道for(:)迴圈其實是將矩陣轉換成Iterator,然後用這個物件操作內容。

而且這個內容只是矩陣元素取值出來操作而已!它只是宣告一個變數,然後這個變數的值會是當下正在操作的矩陣元素的值。不論對這個變數做怎樣的操作,矩陣元素的值都不會有變化。

理論上來說,for(:)反而應該要比較慢才對,因為多了個轉換手續。

所以矩陣的長度要夠長,才會看得出差異。如果太短,這個轉換手續造成的效能差異會讓for(:)迴圈看起來比較慢。




如果有在for(:)迴圈內加長整數遞增,效能依然是一千倍!

顯然。遞增運算絕對不是效能差異的關鍵。(這很理所當然。)兩個迴圈的差異,完全在於Iterator的效能。

但差在哪裡呢?操作記憶體的效率?........傳統的for(;;)迴圈操作矩陣可能沒有位址紀錄器,要重覆的讀取迴圈內設置的長整數,還要判斷長整數的執行範圍........不過這是我隨便猜想啦!

如果在換個方法來測試這兩個迴圈的差異,例如也在傳統的for(;;)迴圈中加上一個區域變數,然後用這個區域變數取值後再對這個區域變數做運算操作,效能會差多少呢?




根本沒差!

或許關鍵根本不再Iterator,而是「變化矩陣元素內容」就是會這麼慢。






一般來說長度會到這個等級的矩陣......都是Stream吐出來的基本型別資料串。(自己寫的物件搞到三千多個?...超級資料庫?)

2014年9月17日 星期三

【Android/Java】ListView中使用ArrayAdapter搭配自己設計的物件

一般官方範例只會說到如何使用ArrayAdapter呈現String資料串。

感覺上大家會以為「ArrayAdapter只是把程式中寫在泛型資料格式指定的資料物件塞到XML Layout中的TextView上」。

但其實ArrayAdapter呼叫的是資料物件的「toString()」這個函數。


也就是說:設計師只要把「自己設定的物件」的「toString()」重新複寫就可以丟進ArrayAdapter了!

但剩下的資料呢?自己設定的物件中一定還有其他資料要顯示在ListView中,否則就不需要自定義物件了......

問這個問題的人去跪算盤!

複寫getView(int position, View convertView, ViewParent parent)這個函式就好啦!

講精準一點來說,ArrayAdapter在產生實體(new)的時候,會需要指令XML Layout跟這個Layout中的一個TextView的id。(如果不指定會預設為android.R.id.text1。)複寫的getView中如果寫了super.getView,就不用另外執行這個toString動作。(記得把super.getView傳出的View設為回傳值...記得getView這個函數要設回傳值。)




這個方法可以用在幾乎所有會用到Adapter的地方。

而且資料動態變更(執行notify函數)時,絕對會成功,因為用的是ArrayAdapter的方法。(我實作BaseAdapter產生的物件幾乎沒有成功過。)



話說......Java物件預設的函式到底還有那些?又有哪些功能?......

複寫他們會有些什麼副作用嗎?一般來說「toString()」是回傳物件的class資訊,就這樣複寫掉會有什麼影響呢?.......

有機會再補充啦。

2014年8月27日 星期三

【Java】interface內的static參數(型別為物件,而不是基本數值型別)

挺奇妙的...

在interface內的參數會被強制宣告為final型態,因此boolean/byte/short/int/char/long/float/double/String這些基本型態的參數一旦寫在裡面,就會被「鎖死」不能再進行修改。

但如果這個參數直接是個類別(A)的實作物件(A),雖然不能修改物件(A)、不能重新宣告或指定新的物件(A),但物件(A)內的參數是可以修改的。

最明顯的例子就是就是宣告ArrayList<T>。

實作這個介面的物件依舊可以動態的、彈性的增減ArrayList<T>的內容。



更奇妙的是可以將這個ArrayList宣告為static屬性。

在多個不同(幾乎沒有相似性)的類別中實作帶ArrayList的介面後,修改、增加ArrayList的內容,然後看看內容組成,(只要看數量就夠了,)會發現ArrayList確實只被產生一次。(它是個綁在介面下的static參數,而不是綁在類別中。)



Android中,Service和Activity的關係一直困擾著我的地方是:Activity要如何對Service內的參數做出修正?或引用Service的屬性和函數?

直接掛上ServiceConnection當然是必要動作,但如果是Activity下所屬的Dialog呢?或是設計者將功能獨立成一個物件,為了能夠在多個Activity中使用,所以傳入參數只使用的基本Context?(好爛的例子......但大家應該懂我真正想表達的,其實就是一個問題:「萬一真的不行,萬一真的有必要另闢途徑,萬一我懶到不想這麼做、專案已經複雜到新增一個傳遞參數大家就會因為要修改舊程式碼而爆炸........有沒有別的選擇?」)

將參數打包成一個物件,然後用interface(I)包裝起來再丟給Service去實作/擴充...這樣一來凡是實作/擴充這個interface(I)的類別都可以享有Service下的參數讀取、甚至修改的權限。



我馬上想到的第一個想法就是設計一個ArrayList<Runnable>,任何被丟進去的Runnable都會被Service執行......當然要做基本的數量控制!......這樣就有簡單的「任務控制器」了。.......這概念大家都稱為「任務控制器」吧?

2014年8月22日 星期五

Android的LayoutInflater真是博大精深啊!!!

public View inflate(int resource, ViewGroup root)

這個Inflater裡的函數非常有趣!

「ViewGroup root」帶Null或帶參數...對「int resource」解析的結果會完全不同!



剛剛同事花了半天才發現如果帶「ViewGroup root」,其實回傳的是這個「ViewGroup root」。

一般情況下這沒什麼,但如果「int resource」最外層的ViewGroup是個設計者自行設計的Layout類別,結果Inflater會忽略這個類別,然後只把「int resource」內的ChildView解析出來、加入「ViewGroup root」中。



會不會有點繞口?



詳細一點說明,如果設計者自行設計了一個LinearLayoutA,並且將它用在XML中,而且是包在檔案最外層。

如果帶了個「ViewGroup root」給Inflater,則Inflater會跳過這個LinearLayoutA的XML不去解析。



如果真是這樣,可以做個實驗.......

XML檔最外層是個FrameLayout,但「ViewGroup root」卻是個LinearLayout。

則用一個「FrameLayout frameLayout」參數去接收Inflater的回傳時,肯定會出錯............



等等做實驗!



(十五分鐘後........)

沒錯!實驗結果完全如此!

2014年8月18日 星期一

【Android】決定Dialog的大小和位置

今天的工作內容是「設計一個Dialog,它彈出的位置會隨著呼叫它的Button位置不同,而跟著不同,例如按鈕如果在整個螢幕偏右側,則視窗要在左側,反之則是在右側。」

問題最後算是初步解決了。以下是心得。



簡單來說,先獲得Dialog下的Window物件,然後再用Window物件產生WindowManager。

([Dialog物件].getWindow()可以獲得Window物件。[Window物件].getWindowManager可以獲得.......別問我怎麼獲得Dialog物件!拒絕回答!)

有了WindowManager,就可以進一步獲得這個Dialog的LayoutParams。

(這個LayoutParams可以說是Dialog的LayoutParams嗎?...這個Params的物件路徑明明白白是歸屬在WindowManager下,所以它到底是.........一點都不重要!)



一般的View元件在有LayoutParams後,決定View的大小位置,就不是什麼難事了!

但.........

不管設了什麼值給這個Params,一旦Dialog執行了show()之後,什麼都有可能亂掉!

例如我碰到的情況是「系統會將寬度強制設回-2」。

(系統會強制用預設的-1/-2覆蓋掉我們給予的值!──似乎不是所有人都會有這個困擾!為何只有某些人會碰到、或基本的程序會節外生枝,........抱歉!還在研究中。)

解決方法在於不使用LayoutParams設定位置與大小,而是使用WindowManager的setLayout(int wid, int height)。

(能不能用同樣的方法指定位置?......晚點試驗一下。)


2014年6月28日 星期六

Stream、Port、Ping、Socket

不知道有沒有人試過在瀏覽器的網址輸入欄中輸入「file:///」後面接上某檔案的路徑,然後按下「Enter」,看看會發生什麼事?........

很多檔案其實都可以用這種方式開啟。



因為這就是Stream與Port的概念。



在電腦中,中央對外溝通是藉由Port。

主機板要跟網路溝通?用Port。

主機板要跟列表機溝通?用Port。

主機板要跟硬碟要資料?用Port。

幾乎每個Port都有特定的工作,(或只能執行特定的工作,或是先佔先贏,程式大多會衝突,很多時候就是因為搶Port、或是對Port的設定沒有彈性。)

打入網址,瀏覽器會從Port編號:8080去跟外界網路溝通;如果打入檔案路徑,可能也是從Port編號:8080,但也可能不是,(我不想探究!Java耶!細問這麼多?)

重點是對電腦的中央來說,這都只是資料藉由Stream傳入或傳出而已,數據機跟網路怎麼處理從中央傳入的資料?中央才不管!(或許可以想像哪天真的把網址傳給硬碟,會發生怎樣有趣的事情呢?硬碟會變成變型金剛?)



但真正的問題是:Stream本身只管檢查自己是否可以傳出或傳入資料,能否建立Stream,這完全不是Stream自己可以控管的。

所以有File類別來檢查「檔案路徑是否存在」,所以有各種硬體管理類別來檢查「特定硬體是否存在」。

Socket就是用來檢查「指定的連線IP是否存在、是否可以進行連線」的類別。

網路連線的第一步就是「檢查IP是否正常、正確、可以連線?我在CMD模式下打入[Ping]+[ IP],會收到怎樣的結果?」,但Stream本身都不具備這些功能,這就是我們需要Socket的原因。

所以要清楚意識到Socket並不是用來進行資料傳輸溝通的主體!Stream才是!

雖然沒有Socket就沒有Stream,沒有Stream、Socket只是好看的工具,但這兩者並沒有絕對的依存關係。(.........講是這樣講,但我也不知道怎麼樣不靠Socket建立網路連線。)



學習使用Java Socket,大部分的新手都是死在這個認知上。

能夠清楚、易懂傳達這個認知的文章.......對不起!沒看過。(我也是別人明明白白講給我聽、甚至試範給我看後,才搞懂這一切。)