Search

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

Java - trim left/trim right 去除字串前/後空白

2015-10-14 7:03 PM

String.trim(); 是一個可以將字串前後空白刪除的方法

而若只想要刪除字串左邊/開頭的空白(也就是trim left)

或是只想要刪除字串右邊/結尾的空白(也就是trim right)

就可以使用 Apache Commons Lang 提供的 StringUtils 達成這個效果

程式碼範例
/*
 *  Gradle dependencies
 *  compile 'org.apache.commons:commons-lang3:3.4'
 */

import org.apache.commons.lang3.StringUtils;

// 移除字串開頭空白 結果將印出 "Yeah!   "
StringUtils.stripStart( "   Yeah!   ", " " );

// 移除字串結尾空白 結果將印出 "   Yeah!"
StringUtils.stripEnd( "   Yeah!   ", " " );

// 且不限於刪除空白字元 結果將印出 "120"
StringUtils.stripEnd( "120.00", ".0" );
各項資料連結
Apache Commons Lang
Apache Commons Lang Maven Repository

IntelliJ IDEA - 在 Android Gradle 專案啟用 ProGuard

2015-10-11 12:18 PM

ProGuard 是 Android 官方提供的防止反編譯工具

原理其實跟 Javascript, CSS 的 YUI Compressor 類似

可以壓縮並混淆程式碼

不管你原本寫得多漂亮都會變成亂七八糟

但請放心, 大部分情況打包後都是可以正常執行的

而在 IntelliJ Idea 中啟用 ProGuard 的方式不太一樣

由於預設是使用 Gradle 作為專案建置自動化工具

因此在專案建立時你會發現

專案結構內找不到任何 project.properties 檔案或是 proguard-project.txt 檔案

而在這類專案內修改的方式其實簡單許多

就是到專案的 build.gradle 內找到 minifyEnabled false 這行

並將 false 修改為 true 即可

程式碼範例
buildTypes {
 release {
  minifyEnabled true
  proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
 }
}

如此一來在打包 release 版本時就會自動啟用 porguard

因為混淆程式碼難免會有一些問題

這時就需要修改混淆的規則, 加入一些例外處理等等

可以到專案底下的 proguard-rules.pro 檔案內修改

各項資料連結
IntelliJ IDEA
Gradle
Android ProGuard

Java - Google 也愛用的參數列舉方式

2015-10-09 8:33 PM

因為工作有機會接觸到 Google 的原始碼

就順便學到這個建構方式

可以說是既方便又結構化

最大的優點就是傳遞此類參數時可以用型態防呆

又可以在寫入資料庫時輕鬆轉換為數值

程式碼範例
public class ArticleType extends EnumBase {
    
 // 儲存參數
 private static Map MAP = new LinkedHashMap<>();

 // 建立參數
 private static final String _NORMAL      = "normal";
 private static final String _ALWAYS_TOP  = "always_top";
 private static final String _DELETED     = "deleted";

 // 建立參數物件
 public static final ArticleType NORMAL     = new ArticleType(_NORMAL);
 public static final ArticleType ALWAYS_TOP = new ArticleType(_ALWAYS_TOP);
 public static final ArticleType DELETED    = new ArticleType(_DELETED);

 protected ArticleType(String value) {
     super(value);
 }

 // 取得所有參數物件
 public static List enums() throws IllegalArgumentException, IllegalAccessException{
     return enums(ArticleType.class);
 }

 // 利用參數值取出物件
 public static ArticleType fromValue(String value){
     return MAP.get(value);
 }

 // 取得 Map 物件
 @SuppressWarnings("unchecked")
 @Override
 protected  Map getMap() {
     return (Map) MAP;
 }

 // 取得參數名稱
 public String getName(){
     switch( this.getValue() ){
         case _NORMAL:
             return "一般文章";
         case _ALWAYS_TOP:
             return "置頂文章";
         case _DELETED:
             return "已刪除文章";
         default:
             return "無此類型";
     }
 }
    
}

以下是父類別的程式碼

public abstract class EnumBase {

 protected String value;

 // 創建時順便放入 Map 中
 protected EnumBase(String value) {
     super();
     this.value = value;
     this.setMap(value);
 }

