Search

Showing posts with label 使用者體驗. Show all posts
Showing posts with label 使用者體驗. Show all posts

Android - 自訂 ViewGroup 以及 CustomView 的文字大小設定

2016-05-25 7:28 PM

在 CustomViewGroup 或 CustomView 內設置文字大小時

若直接取得 Dimension 再指定給文字顯示物件

會發現字體變得非常的大

// 使用 dimension
textSize = a.getDimension(R.styleable.StackedTextView_line1_textSize, 0);

textView.setTextSize(textSize);

其實是因為使用以上方式取得的大小設定其實是已經轉換過的大小

因此我們只要取得原始的大小設定再套用解析度設定即可

// 使用 dimensionPixelSize
textSize = a.getDimensionPixelSize(R.styleable.StackedTextView_line1_textSize, 0);

// 加入 TypedValue.COMPLEX_UNIT_PX 的設定
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
各項資料連結
Android Developers

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

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 - 使用 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

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

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

Android - 隱藏 EditText 的虛擬鍵盤

2015-09-18 1:41 PM

在具有輸入介面時

Android 會自動跳出虛擬鍵盤給使用者輸入

但有時卻不會自動隱藏

或是有時我們需要手動隱藏虛擬鍵盤

就可以使用以下方式

顯示虛擬鍵盤使用方式詳見此篇介紹

Android - 顯示 EditText 的虛擬鍵盤

程式碼範例
// 取得鍵盤管理物件
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);

// 隱藏指定 view 的虛擬鍵盤, 通常會是 EditText
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
各項資料連結
Android - 顯示 EditText 的虛擬鍵盤
Android InputMethodManager

Android - 修改 Button 與 ListView, RecyclerView Item 的背景與點擊效果顏色

2015-09-06 1:31 PM

若直接使用 background 屬性修改按鈕等物件的背景顏色時通常無法保留點擊效果

如 android:background="#333333" 雖然修改了背景顏色

點擊觸發的預設動畫卻也跟著不見了

但其實是有辦法可以保留並自訂點擊特效的顏色

不論是背景顏色還是點擊效果的顏色都可以修改

方法就是將以下 xml 設定新增到 drawable 目錄底下即可

套用方式則是修改 background 屬性值

android:background="@drawable/YOUR_DRAWABLE_FILE_NAME"

程式碼範例

我們先看 API 21 以上的版本修改方式

新增一個 drawable 的 Resource

並將內容修改為以下 xml 格式

<ripple xmlns:android="http://schemas.android.com/apk/res/android"
        android:color="@color/main_login_button_ripple">
    <item android:drawable="@color/main_login_button_background"/>
</ripple>

@color/main_login_button_ripple 為點擊後效果的顏色 可自行在 color.xml 設定

@color/main_login_button_background 為按鈕背景顏色 可自行在 color.xml 設定


API 20 以下的版本則要使用以下格式的 xml

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@color/main_login_button_ripple" android:state_pressed="true"/>
    <item android:drawable="@color/main_login_button_ripple" android:state_focused="true"/>
    <item android:drawable="@color/main_login_button_background"/>
</selector>

@color/main_login_button_ripple 為點擊後效果的顏色 可自行在 color.xml 設定

@color/main_login_button_background 為按鈕背景顏色 可自行在 color.xml 設定


所以其實就是自訂兩個 color 參數

分別給背景與特效使用即可

當然此方法也可以自訂背景的形狀 例如加上圓角

方法跟 layer-list 相同

<ripple xmlns:android="http://schemas.android.com/apk/res/android"
        android:color="@color/fav_item_ripple">
    <item>
        <shape>
            <solid android:color="@color/fav_item_background"></solid>
            <corners android:radius="10px"></corners>
        </shape>
    </item>
</ripple>
各項資料連結
Drawable Resources

Android - 取消上一層 Layout 的點擊事件

2015-09-02 8:32 PM

當我們在同一個 activity 內需要跳出一個小型的 layout 供使用者操作時

如輸入方塊、提示、顯示圖片

在此若要同時遮蔽上一層 layout 的點擊狀態

我們就需要做一個滿版的 layout

並在 layout 上加入 clickable="true" 這個屬性

就可以遮蔽上一層 layout 內的所有點擊事件了

程式碼範例
<RelativeLayout
            android:id="@+id/articleContentCommentHolder"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#99000000"
            android:visibility="gone"
            android:alpha="0"
            android:clickable="true">
</RelativeLayout>

Android - Multi-thread 多執行緒與 UI 更新操作

2015-07-23 2:13 PM

在 Android 的開發規則裡有一個規定

就是所有與網路連線的功能都必須在另一個執行緒內執行

而另一個相關的重要規定是

