Search

Andoird - 啟動檔案瀏覽器選擇檔案

2015-12-31 11:16 AM

App 開發真的比 Web 還來得辛苦啊...

抱怨結束

在開發時常常需要有選擇圖片、檔案等等的功能

這時就需要啟動外部 App (啟動檔案瀏覽器) 來獲得選取檔案後的結果

例如點擊按鈕後啟動檔案管理員,選擇檔案後取得檔案路徑

再使用檔案路徑完成上傳檔案啦~ 圖片編輯等等的後續處理

搜尋 Intent file browser 可以得到很多資訊喔

接下來就開始介紹如何實現這項功能

4.4 版本以上由於 Android 修改了檔案的存取方式

請搭配這篇教學的方法使用 KitKat 以上版本從 URI 取得檔案實體路徑

程式碼範例
// 外部 App 回傳結果的類型判斷碼
private static final int FILE_SELECT_CODE = 0;

// 在按鈕上監聽點擊事件
settingsTypefaceButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        fileBrowserIntent();
    }
});

/**
 * 啟動外部 App 的檔案管理員
 */
private void fileBrowserIntent(){
    Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
    // 設定 MIME Type 但這裡是沒用的 加個心安而已
    intent.setType("application/font-sfnt");
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    try {
        startActivityForResult( Intent.createChooser(intent, "選擇字型"), FILE_SELECT_CODE );
    } catch (android.content.ActivityNotFoundException ex) {
        // 若使用者沒有安裝檔案瀏覽器的 App 則顯示提示訊息
        Toast.makeText(this, "沒有檔案瀏覽器 是沒辦法選擇字型的", Toast.LENGTH_SHORT).show();
    }
}

/**
 * 使用者選擇的結果將會作為參數傳遞至此
 * @param requestCode   發出要求的代碼, 用以確定是取得何種結果
 * @param resultCode    回傳結果的代碼, 用已確定是否請求成功
 * @param data          回傳的資料
 */
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
        // 檔案選擇代碼
        case FILE_SELECT_CODE:
            // 請求確認返回
            if (resultCode == RESULT_OK) {
                // 取得檔案路徑 Uri
                Uri uri = data.getData();
                // 取得路徑
                String path = null;
                try {
                    path = FileUtils.getPath(this, uri);
                } catch (URISyntaxException e) {
                    Toast.makeText(this, "檔案不對勁!", Toast.LENGTH_SHORT).show();
                    logger.error("Get file path failed.", e);
                    return;
                }

                // 檢查檔案類型
                String filename = FileUtils.typefaceChecker(path);

                if( filename.isEmpty() ){
                    Toast.makeText(this, "檔案不對勁!", Toast.LENGTH_SHORT).show();
                    return;
                }

                // Do something here...
            }
            break;
    }
    super.onActivityResult(requestCode, resultCode, data);
}

眼尖的人就注意到了,FileUtils 是啥? 為什麼我沒有!!!

為什麼網路教學老是缺東缺西的啊!!! 崩潰啊啊啊啊啊啊啊啊!!!

我說寧王先別急著發飆啊

因為那是需要自己建立的 Class

原始碼如下

import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import cmc.toolkit.FileKit;

import java.io.File;
import java.net.URISyntaxException;

public class FileUtils {

    /**
     * 由外部 Activity 回傳的資料取得檔案路徑
     * @param context   Activity
     * @param uri       Uri
     * @return
     *  檔案路徑
     * @throws URISyntaxException
     */
    public static String getPath(Context context, Uri uri) throws URISyntaxException {
        if ("content".equalsIgnoreCase(uri.getScheme())) {
            String[] projection = { "_data" };
            Cursor cursor = null;

            try {
                cursor = context.getContentResolver().query(uri, projection, null, null, null);
                int column_index = cursor.getColumnIndexOrThrow("_data");
                if (cursor.moveToFirst()) {
                    return cursor.getString(column_index);
                }
            } catch (Exception e) {
                // Do nothing
            }
            finally {
                if( cursor != null ) cursor.close();
            }
        }
        else if ("file".equalsIgnoreCase(uri.getScheme())) {
            return uri.getPath();
        }

        return null;
    }

    /**
     * 檢查取回的檔案副檔名
     * @param path  檔案路徑
     * @return
     *  若檔案類型正確則回傳檔案名稱, 反之則回傳空字串
     */
    public static String typefaceChecker(String path){
        if( path == null || path.isEmpty() ) return "";

        File file = new File(path);

        if( !file.exists() || !file.isFile() ) {
            return "";
        }

        String filename = file.getName();
        String ext      = FileKit.getFileExt(filename);

        if( !ext.equalsIgnoreCase(".ttf") ){
            return "";
        }

        return filename;
    }

}



// 一併附上取得副檔名的程式碼, FileKit 是我剛學 Java 時自己寫的小工具

public static String getFileExt(String fileName) {
    int dotIndex = fileName.lastIndexOf(".");
    return dotIndex == -1 ? "" : fileName.substring(dotIndex);
}
各項資料連結
Stackoverflow - Android file chooser

Java - PTT 文章代碼(AID)轉換為 URL

2015-12-26 5:38 PM

因為開發經驗第二集要花比較多時間撰寫

剛好最近有用到 AID(文章代碼) 轉換成 URL 網址的方法

寫了一個轉換工具 AidConverter

可以轉換 AID 為 URL 或是 檔案名稱

也可以將 URL 或 檔案名稱 轉換為 AID

就決定先寫這篇教學文章了

PTT 的原始碼是用 C 語言寫的 而且檔名很可怕!!!

但我只碰過 C++ 而且那也已經是N年前的事了(遙想當年...)

我懶得複習也懶得去看那幾段程式碼到底在做什麼

就用硬轉的方式把那幾段轉成 Java 語言

所以這段教學不會有什麼說明 打算放放原始碼就了事...

雖然測試回傳結果都是正確的 但只測過十幾篇所以記得自己加一些錯誤處理

如果有大師願意修正為更好的版本也請不要客氣囉~

AidConverter
import com.google.common.base.Splitter;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.ihad.ptt.model.bean.AidBean;
import org.apache.commons.lang3.StringUtils;

import java.util.List;

/**
 * 文章編號與檔案名稱轉換工具<br />
 * 將符合此格式的 [a-zA-Z-_]{8} 文章編號轉換為檔案名稱 或是 URL<br />
 * 範例:<br />
 * 1MVYyFDv -> M.1451110159.A.379 -> https://www.ptt.cc/bbs/Gossiping/M.1451110159.A.379.html<br /><br />
 * 或是將符合 [M|G].[unsigned_integer].A.[HEX{3}] 格式的檔案名稱轉換為文章編號<br />
 * 若為 URL 則將轉換為 {@link AidBean}, 其中包含看板名與文章編號<br />
 * 範例:<br />
 * https://www.ptt.cc/bbs/Gossiping/M.1451110159.A.379.html -> M.1451110159.A.379 -> 1MVYyFDv
 */
public class AidConverter {

    private static final String DOMAIN_URL = "https://www.ptt.cc/bbs/";
    private static final String FILE_EXT = ".html";

    private static String aidTable = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
    private static BiMap<Character, Long> table = tableInitializer();

    /**
     * 建立文章編號字元 Map, 方便取得對應數值
     * @return
     *  文章編號字元 Map
     */
    private static BiMap<Character, Long> tableInitializer(){
        BiMap<Character, Long> table = HashBiMap.create();

        long index = 0;
        int size = aidTable.length();

        for( int i = 0; i < size; i++ ){
            table.forcePut( aidTable.charAt(i), index );
            index ++;
        }

        return table;
    }

    /**
     * 將檔案名稱轉換為˙數字型態的文章序號
     * @param fn    檔案名稱
     * @return
     *  數字型態的文章序號, 若檔案名稱格式不符將回傳 0<br />
     *  轉換後的文章編號將符合 [M|G].[unsigned_integer].A.[HEX{3}]<br />
     *  範例: M.1451100858.A.71E
     */
    private static long fn2aidu(String fn){
        long aidu = 0;
        long type = 0;
        long v1 = 0;
        long v2 = 0;

        if( fn == null ) return 0;

        List<String> fnList = Splitter.on(".").omitEmptyStrings().trimResults().splitToList(fn);

        if( fnList.size() != 4 ) return 0;

        String typeString = fnList.get(0);
        String v1String = fnList.get(1);
        String v2String = fnList.get(3);

        if( !fnList.get(2).equals("A") ) return 0;
        if( !StringUtils.isNumeric(v1String) || v1String.length() != 10 ) return 0;

        switch (typeString){
            case "M":
                type = 0;
                break;
            case "G":
                type = 1;
                break;
            default:
                return 0;
        }

        v1 = Long.parseLong( v1String );
        v2 = Long.parseLong( v2String, 16 );

        aidu = ((type & 0xf) << 44) | ((v1 & 0xffffffffL) << 12) | (v2 & 0xfff);

        return aidu;
    }