 // 取得所有參數物件
 protected static  List enums(Class clazz) 
  throws IllegalArgumentException, IllegalAccessException{
     List  list  = new ArrayList();
     Field[]  fields  = clazz.getFields();

     for( Field field : fields ){
         if( field.getType() == clazz ){
             @SuppressWarnings("unchecked")
             T target = (T) field.get(clazz);
             list.add(target);
         }
     }

     return list;
 }

 // 將參數放入 Map 中
 public void setMap(String value){
     this.getMap().put(value, this);
 }

 // 取得參數值
 public String getValue(){
     return value;
 }

 public String toString(){
     return this.getValue();
 }

 // 取得參數名稱
 public abstract String getName();

 // 取得 Map
 protected abstract  Map getMap();

}
各項資料連結
Google

Android - Activity 傳遞自訂物件參數

8:14 PM

傳遞字串, 數字等參數的方法我想大家應該都沒問題

但 Intent 就是沒有 putExtra( Object ); 的方法

用 Serializable 又會出現錯誤訊息 而且其實有點偷吃步

正確的方法是應該使用 Parcelable 這個介面來完成

首先須實作 Parcelable 介面

public class ArticleBean implements Parcelable

並在 writeToParcel 將欄位值寫入 Parcel 內

@Override
public void writeToParcel(Parcel dest, int flags) {
 dest.writeInt(serialNumber);
 dest.writeString(boardTitle);
 dest.writeString(aid);
 dest.writeString(title);
 dest.writeString(status);
 dest.writeString(pushes);
 dest.writeString(category);
 dest.writeString(author);
 dest.writeString(date);
 dest.writeBooleanArray( new boolean[]{online, read, changed, locked, connected, mail} );
 dest.writeString( articleType.getValue() );
 dest.writeString(articleHeader.getValue());
}

然後再新增一個 static 的 CREATOR 讓參數可以被重新產生

是的 傳遞到另一個 Activity 的參數是重新產生過的新物件

public static final Parcelable.Creator<ArticleBean> CREATOR = new Parcelable.Creator<ArticleBean>() {
 public ArticleBean createFromParcel(Parcel parcel) {
     return new ArticleBean(parcel);
 }

 public ArticleBean[] newArray(int size) {
     return new ArticleBean[size];
 }
};

最後再新增一個使用 Parcel 創建的建構子

重點是所有參數不論型態皆須依照你放入的順序依次取出

例如你第一個放的是字串 第一個取出來的就必須是字串

如果你取得 boolean 他就會報錯

private ArticleBean(Parcel parcel) {
 boolean[] booleans = new boolean[6];

 serialNumber = parcel.readInt();
 boardTitle = parcel.readString();
 aid = parcel.readString();
 title = parcel.readString();
 status = parcel.readString();
 pushes = parcel.readString();
 category = parcel.readString();
 author = parcel.readString();
 date = parcel.readString();

 parcel.readBooleanArray(booleans);

 online = booleans[0];
 read = booleans[1];
 changed = booleans[2];
 locked = booleans[3];
 connected = booleans[4];
 mail = booleans[5];
 articleType = ArticleType.fromValue( parcel.readString() );
 articleHeader = ArticleHeader.fromValue( parcel.readString() );
}

其中的 ArticleType.fromValue 是我慣用的列舉參數方式

詳細建構方式可以參考這個連結 Google 也愛用的列舉參數建構法

傳遞參數時則一樣使用 intent.putExtra

intent.putExtra( "Article.Bean", articleBean );

取出參數則是使用 getParcelable 取出對應的值

articleBean = getIntent().getExtras().getParcelable("Article.Bean");

以下是完整的 Class 程式碼範例

public class ArticleBean implements Parcelable {

    private int serialNumber;

    private String aid;

    private String boardTitle;

    private String title;

    private String status;

    private String pushes;

    private String category;

    private String author;

    private String date;

    private boolean online;

    private boolean read;

    private boolean changed;

    private boolean locked;

    private boolean connected = false;

    private boolean mail = false;

    private ArticleType articleType;

    private ArticleHeader articleHeader;

    public int getSerialNumber() {
        return serialNumber;
    }

    public void setSerialNumber(int serialNumber) {
        this.serialNumber = serialNumber;
    }

    public String getAid() {
        return aid;
    }

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

    public String getBoardTitle() {
        return boardTitle;
    }

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

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public String getPushes() {
        return pushes;
    }

    public void setPushes(String pushes) {
        this.pushes = pushes;
    }

    public String getCategory() {
        return category;
    }