所有與 UI 相關的功能只能在 UI Thread 內執行(也就是主執行緒)

這項規定是由於程式執行流暢度的考量而產生的

但這麼一來

若在其他執行緒的做完某件事情時 該如何使用 UI 告知使用者呢?

這時就需要用到 Thread 搭配 Handler 的 Design Pattern 了

當然還有其他的選項 例如 AsyncTask

但為了最大的開發自由度

我建議還是使用 Thread 搭配 Handler 會較一勞永逸

當然還是要看情況而定

廢話不多說 我們來看看該如何使用這項功能

這裡同時會用到 WeakReference 的 Design Pattern

會在另一個教學文章 WeakReference 與多執行緒的 Handler 提到

程式碼範例
// Handler in main activity
private SSHThreadHandler sshHandler = new SSHThreadHandler(this);

private static class SSHThreadHandler extends Handler{

    private WeakReference<MainActivity> refActivity;

    public SSHThreadHandler(MainActivity activity) {
        refActivity = new WeakReference<>(activity);
    }

    @Override
    public void handleMessage(Message message){
        MainActivity    activity = refActivity.get();
        SSHConnection  sshConnection = activity.getSshConnection();
        TextView        redirectPageStatus = activity.getRedirectPageStatus();

        switch(message.what){
            case SSHConnection.waitingUsername:
                sshConnection.inputUsername();
                redirectPageStatus.setText( "連線中... 快好囉~" );
                break;
            case SSHConnection.waitingPassword:
                sshConnection.inputPassword();
                break;
            case ISSHConnection.waitingDuplicateConfirm:
                activity.showDuplicateLoginAlert();
                break;
            case SSHConnection.waitingContinueConfirm:
                sshConnection.doContinue();
                redirectPageStatus.setText( "歡迎回來 " + sshConnection.getUsername() );
                break;
            case SSHConnection.menuPage:
                activity.hideLoginPageFrame();
                activity.hideLoadingFrame();
                activity.showMenuPageHolder();
                break;
        }
    }
}

// 其他執行緒與 Handler 互動的方式

public static final int connected = 0;
public static final int waitingUsername = 1;
public static final int waitingPassword = 2;
public static final int waitingDuplicateConfirm = 3;
public static final int waitingContinueConfirm = 4;
public static final int menuPage = 5;

@Override
 public void connect(String username, String password, Handler handler) throws IOException {
  if( somthingHappened ){
  sendMessage( doSomething );
 }
 else{
  sendMessage( doAnother );
 }
}

/**
 * Send message to the UI thread.
 */
private void sendMessage(int msg){
        Message message = new Message();
        message.what = msg;
        handler.sendMessage(message);
}

Bootstrap - 自動關閉popover

2015-06-30 1:29 PM

本次要介紹的是 Bootstrap 中的 popover 元件的客製化功能

若要在 popover 失焦時 也就是 onblur 觸發時自動淡出該如何使用呢?

在 Bootstrap 本身並沒有支援這項功能

而且 DIV 本身也不支援 blur 事件

所以我們要使用另外一種方式達成相同的效果

程式碼範例
// Part 1: 初始化 popover 元件
var button = template.find(".itemInfo button");
// 加入 class name 以便下一步使用
button.addClass("popover-toggle");

