[譯]Workcation App – 第四部分. 場景(Scenes)和 RecyclerView 的共享元素轉場動畫(Shared Element Transition)

龍騎將楊影楓發表於2017-06-05

Workcation App – 第四部分. 場景(Scenes)和 RecyclerView 的共享元素轉場動畫(Shared Element Transition)

探索如何通過場景框架(Scene Framework)建立展示詳情頁的共享元素轉場動畫(Shared Element Transition)

歡迎閱讀本系列文章的第四篇也是最後一篇,此係列文章和我前一段時間完成的“研發”專案有關。在文章裡,我會針對開發中遇到的動畫問題分享一些解決辦法。在這篇博文裡,我會編寫最後的部分:如何通過場景框架(Scene Framework)建立展示詳情頁的共享元素轉場動畫(Shared Element Transition)。

Part 1: 自定義 Fragment 轉場

Part 2: 帶有動畫的標記(Animating Markers) 與 MapOverlayLayout

Part 3: 帶有動畫的標記(Animated Markers) 與 RecyclerView 的互動

Part 4: 場景(Scenes)和 RecyclerView 的共享元素轉場動畫(Shared Element Transition)

專案的 Git 地址: Workcation App

動畫的 Dribbble 地址: dribbble.com/shots/28812…

序言

幾個月前我們開了一個部門會議,在會議上我的朋友 Paweł Szymankiewicz 給我演示了他在自己的“研發”專案上製作的動畫。我非常喜歡這個動畫,在開完會以後我準備把用程式碼實現它。我可沒想到到我會攤上啥...

[譯]Workcation App – 第四部分. 場景(Scenes)和 RecyclerView 的共享元素轉場動畫(Shared Element Transition)

GIF 1 “動畫效果”

就像上面 GIF 動畫展示的,需要做的事情有很多。

  1. 在點選底部選單欄最右方的選單後,我們會跳轉到一個新介面。在此介面中,地圖通過縮放和漸顯的轉場動畫在螢幕上方載入,Recycleview 的 item 隨著轉場動畫從底部載入,地圖上的標記點在轉場動畫執行的同時被新增到地圖上.

  2. 當滑動底部的 RecycleView item 的時候,地圖上的標記會通過閃爍來顯示它們的位置(譯者注:原文是show their position on the map,個人認為 position 有兩層含義:一代表標記在地圖上的位置,二代表標記所對應的 item 在 RecycleView 裡的位置。)

  3. 在點選一個 item 以後,我們會進入到新介面。在此介面中,地圖通過動畫方式來顯示出路徑以及起始/結束標記。同時此 RecyclerView 的item 會通過轉場動畫展示一些關於此地點的描述,背景圖片也會放大,還附有更詳細的資訊和一個按鈕。

  4. 當後退時,詳情頁通過轉場變成普通的 RecycleView Item,所有的地圖示記再次顯示,同時路徑一起消失。

就這麼多啦,這就是我準備在這一系列文章中向你展示的東西。在本文中,我回展示如何通過場景框架、共享元素轉場動畫來展示詳情頁。

需求

好吧,我們已經看過上面的GIF了。在點選了RecycleView 的 item 以後,我們進入了詳情頁面,上面顯示了旅行目的地的一些資訊。這確實是一個共享元素的轉場動畫:view 和 Textview 同時改變自身的大小、填充詳情內容,含有紅色按鈕的詳情介紹從底部向上滑動顯示。多虧了轉場動畫框架(Transition Framework),我們可以用程式碼實現這種酷炫的動畫效果。

我最初的想法和 90%的 網上設計一樣 —— 宣告一個 activities 之間的共享元素轉場動畫(Shared Element Transition)。然而讓我們看一下地圖,詳情佈局下面還有一個動畫 —— 繪製路徑同時地圖縮放至特定位置。所以建立另一個背景透明 activity 並試圖在此 activity 上繪製地圖的動畫效果的做法是不合適的。

我第二個想法是建立一個 fragment 之間的共享元素轉場動畫(Shared Element Transition)—— 將 DetailsFragment 新增在頂端,在兩個 view 之間新增一個轉場動畫 —— 就是 RecycleView 的 item 和 DetailFragment 的容器。這麼做是更好一些 —— 但是對我來說,又是同樣的螢幕啊、fragment什麼的,有所不同的只是最上層又添了一層佈局。那麼,有滿足我需求的辦法嗎?