    /**
     * 將數字型態的文章序號轉換為字串型態的文章編號
     * @param aidu  數字型態之文章序號
     * @return
     *  轉換後的文章編號將符合 [a-zA-Z-_]{8}<br />
     *  範例: 1MVWgwSU
     */
    private static String aidu2aidc(long aidu){
        int size = table.size();
        BiMap<Long, Character> inverseTable = table.inverse();

        StringBuffer stringBuffer = new StringBuffer();

        while( stringBuffer.length() < 8 ){
            long v = aidu % size;
            if( !inverseTable.containsKey( v ) ) return null;

            stringBuffer.insert( 0, inverseTable.get( v ) );
            aidu = aidu / size;
        }

        return stringBuffer.toString();
    }

    /**
     * 將文章編號轉換為數字型態的文章序號
     * @param aid   文章編號
     * @return
     *  數字型態的文章序號
     */
    private static long aidc2aidu(String aid){
        char[] aidChars = aid.toCharArray();
        long aidu = 0;

        for( char aidChar : aidChars ){
            if( aidChar == '@' ) break;
            if( !table.containsKey(aidChar) ) return 0;

            long v = table.get(aidChar);

            aidu = aidu << 6;
            aidu = aidu | (v & 0x3f);
        }

        return aidu;
    }

    /**
     * 將文章序號(數字型態)轉換為檔案名稱
     * @param aidu  文章序號(數字型態)
     * @return
     *  轉換後的檔案名稱, 格式將符合 [M|G].[unsigned_integer].A.[HEX{3}]<br />
     *  最後的16進位表示法若未滿3個字將以0從左邊開始補齊<br />
     *  範例: M.1451100858.A.71E
     */
    private static String aidu2fn(long aidu){
        long type = ((aidu >> 44) & 0xf);
        long v1 = ((aidu >> 12) & 0xffffffffL);
        long v2 = (aidu & 0xfff);

        // casting to unsigned
//        v1 = v1 & 0xffffffffL;
        String hex = Long.toHexString(v2).toUpperCase();

        return ((type == 0) ? "M" : "G") + "." + v1 + ".A." + StringUtils.leftPad(hex, 3, "0");
    }

    /**
     * 將文章編號轉換為檔案名稱
     * @param aid   文章編號
     * @return
     *  轉換後的檔案名稱, 格式將符合 [M|G].[unsigned_integer].A.[HEX{3}]<br />
     *  最後的16進位表示法若未滿3個字將以0從左邊開始補齊<br />
     *  範例: M.1451100858.A.71E
     */
    public static String aidToFileName(String aid){
        return aidu2fn( aidc2aidu(aid) );
    }

    /**
     * 將文章編號轉換為 WEB 版 URL
     * @param boardTitle    文章所屬看板名稱
     * @param aid           文章編號
     * @return
     *  WEB 版的完整 URL
     */
    public static String aidToUrl(String boardTitle, String aid){
        if( boardTitle.isEmpty() || aid.isEmpty() ) return "";

        return DOMAIN_URL + boardTitle + "/" + aidToFileName(aid) + FILE_EXT;
    }

    /**
     * 將檔案名稱(也就是 URL 的最後一段 不包含副檔名)轉換為文章編號
     * @param fileName  檔案名稱
     * @return
     *  轉換後的文章編號, 若檔案名稱格式不符則將回傳 null
     */
    public static String fileNameToAid(String fileName){
        return aidu2aidc( fn2aidu(fileName) );
    }

    /**
     * 將 URL 轉換為 AID 物件
     * @param url   PTT WEB 版的 URL
     * @return
     *  物件內包含 文章編號 與 看板名
     * @see AidBean
     */
    public static AidBean urlToAid(String url){
        List<String> urlList = Splitter.on("/").omitEmptyStrings().trimResults().splitToList(url);

        if( urlList.size() < 4 ) return null;

        String boardTitle   = urlList.get(3);
        String fileName     = urlList.get(4).replaceAll("\\.(html|htm)", "");
        String aid          = fileNameToAid( fileName );

        return AidBean.Builder.anAidBean()
                .withBoardTitle( boardTitle )
                .withAid( aid )
                .build();
    }

}

AidBean
public class AidBean{

    // 看板名稱
    private String boardTitle;

    // 文章編號
    private String aid;

    public String getBoardTitle() {
        return boardTitle;
    }

    public void setBoardTitle(String boardTitle) {
        this.boardTitle = boardTitle;
    }

    public String getAid() {
        return aid;
    }

    public void setAid(String aid) {
        this.aid = aid;
    }

    public static class Builder {
        private String boardTitle;
        private String aid;

        private Builder() {
        }

        public static Builder anAidBean() {
            return new Builder();
        }

        public Builder withBoardTitle(String boardTitle) {
            this.boardTitle = boardTitle;
            return this;
        }

        public Builder withAid(String aid) {
            this.aid = aid;
            return this;
        }

        public Builder but() {
            return anAidBean().withBoardTitle(boardTitle).withAid(aid);
        }

        public AidBean build() {
            AidBean aidBean = new AidBean();
            aidBean.setBoardTitle(boardTitle);
            aidBean.setAid(aid);
            return aidBean;
        }
    }
}
各項資料連結
Java - PTT 批踢踢實業坊 Client 軟體開發經驗(1) - 前置準備
PTT 原始碼

Java - PTT Client 軟體開發經驗(1) - 前置準備

2015-12-16 11:41 AM

警告! 開發 PTT Client 非常之危險!

無所不在的時間黑洞以及各種無法預料的狀況會將你的精神折磨至崩潰!!!

哈哈哈哈哈哈哈哈哈哈(就像這樣)

--

以下都是廢話加閒談,趕時間的可以按這裡跳到重點

可喜可賀!

經過了漫長的開發時間終於成功上架了

目前似乎也運氣很好的穩定運行了一段時間

有興趣的朋友可以自己在部落格內尋找 App 連結,我就不多打廣告了

自信滿滿,人稱程式小Bug王的我原本想說這種小東西大概一兩個月就可以完成了吧!

結果花了我快半年...

其中一個原因是第一次開發 Android App

另一個就是氣死人不償命的 VT100 終端機訊息傳遞機制,這個留待往後慢慢說明

而且網路上的資源可以說是少之又少

很多東西都是自己Google來東拼西湊才知道的

所以很多東西看看就好不一定是對的,自己找重點記(超不負責任)

尤其很多都是英文的資料,對於 PTT 這中文的論壇所需的技術來說實在是很不友善

於是我就想說寫這篇教學文章來好好闡述一下這血與淚的開發史

希望可以求得一些歷史定位(!?)

呃...是造福其他開發者啦

最主要的目的是告訴其他有興趣的開發者趕快放棄開發的計劃,因為實在太難做了

如果再讓我選一次我絕對不會選擇開發這個 App!!!

心理準備

首先我們來彈彈 PTT 的額頭,要很用力!(到底有多恨...)

是來談談 PTT 使用的 ANSI Escape Sequence 以及相關的知識

如果以為這只是單純的文字處理那就大錯特錯了!

當初天真爛漫的我其實就是這麼想的,殊不知背後龐大的利益鳩葛與情仇交纏...

PTT 是使用 VT100 這種類型的終端機定義來傳遞訊息

而使用 ANSI Escape Sequence 則可以將訊息格式化

諸如文字顏色、背景顏色、閃爍、文字出現位置以及清除畫面等等

所以你用 PC MAN 或任何終端機連上後可以看得見如此精美的畫面