var content = $("
test
"); button.popover({ container:"body", content: content, html:true, placement:"top", title:"Popover title", trigger:'click' }); // Part 2: 加入 click 事件處理 $(document).on("click", function(event){ var target = $(event.target); $.each( $(".popover-toggle"), function(index, value){ var _target = $(value); // 不是點擊按鈕或 popover 本身時關閉 popover 物件 if( !target.is( _target ) && target.closest(".popover").length == 0 ){ _target.popover("hide"); } }); });
各項資料連結
Bootstrap

Bootstrap - 修改 tooltip 樣式

2015-06-19 9:10 PM

Bootstrap 堪稱是工程師的一大福音

這是一款由 Twitter 發佈的 CSS Framework

可以讓網頁版面製作更加輕鬆與簡便

本次要介紹的是其中的客製化功能

若要使用其中的 tooltip 元件,卻又不想使用原生的顏色

例如背景顏色已經是暗色系了

使用原生的黑色 tooltip 根本就看不到

這時就可以使用以下方式修改 tooltip 的樣式

程式碼範例
// 更改背景色及文字顏色

.tooltip > .tooltip-inner {background-color: #F7F7F7; color: #333333;}
.tooltip.left .tooltip-arrow { border-left-color: #F7F7F7; }
.tooltip.top .tooltip-arrow { border-top-color: #F7F7F7; }
.tooltip.right .tooltip-arrow { border-right-color: #F7F7F7; }
.tooltip.bottom .tooltip-arrow { border-bottom-color: #F7F7F7; }


// 若僅要修改指定容器內的tooltip樣式 則使用以下方法

#map .tooltip > .tooltip-inner {background-color: #F7F7F7; color: #333333;}
#map .tooltip.left .tooltip-arrow { border-left-color: #F7F7F7; }
#map .tooltip.top .tooltip-arrow { border-top-color: #F7F7F7; }
#map .tooltip.right .tooltip-arrow { border-right-color: #F7F7F7; }
#map .tooltip.bottom .tooltip-arrow { border-bottom-color: #F7F7F7; }

// 並在初始化tooltip時將container設為#map

editBtn.tooltip({
 animation : true,
 placement : "top", 
 html : false,
 title : "Edit hotspot",
 container : "#map"
});
各項資料連結
Bootstrap

Android - Tweener Parallel Timeline

2015-05-29 3:10 PM

在打造使用者互動特效時

並不會一次只動作一個物件

例如點擊後可能有按鈕特效加上我們自訂的光影特效

或是點擊後出現的物件特效等等

若是想同時啟動這些特效則可使用本單元介紹的 Parallel Timeline

與多個 Tweener 不同的是 Timeline 可以將一系列特效儲存起來

之後使用 start 方法即可直接啟動特效

算是製作大型互動特效的一大利器

程式碼範例
    Timeline.createSequence()
    // 首先設定所有物件的初始狀態(座標, 透明度等等)
    .push(Tween.set(...))
    .push(Tween.set(...))
    .push(Tween.set(...))

    // delay 1秒鐘
    .pushPause(1.0f)

    // 加入不同的 Tweener, 此處會依照加入的順序, 在上一個特效結束時動作
    .push(Tween.to(...))
    .push(Tween.to(...))
    .push(Tween.to(...))

    // 此處即為加入同時動作的 Tweener
    .beginParallel()
        .push(Tween.to(...))
        .push(Tween.to(...))
        .push(Tween.to(...))
    .end()

    // Yo yo 效果, 將會重複播放此 Timeline 內的特效 2 次, 並在重複開始前 delay 0.5 秒
    .repeatYoyo(2, 0.5f)

    // 啟動 Timeline
    .start(myManager);
各項資料連結
Universal Tween Engine(Google Code)
Universal Tween Engine(GitHub)

Android - Tweener 的 Accessor

2015-05-23 9:17 PM

Accessor 是針對 Java 特性加入的一個 Interface

Tweener 需要依靠實作此 Interface 的 Class 來渲染動畫效果

程式碼範例
 public class ButtonAccessor implements TweenAccessor<Button> {

    // 自訂義 Tween 類型, 用以判別在實作的 Method 內需處理的對象
    public static final int POSITION = 1;
    public static final int ALPHA = 2;

    @Override
    public int getValues(Button target, int tweenType, float[] returnValues) {
        switch (tweenType) {
            case POSITION:
  // 由物件內取回實際值
                returnValues[0] = target.getX();
                returnValues[1] = target.getY();
  // 修改 2 個屬性, 因此回傳 2
                return 2;
            case ALPHA:
                returnValues[0] = target.getAlpha();
  // 修改 1 個屬性, 回傳 1
                return 1;
            default:
                return -1;
        }
    }

    @Override
    public void setValues(Button target, int tweenType, float[] newValues) {
        switch (tweenType) {
            case POSITION:
  // 設定新屬性
                target.setX(newValues[0]);
                target.setY(newValues[1]);
                break;
            case ALPHA:
  // 設定新屬性(需對應 getValues 的設定)
                target.setAlpha(newValues[0]);
                break;
            default:
                break;
        }
    }
各項資料連結
Universal Tween Engine(Google Code)
Universal Tween Engine(GitHub)

Android - 使用 Tweener 加入互動特效

9:11 PM

以往在撰寫 Flash 及網頁時

若要加入互動效果,我們常常會使用一個叫做 Tweener 的引擎

使用 Tweener 後可以很容易地達成物件的移動與淡出淡入等特效

如此一來就可以增加介面的互動效果達到更好的使用者體驗

以下就來介紹如何在 Android 內使用 Tweener

首先須下載 Universal Tween Engine

程式碼範例

首先須宣告 TweenManager 以及控制 Renderer 的 boolean 值

 // 所有 Tweener 皆須傳入此 Manager
 private final TweenManager tweenManager = new TweenManager();

 // 設為 false 則停止 renderer
 private boolean isAnimationRunning = true;

新增一個多執行緒的 renderer (可在 onCreate 或 onResume 內新增) 並註冊 Accessor

 // 註冊各類別處理器
 Tween.registerAccessor(Button.class, new ButtonAccessor());

 // Renderer
 new Thread(new Runnable() {
            private long lastMillis = -1;

            @Override
            public void run() {
                while (isAnimationRunning) {
                    if (lastMillis > 0) {
                        long currentMillis = System.currentTimeMillis();
                        final float delta = (currentMillis - lastMillis) / 1000f;

                        runOnUiThread(new Runnable() {
                            @Override public void run() {
                                tweenManager.update(delta);
                            }
                        });

                        lastMillis = currentMillis;
                    } else {
                        lastMillis = System.currentTimeMillis();
                    }

                    try {
   // 60 FPS
                        Thread.sleep(1000/60);
                    } catch(InterruptedException ex) {
                    }
                }
            }
        }).start();

之後便可任意呼叫 Tweener 以及 Timeline

 Tween.to(loginBtn, ButtonAccessor.ALPHA, 0.1f).target(0.5f).ease(Quad.OUT).start(tweenManager);

 Timeline.createSequence()
             .push( Tween.to(loginBtn, ButtonAccessor.ALPHA, 0.1f).target(0.1f).ease(Quad.OUT) )
             .push( Tween.to(loginBtn, ButtonAccessor.ALPHA, 0.3f).target(1).ease(Quad.OUT) )
             .start(tweenManager);
各項資料連結
Universal Tween Engine(Google Code)
Universal Tween Engine(GitHub)

JQuery - 全螢幕控制切換與其事件

2014-11-30 11:11 PM

有時若需要讓使用者完全沉浸於我們設計的使用者體驗內時

就會需要使用全螢幕的功能

但可能是安全性上的考量或是避免開發者濫用全螢幕功能

此功能必須由使用者以按鈕觸發才可動作

以下範例為在特定DIV加上全螢幕控制按鈕功能

並可切換指定容器顯示為全螢幕狀態

程式碼範例
// 全螢幕控制按鈕
var fullscreenDiv = $("#fullscreenDiv");
fullscreenDiv.on("click", doFullScreen);

// 針對各瀏覽器觸發全螢幕要求
function doFullScreen(){
 // 取得要顯示為全螢幕的容器
 var elem = $("#fullScreenTarget")[0];
 
 var isFullScreen = document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen;
 
 // 目前非全螢幕狀態 開啟全螢幕
 if( !isFullScreen ){
  if (elem.requestFullscreen) {
   elem.requestFullscreen();
  } else if (elem.msRequestFullscreen) {
   elem.msRequestFullscreen();
  } else if (elem.mozRequestFullScreen) {
   elem.mozRequestFullScreen();
  } else if (elem.webkitRequestFullscreen) {
   elem.webkitRequestFullscreen();
  }
 }
 // 目前為全螢幕狀態 關閉全螢幕
 else{
  if (document.exitFullscreen) {
    document.exitFullscreen();
  } else if (document.msExitFullscreen) {
    document.msExitFullscreen();
  } else if (document.mozCancelFullScreen) {
    document.mozCancelFullScreen();
  } else if (document.webkitExitFullscreen) {
    document.webkitExitFullscreen();
  }
 }
}

// 全螢幕要求事件(包括進入及退出)
$(document).on("mozfullscreenchange webkitfullscreenchange fullscreenchange", fullscreenChange);


// 針對全螢幕狀態處理
function fullscreenChange(){
 var isFullScreen = document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen;
 
 // 目前非全螢幕狀態 設定回預設大小(由內容決定)
 if( !isFullScreen ){
  $("#fullScreenTarget").width("");
 }
 // 目前為全螢幕狀態 設定為100%大小
 else{
  $("#fullScreenTarget").width("100%");
 }
}

JQuery - Image onLoad 讀取/載入完成事件

2014-11-23 7:42 PM

以下程式碼範例為圖片讀取完成後
淡出原本容器的背景 再淡入背景圖片的效果

程式碼範例
var target  = $(value);
var src  = "/image/cover/thumbnail";
   
$('<img/>').attr('src', src).on("load", function() {
 var base64 = convertImgToBase64( $(this)[0] );
 $(this).remove();

 TweenMax.to(target, 0.5, {
  alpha:0,
  ease:Quad.easeInOut,
  onCompleteParams:[target, base64],
  onComplete:function(t, i){
   t.css("background", "rgba(31, 31, 31, 1) url(" + i + ") no-repeat center center");
   TweenMax.to(t, 1.3, {alpha:1, ease:Quad.easeInOut});
  }
 });
});

function convertImgToBase64(img){
 var canvas  = document.createElement('CANVAS');
 var ctx  = canvas.getContext('2d');
 
 canvas.height  = img.height;
 canvas.width  = img.width;
 ctx.drawImage(img, 0, 0);
 
 var dataURL = canvas.toDataURL();
 canvas = null; 
 
 return dataURL;
}