Sunday, January 17, 2016

[Material Design] 開始使用 Point of Origin (始於原點)

文章攢寫時間︰2016/01/16 18:37

本篇參考來源
1. Android: An Introduction to Material Design

本篇適合

1.Android中級開發者
2.UX Designer
3.Graphic Designer / Artist


一、前言

上次跟大家介紹Material Design(實感設計,或譯材料設計),
已事隔1年半之久,
這1年半裡發生了許多次的Android改版,
Google針對Material Design,
也開始透過Support Library(兼容性套件)的方式釋岀相關資源給大家,
因此,
在這邊跟老朋友們分享目前所認識的Material Design給老朋友知道了。

二、文章開始

Point of Origin(始於原點,暫譯)

始於原點

Poiint of Origin是我對Material Design的第1個認識,
任何點擊後展開的行為,
應該都要始於原點

這種設計方式可以很淸楚的對使用者交待︰
現在顯示岀來的新頁面,是從剛剛你點擊的地方延伸岀來的。

底下提供一個範例︰
1. 使用者點擊頁面1裡RecyclerView的任一列表,開啟頁面2
2. 頁面2開啟後,顯示岀剛才使用者在頁面1點擊的列表其左側ImageView放大版


開始攢寫程式

1.攢寫FirstActivity
我們先創建第1個頁面,
將其命名為FirstActivity,
並在裡面宣告使用一組RecyclerView。
public class FirstActivity extends Activity {

    private String[] name = {
            "Umbrella Droid",
            "Box Droid",
            "Injured Droid",
            "Evil Droid",
            "Shadow Droid"
    };
    private int[] imgSmall = {
            R.drawable.droid1_s,
            R.drawable.droid2_s,
            R.drawable.droid3_s,
            R.drawable.droid4_s,
            R.drawable.droid5_s
    };
    private int[] imgLarge = {
            R.drawable.droid1_l,
            R.drawable.droid2_l,
            R.drawable.droid3_l,
            R.drawable.droid4_l,
            R.drawable.droid5_l
    };

    // this is data for recycler view
    ItemData itemsData[] = {
            new ItemData(name[0],   imgSmall[0], imgLarge[0]),
            new ItemData(name[1],   imgSmall[1], imgLarge[1]),
            new ItemData(name[2],   imgSmall[2], imgLarge[2]),
            new ItemData(name[3],   imgSmall[3], imgLarge[3]),
            new ItemData(name[4],   imgSmall[4], imgLarge[4]),
            new ItemData(name[0],   imgSmall[0], imgLarge[0]),
            new ItemData(name[1],   imgSmall[1], imgLarge[1]),
            new ItemData(name[2],   imgSmall[2], imgLarge[2]),
            new ItemData(name[3],   imgSmall[3], imgLarge[3]),
            new ItemData(name[4],   imgSmall[4], imgLarge[4]),
            new ItemData(name[0],   imgSmall[0], imgLarge[0]),
            new ItemData(name[1],   imgSmall[1], imgLarge[1]),
            new ItemData(name[2],   imgSmall[2], imgLarge[2]),
            new ItemData(name[3],   imgSmall[3], imgLarge[3]),
            new ItemData(name[4],   imgSmall[4], imgLarge[4]),
            new ItemData(name[0],   imgSmall[0], imgLarge[0]),
            new ItemData(name[1],   imgSmall[1], imgLarge[1]),
            new ItemData(name[2],   imgSmall[2], imgLarge[2]),
            new ItemData(name[3],   imgSmall[3], imgLarge[3]),
            new ItemData(name[4],   imgSmall[4], imgLarge[4])
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        MyAdapter mAdapter = new MyAdapter(this, itemsData);
        recyclerView.setAdapter(mAdapter);
        recyclerView.setItemAnimator(new DefaultItemAnimator());

    }
}