當然有!自從 Android 4.4 以來(Workcation App 的 SDK 是 Android 5.0 以上的版本)我們就有了這麼一個選擇 —— 場景(Scenes)!當使用轉場框架(Transition Framework)的時候,它們確實很勥。我們可以用非常精妙的方式管理使用者介面。最重要的是 —— 完全符合我們的需求!看看它是怎麼實現的吧!

RecycleView 的可共享轉場動畫

讓我們從點選 RecycleView 的 item 開始吧。DetailsFragment (帶有地圖和 RecycleView 的那個)實現了 OnPlaceClickedListener 介面。我們是這樣向構造方法傳遞 OnPlaceClickListener 的介面實現類作為引數的:

Java

BaliPlacesAdapter(OnPlaceClickListener listener,Context context){

    this.listener=listener;

    this.context=context;

}複製程式碼

接著在 onBindViewHolder 方法中,點選 RecycleView item 以後觸發 onPlaceClicked。我們簡單的通過給item 設定 onClickListener 來實現:

@Override

public void onBindViewHolder(final BaliViewHolder holder,final int position){

    [...]

    holder.root.setOnClickListener(view->listener.onPlaceClicked(holder.root,TransitionUtils.getRecyclerViewTransitionName(position),position));

    /*
    譯者注:此處是 lamda 表示式,一種便捷的匿名函式語法。等同於
    holder.root.setOnClickListener( new OnClickListener(View view) {
            listener.onPlaceClicked(holder.root,TransitionUtils.getRecyclerViewTransitionName(position),position);
        }
    );

    AS 裡這麼寫需要 2.4 及以上版本,或者第三方的庫。
    推薦小姐姐翻譯的文章:https://github.com/xitu/gold-miner/pull/1578/files
    */

}複製程式碼

如上所見,我們在 holder 的根節點上設定了點選事件( onClickListener),在本專案中,這個根節點就是 CardView 。我們也把它作為第一個引數傳進了 onPlaceClicked
方法。第二個引數是一個固定格式的轉場動畫名字 —— 只是簡單的用位置命名。這麼做的原因是我們需要區分哪個 RecycleView 的 item 需要轉場動畫。每一個名字的格式都是相同的:

Java

public static String getRecyclerViewTransitionName(final int position){

    return DEFAULT_TRANSITION_NAME + position;

}複製程式碼

最後一個引數,傳入了被點選 item 的位置(position)。我們會用同樣的資料集合去填充 RecycleView item 和 DetailsLayout,所以需要通過 position 獲得具體的 item。下面我們會看到 OnPlaceClickListener 和 BaliViewHolder:

Java

interface OnPlaceClickListener{

    void onPlaceClicked(View sharedView,String transitionName,final int position);

}複製程式碼
Java

static class BaliViewHolder extends RecyclerView.ViewHolder{



    @BindView(R.id.title)TextView title;

    @BindView(R.id.price)TextView price;

    @BindView(R.id.opening_hours)TextView openingHours;

    @BindView(R.id.root)CardView root;

    @BindView(R.id.headerImage)ImageView placePhoto;



    BaliViewHolder(finalView itemView){

        super(itemView);

        ButterKnife.bind(this,itemView);

    }

}複製程式碼

含有有 RecycleView 和 Map 的DetailsFragment 實現了 OnPlaceClickListener 介面。讓我們看一下具體的 onPlaceClicked 方法:

Java

@Override

public void onPlaceClicked(final View sharedView,final String transitionName,final int position){

    currentTransitionName=transitionName;

    detailsScene=DetailsLayout.showScene(getActivity(),containerLayout,sharedView,transitionName,baliPlaces.get(position));

    drawRoute(position);

    hideAllMarkers();

}複製程式碼

在最開始,我們將 currentTransitionName 儲存為一個全域性變數 —— 當隱藏 DetailsLayout 的場景(scene) 時就會用到它了。同時我們還將這個場景物件賦值給 detailsScene 變數 —— 該變數負責正確的處理 onBackPressed 方法。下一步,我們會繪製一條我們的位置到目標位置的路徑;同時,我們需要隱藏地圖上所有的標記。

我們最關心的部分是如何展示這些場景,看看 DetailsLayout 是怎麼做的吧!

使用場景(Scene Framework)來建立共享的轉場動畫