    public void setCategory(String category) {
        this.category = category;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    public boolean isOnline() {
        return online;
    }

    public void setOnline(boolean online) {
        this.online = online;
    }

    public boolean isRead() {
        return read;
    }

    public void setRead(boolean read) {
        this.read = read;
    }

    public boolean isChanged() {
        return changed;
    }

    public void setChanged(boolean changed) {
        this.changed = changed;
    }

    public boolean isLocked() {
        return locked;
    }

    public void setLocked(boolean locked) {
        this.locked = locked;
    }

    public boolean isConnected() {
        return connected;
    }

    public void setConnected(boolean connected) {
        this.connected = connected;
    }

    public boolean isMail() {
        return mail;
    }

    public void setMail(boolean mail) {
        this.mail = mail;
    }

    public ArticleType getArticleType() {
        return articleType;
    }

    public void setArticleType(ArticleType articleType) {
        this.articleType = articleType;
    }

    public ArticleHeader getArticleHeader() {
        return articleHeader;
    }

    public void setArticleHeader(ArticleHeader articleHeader) {
        this.articleHeader = articleHeader;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(serialNumber);
        dest.writeString(boardTitle);
        dest.writeString(aid);
        dest.writeString(title);
        dest.writeString(status);
        dest.writeString(pushes);
        dest.writeString(category);
        dest.writeString(author);
        dest.writeString(date);
        dest.writeBooleanArray( new boolean[]{online, read, changed, locked, connected, mail} );
        dest.writeString( articleType.getValue() );
        dest.writeString(articleHeader.getValue());
    }

    // 傳遞參數時 將會呼叫此方法重新創建物件
    public static final Parcelable.Creator<ArticleBean> CREATOR = 
     new Parcelable.Creator<ArticleBean>() {
         public ArticleBean createFromParcel(Parcel parcel) {
             return new ArticleBean(parcel);
         }

         public ArticleBean[] newArray(int size) {
             return new ArticleBean[size];
         }
     };

    private ArticleBean() {}

    // 以 Parcel 創建的建構子
    private ArticleBean(Parcel parcel) {
        boolean[] booleans = new boolean[6];

        // 所有參數皆須按照加入的順序取出
        serialNumber = parcel.readInt();
        boardTitle = parcel.readString();
        aid = parcel.readString();
        title = parcel.readString();
        status = parcel.readString();
        pushes = parcel.readString();
        category = parcel.readString();
        author = parcel.readString();
        date = parcel.readString();

        parcel.readBooleanArray(booleans);

        online = booleans[0];
        read = booleans[1];
        changed = booleans[2];
        locked = booleans[3];
        connected = booleans[4];
        mail = booleans[5];
        articleType = ArticleType.fromValue( parcel.readString() );
        articleHeader = ArticleHeader.fromValue( parcel.readString() );
    }

    public static class Builder {
        private int serialNumber;
        private String aid;
        private String boardTitle;
        private String title;
        private String status;
        private String pushes;
        private String category;
        private String author;
        private String date;
        private boolean online;
        private boolean read;
        private boolean changed;
        private boolean locked;
        private boolean connected = false;
        private boolean mail = false;
        private ArticleType articleType;
        private ArticleHeader articleHeader;

        private Builder() {
        }

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

        public Builder withSerialNumber(int serialNumber) {
            this.serialNumber = serialNumber;
            return this;
        }

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

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

        public Builder withTitle(String title) {
            this.title = title;
            return this;
        }

        public Builder withStatus(String status) {
            this.status = status;
            return this;
        }

        public Builder withPushes(String pushes) {
            this.pushes = pushes;
            return this;
        }

        public Builder withCategory(String category) {
            this.category = category;
            return this;
        }

        public Builder withAuthor(String author) {
            this.author = author;
            return this;
        }

        public Builder withDate(String date) {
            this.date = date;
            return this;
        }

        public Builder withOnline(boolean online) {
            this.online = online;
            return this;
        }

        public Builder withRead(boolean read) {
            this.read = read;
            return this;
        }

        public Builder withChanged(boolean changed) {
            this.changed = changed;
            return this;
        }

        public Builder withLocked(boolean locked) {
            this.locked = locked;
            return this;
        }

        public Builder withConnected(boolean connected) {
            this.connected = connected;
            return this;
        }

        public Builder withMail(boolean mail) {
            this.mail = mail;
            return this;
        }

        public Builder withArticleType(ArticleType articleType) {
            this.articleType = articleType;
            return this;
        }

        public Builder withArticleHeader(ArticleHeader articleHeader) {
            this.articleHeader = articleHeader;
            return this;
        }

        public Builder but() {
            return anArticleBean().withSerialNumber(serialNumber).withAid(aid).withBoardTitle(boardTitle).withTitle(title).withStatus(status).withPushes(pushes).withCategory(category).withAuthor(author).withDate(date).withOnline(online).withRead(read).withChanged(changed).withLocked(locked).withConnected(connected).withMail(mail).withArticleType(articleType).withArticleHeader(articleHeader);
        }

        public ArticleBean build() {
            ArticleBean articleBean = new ArticleBean();
            articleBean.setSerialNumber(serialNumber);
            articleBean.setAid(aid);
            articleBean.setBoardTitle(boardTitle);
            articleBean.setTitle(title);
            articleBean.setStatus(status);
            articleBean.setPushes(pushes);
            articleBean.setCategory(category);
            articleBean.setAuthor(author);
            articleBean.setDate(date);
            articleBean.setOnline(online);
            articleBean.setRead(read);
            articleBean.setChanged(changed);
            articleBean.setLocked(locked);
            articleBean.setConnected(connected);
            articleBean.setMail(mail);
            articleBean.setArticleType(articleType);
            articleBean.setArticleHeader(articleHeader);
            return articleBean;
        }
    }
}
各項資料連結
Android Parcelable
Google 也愛用的列舉參數建構法

Eclipse - Use EGit to push project with ssh key

2015-10-08 3:32 PM
Go to Window -> Preference -> General -> Network Connections -> SSH2

You'll see a tab named "General".

And the default value of "SSH2 home" will be "C:\User\user\.ssh"

The default value of "Private keys" will be "id_rsa" or "id_dsa" (or empty if you never generated one ).




Then go to Key Management tab page and choose what kind of algorithm you want to use, DSA key or RSA key ( bitbucket and github are both using RSA ).

And you can see the public key in the textarea after you click the Generate Key button




Paste thie public key to the key management page on Bitbucket or Github.

Back to Eclipse and click "Save Private Key" to save your key into SSH2 home directory.

Then click "Export Via SFTP" and input your Known hosts ( git@bitbucket.org or git@github.com ).




Click "OK" to save and close the SSH2 settings window.

If you wanna make sure if it's saved correctly, you can reopen this SSH2 settings

and navigate to "Key Management" tab page and click "Load Existing Key" and load the key you just created.




Then you can see your known hosts in "Known Hosts" tab page.




Due to the caching problem you should restart Eclipse after you changed these settings.

Or you may get some certification problems.

And now you can push your project to Bitbucket or Github with ssh key.

Android - 維持 ViewPager 內 View 的隱藏狀態

2015-10-07 6:40 PM

ViewPager 是一個非常好用的分頁工具

但若想要在其中的分頁加入多重的 View

並依狀態顯示不同的 View 時

初學的開發者常會遇到一個問題

就是滑到其他分頁在滑回來時

就是被隱藏的 View 會自動跑出來

那麼要解決這個問題有兩個方法

一個是設置 offscreenPageLimit

viewPager.setOffscreenPageLimit(cachePagers);

如此一來分頁狀態就不會被重設

但此方法會消耗記憶體資源

所以我推薦的是另一個方法

就是在 Fragment 內的 onCreateView 內判斷該顯示的 View 是哪個

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

 if (isSubPage) {
  showSubLayout();
 } else {
  showMainLayout();
 }

}

因為在重繪 Fragment 時會呼叫 onCreateView

因此在這邊預先處理便可解決這個問題

各項資料連結
Android ViewPager

Android - SwipeRefreshLayout 的 setRefreshing 無效

2015-10-05 8:19 PM

相信很多人在開發時都有遇過這個狀況

就是我明明就使用了 SwipeRefreshLayout.setRefreshing(true) 了

卻還是沒有出現轉轉轉的圖示

是 setRefreshing 無效了嗎? 還是有甚麼地方搞錯了?

這裡就要告訴各位一個很簡單的解決方法

只要用以下程式碼來呼叫 setRefreshing(true) 就可以了

程式碼範例
pullToRefreshMailContent.post(new Runnable() {
    @Override
    public void run() {
        pullToRefreshMailContent.setRefreshing(true);
    }
});
各項資料連結
Android SwipeRefreshLayout