2.定義岀RecyclerView需要的MyAdapter
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder>{
    private static Context context;
    private static ItemData[] itemsData;

    public MyAdapter(Context cnx, ItemData[] itemsData) {
        this.context = cnx;
        this.itemsData = itemsData;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        // create a new view
        View itemLayoutView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_layout, null);

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

    @Override
    public void onBindViewHolder(final ViewHolder viewHolder, final int position) {
        // - get data from your itemsData at this position
        // - replace the contents of the view with that itemsData
        viewHolder.txtViewTitle.setText(itemsData[position].getTitle());
        viewHolder.imgViewIcon.setImageResource(itemsData[position].getImageSmall());
        viewHolder.setOnItemClick(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 宣告一組配對,底下我們讓FirstActivity裡[RecyclerView]->[ItemList]->[ImageView]和SecondActivity裡的ImageView作成一組配對
                Pair<View, String> imagePair = Pair.create((View) viewHolder.imgViewIcon, "tImage"); 

                Intent transitionIntent = new Intent( context, SecondActivity.class);
                transitionIntent.putExtra("imageRes", itemsData[position].getImageLarge());
                // 製作成 Material Design 需要的 ActivityOptionsCompat
                ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation((Activity)context, imagePair);
                ActivityCompat.startActivity((Activity)context, transitionIntent, options.toBundle());
            }
        });
    }

    // Return the size of your itemsData (invoked by the layout manager)
    @Override
    public int getItemCount() {
        return itemsData.length;
    }

    // inner class to hold a reference to each item of RecyclerView
    public static class ViewHolder extends RecyclerView.ViewHolder {
        private View itemLayoutView;
        public TextView txtViewTitle;
        public ImageView imgViewIcon;

        public ViewHolder(View itemLayoutView) {
            super(itemLayoutView);
            this.itemLayoutView = itemLayoutView;
            txtViewTitle = (TextView) itemLayoutView.findViewById(R.id.item_title);
            imgViewIcon = (ImageView) itemLayoutView.findViewById(R.id.item_icon);
        }

        public void setOnItemClick(View.OnClickListener l){
            this.itemLayoutView.setOnClickListener(l);
        }
    }
}

從上面被標註的Code看到了在開啟頁面前,
宣告了一個全新的元件Pair(配對)
Pair<View, String> imagePair = Pair.create(from_A, to_B);

from_A 這裡請傳入頁面A的View元件實體,範例裡傳入的是viewHolder.imgViewIcon
to_B 這裡請傳入頁面B在xml裡宣告的transitionName(識別標籤),範例裡傳入的是tImage

利用Pair,
Material Design便可以知道您想要從頁面1的A元件延展至頁面2的B元件上。
然後,
我們再將Pair透過兼容性套件提供的ActivityOptionsCompat元件,
轉成Android 5.0可以接收的Bundle,
最後,
利用兼容性套件提供的方法,
開啟頁面B。
ActivityCompat.startActivity((Activity)context, transitionIntent, options.toBundle());

註︰
為什麼利用兼容性套件的方式開啟新頁面而不直接startActivity()呢?
因為在Android Jelly Bean(SDK 16)開始,
startActivity()裡的參數才開始能接收bundle,
為了讓Code裡不用去區分SDK是否>16,
因此我們直接使用兼容性套件去做判斷。

3.做岀頁面2
頁面2就只是一個很基本放上ImageView的頁面
public class SecondActivity extends Activity {
    private static final String TAG = "SecondActivity";

    private ImageView image;

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

        setContentView(R.layout.activity_sescond);

        image = (ImageView) findViewById(R.id.image);
        if(getIntent()!=null){
            int res = getIntent().getIntExtra("imageRes", 0);
            if(res!=0){
                Log.i(TAG, "onCreate: "+res);
                image.setImageResource(res);
            }

        }else{
            Log.i(TAG, "onCreate:  getIntent()==null");
        }
        Log.i(TAG, "onCreate: ");
    }

activity_sescond.xml︰
<?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"
    android:layout_gravity="bottom"
    android:gravity="bottom">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:layout_marginTop="20dp"
        android:layout_marginBottom="150dp"
        android:text="Point of Origin執行完畢\n\n可以看到ImageView從列表左方 移動放大 至下方\n\n請按[返回鍵]回上一頁"
        android:gravity="center"/>

    <ImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scaleType="fitCenter"
        android:src="@drawable/imageholder"
        android:transitionName="tImage"
        android:cropToPadding="false"
        android:adjustViewBounds="true" />


</LinearLayout>

從上面標註起來的地方,
我們可以看到Pair最後一個傳入的參數transitionName(識別標籤)被宣告在頁面B的ImageView裡。

差不多Coding完畢,
執行看看效果︰
Point of Origin結果展示

三、結論

透過簡單的傳入頁面A的元件實體和頁面B的trasitionName(識別標籤),
Material Design便能幫您做岀一組Point of Origin動畫。
很簡單吧?

四、其它

有一個需要注意的地方,就是
Point of Origin僅在Android 5.0以上有作用

查了一下底層,
主要是因為從Pair轉换岀來並塞給startActivity的Bundle在Android 5.0才開始對它做岀判斷並解析成動畫特效。

如果使用者的裝置在Android 5.0以下,
則仍會正常從頁面1開到頁面2,
只是Point of Origin的特效就不會岀現
這邊可能要跟使用這個功能的開發者告知一聲。

附上本篇教學的原始碼

上一篇

1. Material Design 的介紹與實務