在下面是自定義的 CoordinatorLayout。一眼看上去它非常普通,但是多了兩個特別的靜態方法 showScenehideScene。讓我們再更仔細的看一下它:


public class DetailsLayout extends CoordinatorLayout{



    @BindView(R.id.cardview)
    CardView cardViewContainer;

    @BindView(R.id.headerImage)
    ImageView imageViewPlaceDetails;

    @BindView(R.id.title)
    TextView textViewTitle;

    @BindView(R.id.description)
    TextView textViewDescription;



    public DetailsLayout(final Context context){

        this(context,null);

    }



    public DetailsLayout(final Context context,final AttributeSet attrs){

        super(context,attrs);

    }



    @Override

    protected void onFinishInflate(){

        super.onFinishInflate();

        ButterKnife.bind(this);

    }



    private void setData(Place place){

        textViewTitle.setText(place.getName());

        textViewDescription.setText(place.getDescription());

    }



    public static Scene showScene(Activity activity,final ViewGroup container,final View sharedView,final String transitionName,final Place data){

        DetailsLayout detailsLayout=(DetailsLayout)activity.getLayoutInflater().inflate(R.layout.item_place,container,false);

        detailsLayout.setData(data);



        TransitionSet set=new ShowDetailsTransitionSet(activity,transitionName,sharedView,detailsLayout);

        Scene scene=new Scene(container,(View)detailsLayout);

        TransitionManager.go(scene,set);

        return scene;

    }



    public static Scene hideScene(Activity activity,final ViewGroup container,final View sharedView,final String transitionName){

        DetailsLayout detailsLayout=(DetailsLayout)container.findViewById(R.id.bali_details_container);



        TransitionSet set=new HideDetailsTransitionSet(activity,transitionName,sharedView,detailsLayout);

        Scene scene=new Scene(container,(View)detailsLayout);

        TransitionManager.go(scene,set);

        return scene;

    }

}複製程式碼

最開始我們先渲染了 DetailsLayout。接下來,我們新增了一些資料(詳情頁的標題和描述)。最後我們建立了轉場動畫 —— 為了我們的目的,我建立了一個單獨的類來保持程式碼空間乾淨整潔。第三步建立了一個場景物件 —— 我們傳遞了渲染好的 detailsLayoutcontainerView (DetailsFragment 主要的 ViewGroup —— 在我們的專案中,這是覆蓋整個螢幕並且有一個 RecycleView 作為子元素的 FrameLayout)。我們只需要呼叫 TransitionManager.go(scene, transitionSet) 方法就能建立酷炫的效動畫果:

[譯]Workcation App – 第四部分. 場景(Scenes)和 RecyclerView 的共享元素轉場動畫(Shared Element Transition)

魔法出現了。TransitionManager 是一個當場景發生改變時啟動轉場動畫的類。通過簡單的呼叫 TransitionManager.go(scene, transitionSet) ,我們可以轉到擁有特定轉場動畫的特定的場景。在我們的專案中,通過使用 TransitionManager 就可以上面那種展示含有詳情和旅途描述的 DetailsLayout 了。現在讓我們看一下如何實現 ShowDetailsTransitionSet 吧。

使用 TransitionBuiler 建立自定義的 TransitionSet

為了保持程式碼整潔,我建立了一個 TransitionBuilder —— 一個尊遵從 builder 模式的類,該類允許我們用少量的程式碼建立一個轉場動畫, 尤其是共享元素轉場動畫。它看起來像是這個樣子的:

Java

public class TransitionBuilder{



    private Transition transition;



    public TransitionBuilder(final Transition transition){

        this.transition=transition;

    }



    public TransitionBuilder duration(long duration){

        transition.setDuration(duration);

        return this;

    }



    public TransitionBuilder target(View view){

        transition.addTarget(view);

        return this;

    }



    public TransitionBuilder target(Classclazz){

        transition.addTarget(clazz);

        return this;

    }



    publicTransitionBuilder target(Stringtarget){

        transition.addTarget(target);

        return this;

    }



    public TransitionBuilder target(int targetId){

        transition.addTarget(targetId);

        return this;

    }



    public TransitionBuilder delay(long delay){

        transition.setStartDelay(delay);

        return this;

    }



    public TransitionBuilder pathMotion(PathMotion motion){

        transition.setPathMotion(motion);

        return this;

    }