但是你得到的資料其實是...

 [H [1;37;44m【主功能表】                      [33m批踢踢實業坊 [0;1;37;44m                                  
 [0;47m        [30m│ [2;6H                                                      [0;47m﹎ [30m﹏︷~一︸一~︷﹏﹎ [0;47m  
     [30;43m╱ [3;3H     [31;47m◢ [3;5H [41m      [30m﹏     ╱ [3;15H [47m           [1m▄ [3;23H    ▄ [3;25H    ▄ [3;27H    ▄ [3;29H    ▄ [3;31H    ▄ [3;33H    ▄ [3;35H    ▄ [3;37H    ▄ [3;39H    ▄ [3;41H                       [0;30;47m∕ [3;61H明天要出去玩,  ﹨  
 [0;43m   [41m  [30m#          ╱ [4;13H   [47m          [0;47m█ [4;22H [0;1;30m  [34m/  [0;1;37m.       [5;34m∕ [4;30H     [0;30m█ [4;32H [1;36m/   [5;34m/ [0;5;34m      [0;1;30m║ [4;40H    █ [4;42H [0;47m           ▁ [4;51H     [30m▂ [4;53H    ▁ [4;55H [0;47m     [30m﹨   雨還沒停嗎...    ∕ [4;79H  
     [0;43m◢ [5;3H [33;41m     [30m〞    ╱ [5;11H _   [47m          [0;47m█ [5;22H [0;1;30m   [5;36m/ [0;5;36m  [0;1;5;37m.      [0;1;36m∕ [5;31H     [30m█ [5;33H [0;30m  [0;1;37m.     ● [5;38H [30m人    █ [5;42H [0;47m          [30m◢ [5;49H [0;30m           [47m◣ [5;57H [0;47m  ﹊ [30m`"︸一~︷~一︸"`﹊ [0;47m  
     [43m◤ [6;3H [33;41m      [30m氵 [6;6H     ╱ [6;9H     ╱ [6;12H    ║ [6;14H  [47m          [0;47m█ [6;22H [0;1;30m      [34m∕ [6;25H     [36m′ [6;27H [0;36m       [30m▃ [6;31H     [1m█ [6;33H     [5;34m′ [6;35H [36m/      [0;1;37m▲ [6;39H      [30m█ [6;42H [0;47m          [30m◤ [6;49H     [43m◤ [6;51H    ◤ [6;53H [0;30m        [47m▏ [6;58H      ∞ [6;62H [0;47m         [36m▃ [6;68H    ▄ [6;70H    ▃ [6;72H [0;47m         
     [33;41m◤ [7;3H       [30m╱ [7;7H    ▏ [7;9H/      ║ [7;14H  [47m          [0;47m█ [7;22H [0;1;30m     [5;34m/ [0;1;36m/     [0;30m◤ [7;31H     [1m█ [7;33H [0;30m  [1;34m/     ′ [7;38H [0;34m  [1;5m/     [0;1;30m█ [7;42H [0;47m       [43m      [30m˙ [7;51H      [33m◤ [7;54H     [30m◥ [7;56H     [47m▎ [7;58H。           [36;46m◤ [7;68H [1;33;47m/|\          
 [0;30;41m`     ╱ [8;5H      [31m▋ [8;8H     [30m║ [8;10H      ║ [8;14H  [47m          [0;47m█ [8;22H [0;1;30m      [36m′ [8;25H [0;36m     [1;34m/     [30m█ [8;33H [0;30m   [1;36m/ [0;36m  [0;1;5;37m.      [0;1;30m█ [8;42H [0;47m       [43m          [33m◢ [8;55H [0;30m      [47m▏ [8;58H         [46m  [47m              
     [41m╱ [9;3H _      [31m▋ [9;8H     [30m║ [9;10H      ║ [9;14H  [47m          [0;47m█ [9;22H     [0;1;5;36m∕ [9;24H     [0;1;34m′ [9;26H [5;36m/ [0;5;36m  [0;1;37m.      [30m█ [9;33H     [34m∕ [9;35H     [5;36m′ [9;37H [0;5;36m      [0;1;36m∕ [9;40H     [30m█ [9;42H [0;47m         ◥ [9;49H [31;43mㄟ [33m      ▲ [9;55H     [30;47m◤ [9;57H [0;47m          [46m  [47m                [1;30mξ [9;80H 
 [0;41m      [30m╱ [10;4H    ║ [10;6H     [31m▋ [10;8H     [30m║ [10;10H      ║ [10;14H  [47m          [0;47m█ [10;22H     [0;1;30m▄ [10;24H    ▄ [10;26H    ▄ [10;28H    ▄ [10;30H    ▄ [10;32H    ▄ [10;34H    ▄ [10;36H    ▄ [10;38H    ▄ [10;40H    █ [10;42H [0;47m              [30;42m╭ [10;53H    ╮ [10;55H [47m             [33m◢ [10;65H     [36;43m▃ [10;67H    ▃ [10;69H [33m       [31m▁ [10;73H    ▁ [10;75H       [33m▲ [10;79H     [44m▲ [10;81H
 [30;41m/      ║ [11;6H     [31m▋ [11;8H     [30m║ [11;10H      ║ [11;14H      [1;47mψ [11;17Hrod24574575                            [0;30;42m│ [11;53H    │ [11;55H [47m           [33m◢ [11;63H [43m           [31m▕ [11;71H     [30;47m≡ [11;73H     [1m▏ [11;75H     [0;31;43m▏ [11;77H  [34m ̄ 
 [m [K
    ─ [13;3H    ─ [13;5H    ─ [13;7H    ─ [13;9H    ─ [13;11H    ─ [13;13H    ─ [13;15H    ─ [13;17H    ─ [13;19H 上方為使用者心情點播留言區,不代表本站立場     ─ [13;65H    ─ [13;67H    ─ [13;69H    ─ [13;71H    ─ [13;73H    ─ [13;75H    ─ [13;77H    ─ [13;79H [14;10H             ( [1;36mA [m)nnounce     【 精華公佈欄 】 [K [15;10H             ( [1;36mF [m)avorite     【 我 的 最愛 】 [K [16;10H             ( [1;36mC [m)lass        【 分組討論區 】 [K [17;10H             ( [1;36mM [m)ail         【 私人信件區 】 [K [18;10H             ( [1;36mT [m)alk         【 休閒聊天區 】 [K [19;10H             ( [1;36mU [m)ser [19;38H【 個人設定區 】 [K [20;10H             ( [1;36mX [m)yz          【 系統資訊區 】 [K [21;10H             ( [1;36mP [m)lay         【 娛樂與休閒 】 [K [22;10H             ( [1;36mN [m)amelist     【 編特別名單 】 [K [23;10H           > ( [1;36mG [m)oodbye         離開,再見    … [23;53H [K
 [34;46m[12/16 星期三 12:04] [1;33;45m [ 天秤時 ]    [24;36H [30;47m線上 [31m108017 [30m人, 我是 [31mnoartsarc [30m    [呼叫器] [31m打開  [m [23;21H

有沒有感覺到一種被欺騙了的感覺

...

...

...

那麼接下來我們就進入正式的介紹了(現在才開始!? 那前面打那麼多字幹嘛啦)

ANSI Escape Sequence

首先我希望各位將以下幾個網頁加入最愛或是直接儲存整個網頁

對於往後的開發來說會是非常重要的資源

控制字元對照表 - ASCII code caracters

ANSI 控制碼對照表 - ANSI Codes

CTRL 複合按鍵對照表 - Key Pressed with CTRL key down

第一個跟第三個連結暫時還用不上,我們先打開 ANSI Codes 的連結

然後看到下方表格的 color and text formatting 這欄


這邊主要是在說明所有可以用的文字格式設定控制碼

 /**
  * ESC 就是代表鍵盤上的 ESC 字元, 如何輸入留待後面再提
  * [#(;#) 中間的 # 就是表格內的數字組合, 可以放很多組格式設定
  * 但重複的設定會被後面的覆蓋掉
  * 例如我想要有紅色的字體, 綠色的背景再讓他閃爍就會使用以下控制碼
  * ESC[1;31;42m紅字綠背景ESC[m
  * 而如果重複使用了前景或背景設定
  * ESC[1;36;35;34;33;32;43;42m紅字綠背景ESC[m
  * 他則會以最後出現的設定來顯示(以上範例為32;42兩組設定)
  * 所以以上兩個範例顯示的結果會是相同的
  * 最後面的 ESC[m 則是還原用控制碼
  * 將所有文字格式設定還原
  * 而控制碼的結尾必須要以 m 告知處理器格式設定已經完成了
  * 這邊的觀念非常重要, 因為我們必須親自寫出一個上面提到的處理器
  * 也就是 ANSI Escape Sequence Parsing Engine
  */
 ESC[#(;#)m

再來就是另一個比較複雜的重頭戲了

請看到網頁中表格的 cursor controls 以及 erase functions


這邊主要是在處理文字的排版

也是上面那一大串文字的最主要原因

PTT 回傳的資料有很多種類型, 有時會給你完整的字串用空白補齊位子

有時會不給你空白, 而是使用 ESC[12;30H 的方式告訴你接下來的字是放在哪個位置

 /**
  * ESC 就是代表鍵盤上的 ESC 字元, 如何輸入留待後面再提
  * [#;#H 中間的 # 表示要將游標移動到哪個位置
  * 而這個游標位置又影響了接下來的字串會在哪個位置開始插入
  * 可以想像成一個 24 * 80 的矩陣會比較容易了解
  * 例如PTT回傳了要在 第 2 行的第 20 個的位置開始插入字串就會使用
  * ESC[2;20H隨便字串
  * 以此類推, 而若是使用
  * ESC[H
  * 則表示將游標位置歸零
  */
 ESC[#;#H

 // 清空所有資料
 ESC[2J
 // 清空目前所在行數的資料
 ESC[K

 // 將游標往後退一格 Backspace
 \b

 // 將游標回到同一列的第一個位置
 \r

 // 將游標往下移動一個 row
 \n

其他的部分因為 PTT 不會回傳那些控制碼所以可以暫時不用看

下一章就會開始講解 ANSI Escape Sequence Parsing Engine 該如何撰寫

用一長串英文有沒有感覺很專業?

其實就是將 PTT 回傳的資料一個蘿蔔一個坑的放入剛剛提到的 24 * 80 陣列的方法

算是整個 PTT Client 的第一個最重要的核心

也是當初撰寫時遇到的第一個難關

那麼就容我稍微拖個稿,請待下回分解!!!

各項資料連結
Java - PTT 文章代碼(AID)轉換為 URL
VT100
ASCII code caracters
ANSI Codes
Key Pressed with CTRL key down
PTT 完全操作手冊

PTT 行動版無廣告瀏覽器 - PiTT

2015-11-27 12:03 PM


全新無廣告行動版 PTT 瀏覽器登場囉! 用手機也可以輕鬆逛 PTT!

行動裝置上最懂你的 PTT 瀏覽器,心目中最理想的行動版 PTT 就該像這樣! I SEE YOU

立即前往 Google Play 下載!

讚不絕口的獨家特色

- 無廣告! 無廣告! 無廣告!
因為很重要所以說三次,
在此保證在功能 "做好做滿" 之前不會置入廣告!
置入廣告前的幾個版本更新會通知各位使用者,不更新就永遠不會有廣告!

- ANSI/MOBI 自由切換模式
看 ANSI ART 不需重新載入文章內容消耗行動網路流量,
讓你輕鬆看鄉民的藝術天分有多高!
ANSI 模式(其他 APP 稱為整頁模式) 可以等寬文字進行瀏覽
MOBI 模式則是以最佳化的行動版介面顯示

- 帳號任意門
其實全PTT只有我一個人,不信我換個帳號推一樣的話給你看。
不論是在最愛列表,文章列表還是文章內容,
你愛怎麼換帳號就怎麼換帳號。

- 推文進度報馬仔
打了一大堆推文卻不知到要推到何年何月?
獨家推文進度顯示告訴你還有幾則推文要繼續推
也告訴你還要等幾秒才可以繼續推
推到一半不想推還可以隨時中斷
直到你離開文章內容頁之前,推文暫存會為了你一直存在!
剩餘推文字數就不用說了,你懂的~

- 發文暫存功能
就算你關機了我也可以幫你回復你放棄編輯的文章!
文章編輯到一半突然想看八卦?
放心退出文章編輯模式去看吧!
我已經把你的文章內容都放在資料庫裡了!

- 發文 Redo/Undo 功能
複製貼上發現貼錯了...
刪除到不該刪的段落了...
乖,按一下 Redo/Undo 就通通幫你恢復原樣
比哆啦A夢的時光包巾還好用

- 文章瀏覽紀錄
忘記推了哪篇文章想看又找不到?
經典大長篇卻忘記標題是什麼?
不只記錄看板,你看過的文章通通幫你記錄下來
不用消耗行動網路流量也可以輕鬆追文

- 正/反向文章閱讀順序
文章太長想 End?
End 後發現是長篇經典文章又想從頭看?
沒問題! Home/End 切換讓你不管從上到下還是從下到上都滑得精彩!

接下來就來看看獨家功能的使用教學

文章編輯介面

Redo/Undo 可以記錄最多50筆編輯紀錄

只要按下下方工具列的 Redo/Undo 即可輕鬆回復編輯紀錄

讓你不再為行動版編輯而困擾



暫存文章回復功能可以在你放棄編輯時自動儲存文章內容

只要回到文章編輯頁面即可自動偵測是否有上次放棄編輯的文章內容

就算 App 曾經被關閉也可以回復

再也不怕辛苦打字的心血付諸東流



貼心防呆功能

按兩次返回鍵才會關閉文章編輯頁面

不須按其他確認離開按鈕

讓你輕鬆離開編輯介面又不怕按錯

就算真的按錯也有文章恢復功能

安心,放心又貼心

文章瀏覽

無邊框瀏覽模式,往下滑動自動隱藏占空間的工具列

往上滑動自動出現,讓你輕鬆瀏覽無負擔



ANSI/MOBI 模式自由切換(ANSI 模式在其他 App 稱為整頁模式)

看鄉民的藝術不再需要花兩倍流量

文章太長想直接 End 也沒問題

反向順向瀏覽隨你高興

看新推文只要按 End 重新整理即可看見鄉民最新回應

一鍵多工的按鍵設計讓你踏入鄉民的思考領域




推文分段預覽功能

懶得自己分段卻又不知道自動分段結果到底長怎樣?

按一下預覽分段讓你知道推文結果長怎樣



帳號任意門

讓你不管在看板頁,文章列表頁還是看文章推文

你愛怎麼換帳號就怎麼換帳號

其實全 PTT 只有我一個人,不信我換個帳號推一樣的話給你看

後記

開發這款 App 沒有想像中那麼容易

一來這是我的第一個 Android 專案

二來網路上的資源實在不夠多

光是 ANSI 控制碼還有資料傳輸規則就讓我崩潰了好幾次

原本只想花兩個月就完成,到現在做了快五個月

雖然中間有很多時間是在做介面的視覺設計,有時一調一整天就過去了...

於是為了不讓其他開發者遇到跟我一樣的困擾

在這幾天我會發布幾篇開發 PTT 行動版的教學文章

讓各位有興趣的開發者可以一起加入

讓 PTT 行動版有更好的瀏覽體驗

畢竟有競爭才會有進步

希望大家一起努力來提升台灣的軟體競爭力

Java - JSON 資料解析基本教學

2015-11-19 1:53 PM

JSON 算是目前主流的資料交換格式

跟 XML 比較起來 JSON的優點就是檔案小很多

但可讀性卻一點也不輸給 XML(這裡的可讀性是指"人")

以下就來簡單介紹 JSON 格式 以及如何解析資料

將 JSON 字串轉為物件 或是將物件轉換為 JSON 字串

在這裡會用到 Google 維護的 Gson Library

程式碼範例
// 陣列表達
[string1, string2, string3]

// 物件表達
{key1 : value1, key2 : value2, key3 : value3}

// 基本JSON表達式
{
  "id": 1,
  "name": "Hello JSON",
  "friends": [
    {
      "id": 2,
      "name": "Hello JAVA",
    },
    {
      "id": 3,
      "name": "Hello Andoird",
    }
  ],
  "certified": true
}

// 將 JSON 字串轉換為物件
return new GsonBuilder()
            // 若不須將日期自動轉換為 Date 物件則不須加入 setDateFormat 方法
            .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
            .create()
            // json 為 JSON字串, YourJsonObject.class 就是要轉換為的物件類型
            .fromJson(json, YourJsonObject.class);

// 若物件內的名稱與 JSON 內的名稱有些許差異時
// 可以用 @SerializedName("field_name") 指定
@SerializedName("issued_at")
private String fieldName;

// 若想轉換為 Map 格式 可使用以下方式 
Type t = new TypeToken<Map<String,Machine>>() {}.getType();
Map<String,Machine> map = (Map<String,Machine>) new Gson().fromJson("json", t);

// 若 JSON 內容最外層為陣列 而想轉換為 List 時可使用以下方式
Type t = new TypeToken<List<SearchResult>>() {}.getType();
List<SearchResult> list = (List<SearchResult>) new Gson().fromJson("json", t);

// 將物件轉為 JSON 格式字串
return new GsonBuilder()
            // 指定 Gson 不預先 escape html 的某些字元(如 "<", ">")
            .disableHtmlEscaping()
            .create()
            .toJson(this);
各項資料連結
Android - HttpURLConnection 基本教學 取得網頁資料(HTML, XML, JSON)
Android - 使用 HttpURLConnection 自動轉址失效的解決方式
google/gson - GitHub
JSON - MDN

Android - UncaughtException 很抱歉, app 已停止的錯誤處理

2015-11-18 6:00 PM

有時在實機測試會遇到 很抱歉,XXX已停止的錯誤並跳開應用程式

這種情況只靠 Log4J 是抓不到錯誤訊息的

因此 Debug 就非常困難 沒有錯誤訊息根本不知道哪裡有問題

那麼要如何取得相關的錯誤訊息呢?

由於是未處理的 Exception (稱作 UncaughtException)

因此會直接拋到 UI Thread 然後報錯 應用程式就停止了

要處理這類 UncaughtException 只要在 Application 加入以下程式碼即可

程式碼範例

public class App extends MultiDexApplication {

    // 建立 Logger 將以此輸出錯誤訊息
    private final Logger logger = Logger.getLogger(App.class);

    // 建立 UncaughtExceptionHandler UI Thread 的錯誤訊息會拋給這個物件處理
    private Thread.UncaughtExceptionHandler androidDefaultUEH;
    private Thread.UncaughtExceptionHandler handler = new Thread.UncaughtExceptionHandler() {
        public void uncaughtException(Thread thread, Throwable ex) {
            // print 錯誤訊息
            logger.error("Uncaught exception is:", ex);
            // 回報給預設錯誤處理
            androidDefaultUEH.uncaughtException(thread, ex);
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();

        // 初始化 Log4J
        ConfigureLog4J.configure();

        // 建立 Handler 並指定為預設處理 Handler
        androidDefaultUEH = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(handler);
    }
}

各項資料連結
Android Handling the Unexpected
Android Developers - Thread.UncaughtExceptionHandler

Android - ObjectAnimator 的 Easing 進階動畫效果

2015-11-17 3:37 PM

使用 ObjectAnimator 實現動畫效果 這篇文章裡

我們學會了如何使用 Android 原生的 ObjectAnimator 實現動畫效果

在這裡我們要用第三方的 Framework 實現一些較進階的動畫效果

若有做過介面相關特效的人員應該很熟悉一個叫做 Easing 的函數

但預設的 ObjectAnimator 能用的函數很少 而且名稱也不同

於是我們就須使用以下兩個開源的 Libraries

JakeWharton/NineOldAndroidsdaimajia/AnimationEasingFunctions

達成可指定 Easing 函數 以及同時運行多個動畫參數的效果

以下將以原本隱藏在畫面右方的選單元件往左滑動顯示的特效作為範例

程式碼範例
// 首先請先在專案加入這兩個 dependency
compile 'com.nineoldandroids:library:2.4.0'
compile 'com.daimajia.easing:library:1.0.1@aar'

// 在這裡使用的都是第三方的類別 名稱都跟原生 API 長得一模一樣 千萬不要搞錯
import com.daimajia.easing.Glider;
import com.daimajia.easing.Skill;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.AnimatorSet;
import com.nineoldandroids.animation.ObjectAnimator;

// 若之前狀態為 Gone 將無法取得寬度 因此在設為 Visible 後再以 AsyncTask 取得寬度設定動畫
mainMenuHolder.setVisibility(View.VISIBLE);

mainMenuHolder.post(new Runnable() {
    @Override
    public void run() {
     // 由於此範例是在 Fragment 內執行 因此需呼叫 getActivity 才能取得畫面尺寸
        Display display = getActivity().getWindowManager().getDefaultDisplay();
        Point size = new Point();
        display.getSize(size);
        int width = size.x;

        // 建立動畫物件 將選單由最右方向左滑出 直至顯示完整元件
        ObjectAnimator objectAnimator1 = ObjectAnimator.ofFloat(mainMenu, "x", width - mainMenu.getWidth());
        // 建立動畫物件 將選單外的畫面變暗的效果
        ObjectAnimator objectAnimator2 = ObjectAnimator.ofFloat(mainMenuMask, "alpha", 1f);

        // 建立動畫監聽事件 在動畫開始時設定為 VISIBLE
        // 這是由於在隱藏選單時同樣會加入此監聽事件 並在動畫結束時隱藏的關係
        objectAnimator1.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart (Animator animation){
                mainMenu.setVisibility(View.VISIBLE);
            }

            @Override
            public void onAnimationEnd (Animator animation){
            }

            @Override
            public void onAnimationCancel (Animator animation){
            }

            @Override
            public void onAnimationRepeat (Animator animation){
            }
        });

        // 將剛剛建立的動畫加入動畫陣列
        AnimatorSet set = new AnimatorSet();
        set.playTogether(
                // 在此可設定 Easing 的函數 有開發過互動式介面的應該很熟悉
                // 若不清楚有那些可以設定 差別又在哪裡 可以看下方的 Easing Functions 參考連結
                Glider.glide( Skill.QuadEaseOut, 300, objectAnimator1 ),
                Glider.glide( Skill.QuadEaseOut, 300, objectAnimator2 )
        );

        // 設定動畫時間間隔 請以動畫陣列內執行時間最長的為主
        set.setDuration(300);
        // 開始執行動畫
        set.start();
    }
});
各項資料連結
Android - 使用 ObjectAnimator 實現動畫效果
Easing Functions
GitHub - JakeWharton/NineOldAndroids
GitHub - daimajia/AnimationEasingFunctions
Android - ObjectAnimator

Android - 使用 ObjectAnimator 實現動畫效果

12:51 PM

之前在開發 Flash 及 Javascript 上的介面時

我習慣用 GreenSock 開發的 TweenLite Library

因為他的執行效率非常好 而且使用也很方便直覺

但可惜的是他沒有支援 Android

而在另一篇介紹的 TweenEngine 雖然使用上很類似

詳見 Android - 使用 Tweener 加入互動特效

可是常常會出現動畫效果出不來或是直接報錯給你看

於是就萌生了使用原生 Android 動畫 API 的想法

以下就來介紹 ObjectAnimator 的使用方法

這裡將以淡出 View 為範例 並在完全淡出後設定 Visible 為 GONE

程式碼範例
// 建立動畫操作物件
ObjectAnimator objectAnimator = ObjectAnimator
                    // 指定動畫效果
                    .ofFloat(articleAuthorDataHolder, "alpha", 1.0f, 0.0f)
                    // 指定動畫時間長度
                    .setDuration(300);

// 加入動畫監聽 
objectAnimator.addListener(new Animator.AnimatorListener() {
    @Override
    public void onAnimationStart(Animator animation) {

    }

    // 動畫執行完成後將 View 設定為 GONE
    @Override
    public void onAnimationEnd(Animator animation) {
        articleAuthorDataHolder.setVisibility(View.GONE);
    }

    @Override
    public void onAnimationCancel(Animator animation) {

    }

    @Override
    public void onAnimationRepeat(Animator animation) {

    }
});

// 開始動畫
objectAnimator.start();

各項資料連結
Android - 使用 Tweener 加入互動特效
Android - ObjectAnimator

Java - Thread 執行緒的 lock, wait, 與 notify

2015-11-09 6:17 PM

在使用多執行緒我們常常需要使用到 synchronize 確保值的一致性

但是有一些情況很容易造成資料錯亂

這時我們就必須將某一些 Thread lock 住

直到確認其他 Thread 的前置工作完成了 才執行其他 Thread 的後續工作

以下就來介紹執行緒的 lock 該如何設計

本範例將以同一個 class 內的多個執行緒做個簡單示範

程式碼範例
// 宣告一個 final 的變數 將作為參數傳遞至各執行緒
private final Object locker = new Object();

// 宣告另一個變數 作為判斷是否需要 lock 執行緒
// 至於在各執行緒間的傳遞方式有很多種 在這裡就不多作介紹
private boolean commandLock = false;

// Thread 1
while(true){
    if( !isConnected() ){
        lostConnection = true;
        break;
    }

    // 設定為 lock 狀態
    commandLock = true;

    // Do something

    // 設定為 unlock 狀態 並喚醒上一個等待中的Thread
    // 注意此處的 synchronized 是確保同時間只有一個執行緒操作 locker 物件
    commandLock = false;
    synchronized (locker) { locker.notify(); }
}

// Thread 2
public void reloadPage(){
    commandLocker();
    command( "Do reload" );
}

// Locker
private void commandLocker(){
    // 檢查 lock 狀態 若為 lock 狀態則進行 wait 動作
    // 直至 lock 狀態結束
    synchronized (locker) {
        while(commandLock){
            try {
                locker.wait();
            } catch (InterruptedException e) {
                logger.error("Thread sleep error in command locker section.");
            }
        }
    }
}

可能有人會看過在 while loop 中不使用 obj.wait(); 的作法

而是使用 Thread.sleep(millis); 的方式

但此方式容易消耗過多的效能進行不必要的 loop 執行

而且若 millis 沒有設定好 若同時有其他 Thread.sleep(); 的動作

很有因為時間差而可能造成 locker 永遠不會被解除的問題

但若單純使用空的 while 那效能的浪費是非常可怕的

因此還是使用 synchronized 加上 locker 物件的方式會較適當

各項資料連結
Java - Synchronized

Android - AlertDialog 的使用與建置

2015-11-08 11:06 AM

Android 的 AlertDialog 是一個非常好用的提示視窗元件

初始化非常的方便 且可應付大多數的情況

可以修改提示字元 確認按鈕字元 取消按鈕字元

設置 Listener 也相當的簡便

以下就來介紹該如何使用這個方便的控制元件

程式碼範例
// 建立元件
deleteAlert = new AlertDialog.Builder(this);
// 設置提示標題
deleteAlert.setTitle("貼心小提醒");
// 設置提示內容
deleteAlert.setMessage("確定要刪除嗎?");
// 設置確認按鈕顯示文字與 Listener
deleteAlert.setPositiveButton("確定", onClickPositiveDeleteArticle);
// 設置取消按鈕顯示文字與 Listener
deleteAlert.setNegativeButton("取消", onClickNegativeDeleteArticle);
// 設置使用者按下提示元件的其他地方與按下返回鍵導致取消的 Listener
deleteAlert.setOnCancelListener(onCancelDeleteArticle);

// 按下確認的 Listener
private DialogInterface.OnClickListener onClickPositiveDeleteArticle = new DialogInterface.OnClickListener(){
    @Override
    public void onClick(DialogInterface dialog, int which) {
        // TODO: Do something
    }
};

// 按下取消的 Listener
private DialogInterface.OnClickListener onClickNegativeDeleteArticle = new DialogInterface.OnClickListener(){
    @Override
    public void onClick(DialogInterface dialog, int which) {
        // TODO: Do something
    }
};

// 按下提示視窗之外的部分與按下返回鍵的 Listener
private DialogInterface.OnCancelListener onCancelDeleteArticle = new DialogInterface.OnCancelListener(){
    @Override
    public void onCancel(DialogInterface dialog) {
        // TODO: Do something
    }
};
各項資料連結
Android - AlertDialog

Android - 顯示 EditText 的虛擬鍵盤

2015-11-04 6:20 PM

若要在顯示某個 view 時

或是在按下其他按鈕時自動顯示指定 EditText 的虛擬鍵盤

可以使用以下方式

隱藏虛擬鍵盤使用方式詳見此篇介紹

Android - 隱藏 EditText 的虛擬鍵盤

程式碼範例
private EditText articleContentCommentEditor;

// 取得 focus
articleContentCommentEditor.requestFocus();
// 顯示虛擬鍵盤
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(articleContentCommentEditor, InputMethodManager.SHOW_IMPLICIT);
各項資料連結
Android - 隱藏 EditText 的虛擬鍵盤
Android Developer

Android - 使用 HttpURLConnection 自動轉址失效的解決方式

2015-10-31 11:30 AM

基本上 HttpURLConnection 是會自動處理轉址動作的

但只有一個情況例外

就是 http 協定轉換為 https 協定時

此時由於安全性改變 預設會要求開發者提示使用者

但若要使用程式讀取網頁內容時將會導致無法取得正確的網頁內容

這時我們可以使用以下方式達成協定轉換時的自動轉址

只要加入一段轉址判斷即可

程式碼範例
int statusCode = connection.getResponseCode();

// https redirect
if (statusCode == HttpURLConnection.HTTP_MOVED_TEMP || statusCode == HttpURLConnection.HTTP_MOVED_PERM
        || statusCode == HttpURLConnection.HTTP_SEE_OTHER){
    String newUrl = connection.getHeaderField("Location");
    String cookies = connection.getHeaderField("Set-Cookie");
    connection = (HttpURLConnection) new URL(newUrl).openConnection();
    connection.setRequestProperty("Cookie", cookies);
}
各項資料連結
Android - HttpURLConnection 實作 HttpRequest 取得網頁內容(HTML, XML, JSON)
Android HttpURLConnection

Android - HttpURLConnection 基本教學 取得網頁資料(HTML, XML, JSON)

11:27 AM

若要在 Android 內實作 HttpRequest 方法有很多種

但其實最簡單的方式就是使用內建的 HttpURLConnection 來實作

不需要引入其他 Library, 也沒有甚麼前置作業

以下就使用 HttpURLConnection 示範如何實作 HttpGet 要求

並取得網頁內容

不論是 HTML, XML, JSON 方法都相同 不同的地方在於取得內容後的解析方式

此處將止於取得內容字串

若遇到https轉址失效可查看這篇教學

使用 HttpURLConnection 自動轉址失效的解決方式

若要處理 JSON 格式字串可以查看這篇教學

JSON 資料解析基本教學

程式碼範例
String urlString = "https://www.google.com.tw/";
HttpURLConnection connection = null;

try {
    // 初始化 URL
    URL url = new URL(urlString);
    // 取得連線物件
    connection = (HttpURLConnection) url.openConnection();
    // 設定 request timeout
    connection.setReadTimeout(1500);
    connection.setConnectTimeout(1500);
    // 模擬 Chrome 的 user agent, 因為手機的網頁內容較不完整
    connection.addRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36");
    // 設定開啟自動轉址
    connection.setInstanceFollowRedirects(true);

    // 若要求回傳 200 OK 表示成功取得網頁內容
    if( connection.getResponseCode() == HttpsURLConnection.HTTP_OK ){
        // 讀取網頁內容
        InputStream     inputStream     = connection.getInputStream();
        BufferedReader  bufferedReader  = new BufferedReader( new InputStreamReader(inputStream) );

        String tempStr;
        StringBuffer stringBuffer = new StringBuffer();

        while( ( tempStr = bufferedReader.readLine() ) != null ) {
            stringBuffer.append( tempStr );
        }

        bufferedReader.close();
        inputStream.close();

        // 取得網頁內容類型
        String  mime = connection.getContentType();
        boolean isMediaStream = false;

        // 判斷是否為串流檔案
        if( mime.indexOf("audio") == 0 ||  mime.indexOf("video") == 0 ){
            isMediaStream = true;
        }

        // 網頁內容字串
        String responseString = stringBuffer.toString();
    }
} catch (IOException e) {
    e.printStackTrace();
}
finally {
    // 中斷連線
    if( connection != null ) {
        connection.disconnect();
    }
}
各項資料連結
Android - 使用 HttpURLConnection 自動轉址失效的解決方式
Java - JSON 資料解析基本教學
Android HttpURLConnection

Android - 取得手機 wifi 連線狀態

2015-10-28 4:48 PM

在 Android 中取得 wifi 連線狀態

可使用以下方式

記得先到 AndroidManifest.xml 開啟權限

程式碼範例
// 開啟讀取網路連線狀態權限
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

ConnectivityManager connManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mWifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);

if ( mWifi.isConnected() ) {
 // TODO: Do something here
}
各項資料連結
Android Developer

Android - 使用 Picasso 解決圖片載入造成 OutOfMemoryError 的問題

2015-10-27 10:00 PM

OOM - OutOfMemoryError

一件非常討厭的錯誤

而且是在轉戰 Android 時常常會遇見的錯誤

以前寫 Web 可以說是要用多少記憶體就用多少記憶體...

其實也不是啦 只是 Web 用的記憶體相對於使用者的記憶體大小來說實在是小 case

就算載入一個單眼拍出來的 20M 高解析照片也不是問題

但在 Android 上就麻煩了

好在有團隊開發出一個免費的 Library - Picasso

不論是圖片載入問題, 快取問題還是 AsyncTask 他都幫你處理的穩當當

好了,為了 SEO 而寫的廢話就到這裡

開始介紹如何使用 Picasso 這個套件來簡單避免 OOM 的錯誤

程式碼範例
// 若要了解圖片是從網路, 手機本地端或是從記憶體內取出
// 可開啟圖片標示 分別會以 紅色, 藍色, 綠色表示
Picasso.with(mContext).setIndicatorsEnabled(true);

// 初始化 Picasso
Picasso.with(mContext)
        // 傳入檔案路徑 可以是網址或本地檔案
        .load( "URL or PATH" )
        // 調整大小至與容器相等 亦可使用 resize,  但 resize 似乎只有調整大小 未實作 inSampleSize
        .fit()
        // 指定圖片尚未載入時的預設圖片
        .placeholder(R.drawable.v_image_placeholder)
        // 指定圖片載入失敗時的預設圖片
        .error(R.drawable.v_image_error)
        // 指定圖片置於容器中央 不裁切, 另有 centerCop
        .centerInside()
        // 不將圖片儲存於記憶體內
        .memoryPolicy(MemoryPolicy.NO_STORE)
        // 不檢查記憶體內是否存在圖片快取
        .memoryPolicy(MemoryPolicy.NO_CACHE)
        // 不儲存任何快取
        .networkPolicy(NetworkPolicy.NO_CACHE)
        // 指定圖片顯示的容器
        .into(holder.imageView);

各項資料連結
Picasso

Java - 避免 NullPointerException 的設計模式

2015-10-21 6:08 PM

Java 的 null 處理算是一位程式設計師由新手進入進階的最好證明

眾所皆知 若 Java 出現 NullPointerException 往往很難找到問題發生所在

但為了方便 又常常使用 null 作為回傳值及初始值

若忘記判斷是否為 null 就很容易出現 NullPointerException

這時光看 Log 檔完全無法得知是哪個變數出錯

為了避免這種狀況發生 我們將搭配使用 Google Guava Library 裡面的 Optional 物件

一來可以告知程式撰寫人員這個參數有可能是 null

二來若是忘記判斷 在取得 null 時將會明顯地告知是哪一行哪一個變數出錯

程式碼範例
// 此處為 Optional 的初始化方式 以取得資料庫資料為範例
public Optional<UserPreference> findByName(String owner, String name) throws SQLException {

    QueryBuilder<UserPreference, Integer> queryBuilder = queryBuilder();

    queryBuilder.limit(1L)
            .where().eq("owner", owner)
            .and().eq("name", name);

    List<UserPreference> list = query( queryBuilder.prepare() );

    // 若沒有資料則以 absent 表示為 null
    if( list.isEmpty() ) return Optional.absent();

    // 若有資料則使用 Optional.of( object ); 建立物件
    return Optional.of( list.get(0) );
}


// 此處以資料庫新增資料作範例
public UserPreference insertWithCheck(String owner, String name, String value) throws SQLException, UnsupportedEncodingException {

    // 首先取得資料
    Optional<UserPreference> userPreferenceOptional = getUserPreference(owner, name);

    // 檢查資料是否為 null 若否則更新資料
    if( userPreferenceOptional.isPresent() ){
        return userPreferenceDao.updateValue( userPreferenceOptional.get(), name, value, encrypted);
    }

    // 資料為 null 則直接新增
    return userPreferenceDao.insert(owner, name, value, encrypted);
}

// 到資料庫撈出單筆資料
public Optional<UserPreference> getUserPreference(String owner, String name) throws SQLException {
    return findByName(owner, name);
}

// 此方法可以在 null 時報錯
userPreferenceOptional.get();

// 此方法可以在 null 時賦予預設值
userPreferenceOptional.or( defaultValue );

// 此方法可以在 null 時直接回傳 null
userPreferenceOptional.orNull();
各項資料連結
GitHub - Google Guava
MavenRepository - Google Guava

Amazon Cloud Drive - 使用 odrive 實現同步電腦內的檔案 不再只有備份功能

2015-10-20 12:37 PM

相信大家都知道目前雲端硬碟最便宜的選項就是 Amazon Cloud Drive




每年只要 59.99 美金 相當於每個月只要 160 元(台幣)左右

就可以享用完全無容量限制的雲端硬碟

但缺點就是... 官方未提供像 Dropbox 與 Google Drive 的檔案同步功能

等於你只能手動上傳檔案

這個缺點真的是讓人非常想打退堂鼓

因為我們怎麼可能記得哪些檔案我們傳上去了? 哪些沒有呢?

感覺起來就像花錢租用一個可以連到你電腦的 Domain Name 而已

不過網頁版介面還算不錯




以這樣的功能性來看

其實一年買一顆硬碟還比較划算...

但好消息是 Amazon 有提供 Amazon Cloud Drive 的 API

更好的消息是已經有團隊開發出具同步功能的 PC 用同步軟體了

這就是今天要介紹的 odrive


odrive




他使用的方式是跟 Dropbox 相同的資料夾管理介面

相信大家應該非常容易上手




他可以整合你所有的雲端硬碟

包括 Dropbox, Google Drive, OneDrive 等等知名雲端硬碟服務

連 FTP 都有!

當然也包括今天要介紹的重點 Amazon Cloud Drive




無限容量加上同步服務! 這不就是夢寐以求的雲端硬碟嗎?

而且現在他是免費的應用程式!!!

不過之後可能會加入付費會員專屬的服務 目前是完全免費的




之前一直沒有使用 Amazon Cloud Drive 免費3個月的試用方案剛好可以來試一試


註冊與設定

首先我們必須要連到 odrive 的網頁並使用以下任一個帳戶登入




登入後會跟你要帳戶的存取權 這樣他們才可以管理你的檔案

然後就會自動開始下載 Client 端的應用程式

安裝完成後可以在工具列看到執行中的小圖示

而預設同步資料夾路徑會在 C 槽的 User 資料夾內

可以在小圖示上點擊右鍵開啟選單將同步資料夾指定到你想存放的路徑




然後就會看見你的資料夾圖示多了一個 odrive 的符號

這部分是不是跟目前主流的雲端硬碟服務很像呢?




點進去後會發現有預設的三個資料夾 Dropbox, Google Drive 以及 Facebook

並在資料夾後面會有一個 .cloudfx 的後綴詞 表示這個資料夾尚未同步資料至本地端

點擊後就會自動開始同步將檔案下載下來 同時.cloudfx 的後綴詞也會消失

這項特性會在稍後多做介紹

而若要加入新的雲端硬碟同步服務 可以在工具列的小圖示選單點擊 Add Link

點擊加入想連結的服務之後會開啟網頁要求帳號存取權限

成功後就會在 odrive 同步資料夾內增加一個專屬那一個服務與帳號的資料夾

沒錯! 他也支援多重帳號同步功能!






Progressive Sync

特殊的地方是它並不會自動開始下載你雲端硬碟上的所有檔案到電腦內

因為若像 Amazon 這樣無限容量的服務

如果真的全部下載下來硬碟應該會爆炸吧...

所以他們使用了一種叫做 Progressive Sync 的技術

只有在你打開那個資料夾或檔案的時候才會開始同步下載資料到你的電腦裡面

若是小於你設定的檔案大小限制 就會直接被下載下來

大於檔案大小限制的則會以 .cloudfx 的副檔名存在

但也因為如此 第一次開啟資料夾時或是其他同步正在進行中的時候

開啟資料夾或檔案的速度會比較慢

但另外的優點是你不需要指定你想同步的資料夾

只要不打開他就不會開始同步

不過要是不小心點開了具有龐大檔案的資料夾該怎麼辦?

這也不用擔心 就像剛剛提到的下載設定

檔案大小限制有 永不下載, 10 MB, 100 MB, 500 MB 以及 無限制 這五種

預設他們只會下載 500 MB 以下的檔案

或是直接在資料夾點擊右鍵選擇 Unsync 就會停止同步

但同時資料夾內的所有資料都會被清空! 這點要非常注意!


傳輸速度

接下來就是各位非常擔心地同步速度了

這點目前看起來還不錯 平均都在 3.5 MB 左右 有時還可以飆到最高速





不過可能是因為我目前還在同步大資料夾到雲端的關係(250 GB)

在操作其他資料夾時反應會慢很多

例如我在十分鐘前新增在 A 資料夾的檔案目前還沒同步上去

但新增的 B 資料夾已經同步完成了 包括裡面的檔案

這點可能在同步差不多都完成後會有所改善

而在刪除方面不管在哪裡將檔案刪除都會同步將兩邊的檔案都刪除

這點也要很注意


結論

因為是免費服務加上背後似乎沒有大廠商撐腰

所以穩定度還待考驗 若檔案被誤刪可是非常嚴重的

我目前的作法還是將 Amazon Cloud Drive 當作額外備份用

也就是複製一份到 Amazon Cloud Drive 的同步資料夾讓他自動同步

而在其他硬碟還是有原始的檔案存在

畢竟在 Amazon 還沒推出官方版同步軟體前還是不要拿檔案開玩笑

所以若要拿 Amazon Cloud Drive 當作雲端硬碟主力的可能還是要三思而後行

目前沒有官方同步功能的 Amazon Cloud Drive 競爭力還是不太夠


各項資料連結

Android - RecyclerView 與 SwipeRefreshLayout 以及捲動加載下一頁資料的完整建置

2015-10-17 11:10 AM

RecyclerView 是 Android 釋出用以取代 ListView 的元件

他比 ListView 效能更好 資源運用更靈活

不過在初始化方面是稍微複雜了點

現在我們就來一步步介紹該怎麼加入 RecyclerView 並使用 SwipeRefreshLayout

實作向下滑動更新的功能

首先我們先來看看 xml layout 的部分

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <RelativeLayout
            android:id="@+id/favItemHolder"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <android.support.v4.widget.SwipeRefreshLayout
                android:id="@+id/pullToRefreshCateRecycler"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:visibility="visible">

            <android.support.v7.widget.RecyclerView
                    xmlns:android="http://schemas.android.com/apk/res/android"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:id="@+id/cateRecyclerView"
                    android:layout_centerVertical="true"
                    android:layout_centerHorizontal="true"/>

        </android.support.v4.widget.SwipeRefreshLayout>

    </RelativeLayout>
</LinearLayout>

這時你會看到右方預覽介面是空白的



這是正常的結果所以不用擔心

接下來我們要新增 RecyclerView 裡面的 Item Layout

並將內容置中

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             android:orientation="vertical"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:background="@drawable/fav_item_background_shape"
             android:layout_marginBottom="5px"
             android:layout_marginTop="10px"
             android:layout_marginLeft="5px"
             android:layout_marginRight="5px">

    <LinearLayout
            android:paddingLeft="5dp"
            android:paddingTop="3dp"
            android:paddingRight="5dp"
            android:paddingBottom="5dp"
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

        <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content">

            <TextView
                    android:id="@+id/cateItemTitle"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textStyle="bold"
                    android:textColor="#E7E7E7"
                    android:layout_centerVertical="true"
                    android:layout_centerHorizontal="true"
                    android:text="Title"/>

        </RelativeLayout>

        <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="10px">

            <TextView
                    android:id="@+id/cateItemDesc"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Description of this item."
                    android:textColor="#E1E1E1"
                    android:layout_centerVertical="true"
                    android:layout_centerHorizontal="true"/>

        </RelativeLayout>

    </LinearLayout>

</FrameLayout>

看起來就會像這樣



那麼介面完成了 現在就要開始程式碼的部分

RecyclerView 需要一個 Adapter 配合

主要用途是 Item 的操作, 以及 layout 的載入

我們先來看 Adapter 該如何建立

import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.ihad.ptt.model.bean.CategoryBean;

import java.util.Map;
import java.util.Set;

public class CateRecyclerAdapter extends RecyclerView.Adapter<CateRecyclerAdapter.ItemHolder> {

    // 物件儲存 Map
    private Map<Integer, CategoryBean> categoryBeans;
    // 點擊處理
    private ItemHolder.IClickHandler clickHandler;

    public CateRecyclerAdapter(Map<Integer, CategoryBean> categoryBeans, ItemHolder.IClickHandler handler) {
        this.categoryBeans = categoryBeans;
        this.clickHandler = handler;
    }

    // 載入 Layout
    @Override
    public ItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // create a new view
        View itemLayoutView = LayoutInflater.from(parent.getContext()).inflate(R.layout.cate_item, parent, false);

        // create ViewHolder
        ItemHolder viewHolder = new ItemHolder(itemLayoutView, clickHandler);
        return viewHolder;
    }

    // 更新 View
    @Override
    public void onBindViewHolder(ItemHolder holder, int position) {

        CategoryBean categoryBean = categoryBeans.get( position + 1 );

        if( categoryBean == null ) return;

        holder.cateItemTitle.setText( categoryBean.getName() );
        holder.cateItemDesc.setText( categoryBean.getDesc() );

    }

    // 取得已加入 Item 數量
    @Override
    public int getItemCount() {
        return categoryBeans.size();
    }

    // 取得 Item
    public CategoryBean getItem(int key){
        return categoryBeans.get( key );
    }

    // 已加入 Item 是否重複判斷
    public boolean isDuplicate( Map<Integer, CategoryBean> categoryBeanMap ){

        if( !categoryBeans.isEmpty() ){
            Set<Integer> keySet = categoryBeanMap.keySet();
            int foundCount = 0;

            for (Integer key : keySet) {
                if ( categoryBeans.containsKey(key) ) foundCount++;
            }

            if ( foundCount == categoryBeanMap.size() ) return true;
        }

        return false;
    }

    // 加入 Item
    public void addItem(CategoryBean categoryBean){
        categoryBeans.put( categoryBean.getSerialNum(), categoryBean );
    }

    // 清除所有 Item
    public void clear(){
        this.notifyItemRangeRemoved( 0, categoryBeans.size() );
        categoryBeans.clear();
    }

    // Item
    public static class ItemHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        public TextView cateItemTitle;
        public TextView cateItemDesc;
        public IClickHandler clickHandler;

        public ItemHolder(View view, IClickHandler handler) {
            super(view);
            view.setOnClickListener(this);
            cateItemTitle = (TextView) view.findViewById(R.id.cateItemTitle);
            cateItemDesc = (TextView) view.findViewById(R.id.cateItemDesc);

            clickHandler = handler;
        }

        @Override
        public void onClick(View view) {
            int position = getLayoutPosition();

            clickHandler.onClick(view, position);
        }

        public interface IClickHandler {
            void onClick(View caller, int position);
        }
    }
}

這個部分應該沒什麼問題

接下來我們來看該如何連結所有的 Layout 及 Adapter 並將 Item 讀入 RecyclerView

import android.os.Bundle;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.Toast;
import cmc.toolkit.CTrace;
import com.ihad.ptt.model.bean.CategoryBean;
import com.ihad.ptt.model.bean.FavoriteBoardBean;
import roboguice.fragment.RoboFragment;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

public class CategoryFragment extends RoboFragment {

    public static final String CATE_PAGE = "CATE_PAGE";
    private int mPage;

    // 滑動更新用
    private SwipeRefreshLayout cateSwipeRefreshLayout;
    private SwipeRefreshLayout cateSubSwipeRefreshLayout;

    // RecyclerView 的呈現方式
    // 有線性(LinearLayoutManager), 塊狀(GridLayoutManager) 以及不規則塊狀(StaggeredGridLayoutManager) 三種
    private LinearLayoutManager mCateLinearLayoutManager;
    // Adapter
    private CateRecyclerAdapter mCateAdapter;

    // 是否正在讀取
    private boolean isCateLoading   = false;
    // 是否已讀取完整資料
    private boolean noCateMoreData  = false;

    // 若是在 Activity 中建立的不需要管這個 本例為使用 Fragment 加載
    public static CategoryFragment newInstance(int page) {
        Bundle args = new Bundle();
        args.putInt(CATE_PAGE, page);
        CategoryFragment fragment = new CategoryFragment();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPage = getArguments().getInt(CATE_PAGE);

        // 分類目錄
        mCateAdapter = new CateRecyclerAdapter( new LinkedHashMap<Integer, CategoryBean>(), new CateRecyclerAdapter.ItemHolder.IClickHandler() {
            @Override
            public void onClick(View view, int position) {
                // 使用 Position 取得物件 這裡 +1 是因為我使用 1 based 方式建立的序號
                CategoryBean categoryBean = mCateAdapter.getItem( position + 1 );

                if( categoryBean == null ) {
                    Toast.makeText(view.getContext(), "Find nothing", Toast.LENGTH_SHORT).show();
                }
                else{
                    // 點擊後的處理 可自行設定 若是在 Activity 新增則不須再取得 Activity
                    MainActivity mainActivity = (MainActivity) getActivity();
                    mainActivity.goCateSubList(categoryBean);
                }
            }
        });
    }

    // 取得 Layout 並初始化各項元件
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        // 這個是放 RecyclerView 的 Layout
        View view = inflater.inflate(R.layout.cate_frag_page, container, false);

        cateSwipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.pullToRefreshCateRecycler);

        // 設定滑動更新 loader 的顏色 預設為黑色
        cateSwipeRefreshLayout.setColorSchemeColors(0xFF4FC3F7, 0xFF8558E0, 0xFFFF326F, 0xFFF9F765);

        // 滑動更新監聽設定
        cateSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                // 清除所有 Item
                mCateAdapter.clear();

                // 呼叫重新整理 function
                MainActivity mainActivity = (MainActivity) getActivity();
                mainActivity.reloadCatePage();

                // 設定為尚有資料
                noCateMoreData = false;
            }
        });

        // new 一個 LinearLayoutManager 結果將呈現為縱向捲動清單
        mCateLinearLayoutManager = new LinearLayoutManager(getActivity());
        mCateLinearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);

        // 取得 RecyclerView
        RecyclerView cateRecyclerView = (RecyclerView) view.findViewById(R.id.cateRecyclerView);

        // 底下一併介紹其他呈現方式的設定方法
        // List layout
        cateRecyclerView.setLayoutManager(mCateLinearLayoutManager);
        // Grid layout
        //cateRecyclerView.setLayoutManager(new GridLayoutManager(this, 2));
        // StaggeredGrid layout
        //cateRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(2, OrientationHelper.VERTICAL));

        // 將 Adapter 指定給 RecyclerView
        cateRecyclerView.setAdapter(mCateAdapter);
        // 加入動畫 若指定 null 就不會有動畫
        cateRecyclerView.setItemAnimator(new DefaultItemAnimator());

        // 向下捲動自動加載新資料
        cateRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {

                int visibleItemCount = mCateLinearLayoutManager.getChildCount();
                int totalItemCount = mCateLinearLayoutManager.getItemCount();
                int pastVisiblesItems = mCateLinearLayoutManager.findFirstVisibleItemPosition();

                // 視情況修改在第幾項 Item 顯示時自動讀取下一頁
                // 本範例為單次讀取 20 筆資料, 捲動至第 10 筆時自動讀取下一頁
                if( totalItemCount > 10 ){
                    totalItemCount = totalItemCount - 10;
                }

                // 確保為向下捲動
                if( dy < 0 ) return;

                // 用來控制重複讀取以及已無新資料的狀況
                if (!isCateLoading && !noCateMoreData) {
                    // 顯示項目已超過十項 自動讀取資料
                    if ((visibleItemCount + pastVisiblesItems) >= totalItemCount) {
                        isCateLoading = true;
                        // 呼叫讀取下一頁資料的 function
                        MainActivity mainActivity = (MainActivity) getActivity();
                        mainActivity.nextCatePage();
                    }
                }
            }
        });

        return view;
    }

    // 設定為讀取中 顯示 loader
    public void cateRefreshing(boolean refreshing){
        if( cateSwipeRefreshLayout == null ) return;

        cateSwipeRefreshLayout.setRefreshing(refreshing);
    }

    // 加入讀取完成的資料
    public boolean addCateItems(Map<Integer, CategoryBean> categoryBeanMap){

        Set<Integer> keySet = categoryBeanMap.keySet();

        for( Integer key : keySet ){
            CategoryBean categoryBean = categoryBeanMap.get(key);
            mCateAdapter.addItem(categoryBean);
            // 因資料建立時是使用 1 based 方式所以需要 - 1
            mCateAdapter.notifyItemChanged(categoryBean.getSerialNum() - 1);
        }

        return false;
    }

    // 資料已加入 設定為可讀取下一頁資料
    public void addCateFinished(){
        isCateLoading = false;
    }

}

到這裡就完成了

本範例使用的雖然是 Fragment 但大致上使用方式使相同的

若要在直接在 Activity 中加入 RecyclerView 方法是一樣的

但 Adapter 的建立就不需要分開在不同的地方

Fragment 是因為若不先在 OnCreate 時建立 Adapter

而在 OnCreateView 內建立的話會有問題

Log 會顯示 RecyclerView 沒有配對的 Adapter 將忽略載入

這種情況即使你有將物件加入 Adapter 也會呈現空白的資料

原因目前還不清楚 但若在 Activity 內就不會有這個問題

以下是在 Activity 內建立的範例


@InjectView(R.id.pullToRefreshCateRecycler)
private SwipeRefreshLayout cateSwipeRefreshLayout;

@InjectView(R.id.cateRecyclerView)
private RecyclerView cateRecyclerView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

 mCateAdapter = new CateRecyclerAdapter( new LinkedHashMap<Integer, CategoryBean>(), new CateRecyclerAdapter.ItemHolder.IClickHandler() {
     @Override
     public void onClick(View view, int position) {

         CategoryBean categoryBean = mCateAdapter.getItem( position + 1 );

         if( categoryBean == null ) {
             Toast.makeText(view.getContext(), "Find nothing", Toast.LENGTH_SHORT).show();
         }
         else{
             goCateSubList(categoryBean);
         }
     }
 });

 cateSwipeRefreshLayout.setColorSchemeColors(0xFF4FC3F7, 0xFF8558E0, 0xFFFF326F, 0xFFF9F765);

 cateSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
     @Override
     public void onRefresh() {
         mCateAdapter.clear();
         reloadCatePage();
         noCateMoreData = false;
     }
 });

 mCateLinearLayoutManager = new LinearLayoutManager(getActivity());
 mCateLinearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);

 RecyclerView cateRecyclerView = (RecyclerView) view.findViewById(R.id.cateRecyclerView);

 cateRecyclerView.setAdapter(mCateAdapter);
 cateRecyclerView.setItemAnimator(new DefaultItemAnimator());

 cateRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
     @Override
     public void onScrolled(RecyclerView recyclerView, int dx, int dy) {

         int visibleItemCount = mCateLinearLayoutManager.getChildCount();
         int totalItemCount = mCateLinearLayoutManager.getItemCount();
         int pastVisiblesItems = mCateLinearLayoutManager.findFirstVisibleItemPosition();

         if( totalItemCount > 10 ){
             totalItemCount = totalItemCount - 10;
         }

         if( dy < 0 ) return;

         if (!isCateLoading && !noCateMoreData) {
             if ((visibleItemCount + pastVisiblesItems) >= totalItemCount) {
                 isCateLoading = true;
                 nextCatePage();
             }
         }
     }
 });
}

就這樣囉

一開始可能稍嫌複雜, 但習慣後其實滿方便的

同一個 Adapter 可以重複使用

寫程式的效率也自然跟著變高了

以上就是這次落落長的教學文...

各項資料連結
Creating Lists and Cards
Android RecyclerView