    public TransitionBuilder propagation(TransitionPropagation propagation){

        transition.setPropagation(propagation);

        return this;

    }



    public TransitionBuilder pair(Pair<View,String> pair){

        pair.first.setTransitionName(pair.second);

        transition.addTarget(pair.second);

        return this;

    }



    publicTransitionBuilder excludeTarget(finalView view,finalbooleanexclude){

        transition.excludeTarget(view,exclude);

        return this;

    }



    public TransitionBuilder excludeTarget(final String targetName,final boolean exclude){

        transition.excludeTarget(targetName,exclude);

        return this;

    }



    public TransitionBuilder link(final View from,final View to,final String transitionName){

        from.setTransitionName(transitionName);

        to.setTransitionName(transitionName);

        transition.addTarget(transitionName);

        return this;

    }



    public Transition build(){

        return transition;

    }

}複製程式碼

好了,現在我們可以開始編寫 ShowDetailsTransitionSet 了,正是這個類實現了酷炫的轉場效果。在建構函式中,我們傳遞了一個上下文物件,轉場名 —— 就是以 RecyclerView 的 item 的位置命名的那個,轉場開始的View物件以及轉場結束的DetailsLayout。我們還呼叫了 addTransition 方法,通過該方法傳遞了通過 TransitionBuilder 的具體的方法 —— textResize(), slide()shared() —— 建立的轉場動畫。

Java

class ShowDetailsTransitionSet extends TransitionSet{

    private static final String TITLE_TEXT_VIEW_TRANSITION_NAME="titleTextView";

    private static final StringCARD_VIEW_TRANSITION_NAME="cardView";

    private final String transitionName;

    private final View from;

    private final DetailsLayout to;

    private final Context context;



    ShowDetailsTransitionSet(final Context ctx,final String transitionName,final View from,final DetailsLayout to){

        context=ctx;

        this.transitionName=transitionName;

        this.from=from;

        this.to=to;

        addTransition(textResize());

        addTransition(slide());

        addTransition(shared());

    }



    private String titleTransitionName(){

        return transitionName + TITLE_TEXT_VIEW_TRANSITION_NAME;

    }



    private String cardViewTransitionName(){

        return transitionName + CARD_VIEW_TRANSITION_NAME;

    }



    private Transition textResize(){

        return new TransitionBuilder(newTextResizeTransition())

                .link(from.findViewById(R.id.title),to.textViewTitle,titleTransitionName())

                .build();

    }



    private Transition slide(){

        return new TransitionBuilder(TransitionInflater.from(context).inflateTransition(R.transition.bali_details_enter_transition))

                .excludeTarget(transitionName,true)

                .excludeTarget(to.textViewTitle,true)

                .excludeTarget(to.cardViewContainer,true)

                .build();

    }



    private Transition shared(){

        return new TransitionBuilder(TransitionInflater.from(context).inflateTransition(android.R.transition.move))

                .link(from.findViewById(R.id.headerImage),to.imageViewPlaceDetails,transitionName)

                .link(from,to.cardViewContainer,cardViewTransitionName())

                .build();

    }

}複製程式碼

所以,總結一下上面做的事情。

  1. 讓RecyclerView item 的標題執行了 SharedElementTransition 中的 TextResize 動畫(這是一個特定的專案,這裡有詳細解釋)。

  2. 整個佈局執行了一個滑動的轉場動畫,實現了某種意義上的延遲載入。

  3. RecycleView 的item 的標題和內容有一個共享元素轉場動畫(Shared Element Transition),它實現了Android 框架預設的轉場動畫 —— Move transition。
    ```
    XHTML

<?xml version="1.0"encoding="utf-8"?>

<slide

    android:slideEdge="bottom"

    android:interpolator="@android:interpolator/decelerate_cubic">

    <targets>

        <target android:targetId="@id/descriptionLayout" />

    </targets>

</slide>



<slide

    android:slideEdge="bottom"

    android:interpolator="@android:interpolator/decelerate_cubic"

    android:startDelay="100">

    <targets>

        <target android:targetId="@id/description" />

    </targets>

</slide>



<fade

    android:interpolator="@android:interpolator/decelerate_cubic"

    android:startDelay="100">

    <targets>

        <target android:targetId="@id/description" />

    </targets>

</fade>



<slide

    android:slideEdge="bottom"

    android:interpolator="@android:interpolator/decelerate_cubic"

    android:startDelay="200">

    <targets>

        <target android:targetId="@id/takeMe" />

    </targets>

</slide>複製程式碼


通過這些不同的轉場動畫,我們就可以為我們的佈局建立進入的效果。

![](https://www.thedroidsonroids.com/wp-content/uploads/2017/04/ezgif.com-video-to-gif-1.gif?x77083)

在我看起來真是碉堡了!但是返回怎麼辦呢?看下面。

## 返回上一步場景,處理 **onBackPress**

如果你還記得的話,我們在 DetailsLayout 中寫了兩個方法 —— *showScene* 和 *hideScene*。我們已經寫了第一個方法,但是第二個方法是什麼樣的呢?讓我們繼續把它也寫完吧。複製程式碼

public static Scene hideScene(Activity activity,final ViewGroup container,final View sharedView,final String transitionName){

DetailsLayout detailsLayout=(DetailsLayout)container.findViewById(R.id.bali_details_container);



TransitionSet set=newHideDetailsTransitionSet(activity,transitionName,sharedView,detailsLayout);

Scene scene=newScene(container,(View)detailsLayout);

TransitionManager.go(scene,set);

return scene;複製程式碼

}


現在,有一些小的改變。既然在 DetailsFragment 容器(之前提到的那個 FrameLayout) 上面新增了一個 DetailsLayout ,所以為了獲得 DetailsLayout,我們還得在容器裡呼叫 **findViewById**。然後我們必須建立特定的物件和轉場,編寫特定的設定。為此,我也寫了另一個類來繼承 TransitionSet —— HideDetailsTransitionSet。它看起來像是這個樣子的:複製程式碼

Java

class HideDetailsTransitionSet extends TransitionSet{

private static final String TITLE_TEXT_VIEW_TRANSITION_NAME="titleTextView";

private static final String CARD_VIEW_TRANSITION_NAME="cardView";

private final String transitionName;

private final View from;

private final DetailsLayout to;

private final Context context;



HideDetailsTransitionSet(final Context ctx,final String transitionName,final View from,final DetailsLayout to){

    context=ctx;

    this.transitionName=transitionName;

    this.from=from;

    this.to=to;

    addTransition(textResize());

    addTransition(shared());

}



private String titleTransitionName(){

    return transitionName+TITLE_TEXT_VIEW_TRANSITION_NAME;

}



private String cardViewTransitionName(){

    return transitionName+CARD_VIEW_TRANSITION_NAME;

}



private Transition textResize(){

    return newTransitionBuilder(newTextResizeTransition())

            .link(from.findViewById(R.id.title),to.textViewTitle,titleTransitionName())

            .build();

}



private Transition shared(){

    return new TransitionBuilder(TransitionInflater.from(context).inflateTransition(android.R.transition.move))

            .link(from.findViewById(R.id.headerImage),to.imageViewPlaceDetails,transitionName)

            .link(from,to.cardViewContainer,cardViewTransitionName())

            .build();

}複製程式碼

}
```

在這個專案,我們又一次編寫了 textResize()shared() 。如果你仔細檢查兩個方法的話,你會發現 TranstionBuilder 有 link()
方法。這種方法接收了3個引數 —— 源頭 view、目標 view 和動畫名字。它把轉場動畫的名字新增給了 源頭 View 和目標 view,就像把它指定到了一個轉場物件上。所以它用來“連線(link)” 兩個view。

剩下的部分就一樣啦,我們又建立了一個場景物件,呼叫 TransitionManager.go() 然後哈利路亞~我們就可以返回之前的狀態了。

結語

如我們所見 —— 思考永無止境(the sky’s the limit)!我們可以為 activities、fragments 甚至 layouts 創造有意義的轉場動畫。場景和轉場動畫十分流弊,增進了使用者介面和 使用者體驗。這種解決方案有什麼好處呢?首先,我們不需要在關注另一個生命週期。其次,有許多第三方的庫幫我們建立不需要 fragment 的使用者介面。通過部署場景和轉場動畫,我們可以開發出一個非常不錯的 app。第三,該方案很少見,但是確實讓我們能更多的控制效果如何實現。

就這麼多了。非常感謝閱讀這一系列的文章,希望你喜歡它!

鐵甲依然在!(譯者:咳咳,原文是 See you soon!)

Mariusz Brona aka panwrona


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃

相關文章