探索如何經過場景框架(Scene Framework)建立展現詳情頁的共享元素轉場動畫(Shared Element Transition)。javascript
歡迎閱讀本系列文章的第四篇也是最後一篇,此係列文章和我前一段時間完成的「研發」項目有關。在文章裏,我會針對開發中遇到的動畫問題分享一些解決辦法。在這篇博文裏,我會編寫最後的部分:如何經過場景框架(Scene Framework)建立展現詳情頁的共享元素轉場動畫(Shared Element Transition)。前端
Part 1: 自定義 Fragment 轉場java
Part 2: 帶有動畫的標記(Animating Markers) 與 MapOverlayLayout react
Part 3: 帶有動畫的標記(Animated Markers) 與 RecyclerView 的互動android
Part 4: 場景(Scenes)和 RecyclerView 的共享元素轉場動畫(Shared Element Transition)ios
項目的 Git 地址: Workcation Appgit
動畫的 Dribbble 地址: dribbble.com/shots/28812…github
幾個月前咱們開了一個部門會議,在會議上個人朋友 Paweł Szymankiewicz 給我演示了他在本身的「研發」項目上製做的動畫。我很是喜歡這個動畫,在開完會之後我準備把用代碼實現它。我可沒想到到我會攤上啥...後端
GIF 1 「動畫效果」app
就像上面 GIF 動畫展現的,須要作的事情有不少。
在點擊底部菜單欄最右方的菜單後,咱們會跳轉到一個新界面。在此界面中,地圖經過縮放和漸顯的轉場動畫在屏幕上方加載,Recycleview 的 item 隨着轉場動畫從底部加載,地圖上的標記點在轉場動畫執行的同時被添加到地圖上.
當滑動底部的 RecycleView item 的時候,地圖上的標記會經過閃爍來顯示它們的位置(譯者注:原文是show their position on the map,我的認爲 position 有兩層含義:一表明標記在地圖上的位置,二表明標記所對應的 item 在 RecycleView 裏的位置。)
在點擊一個 item 之後,咱們會進入到新界面。在此界面中,地圖經過動畫方式來顯示出路徑以及起始/結束標記。同時此 RecyclerView 的item 會經過轉場動畫展現一些關於此地點的描述,背景圖片也會放大,還附有更詳細的信息和一個按鈕。
當後退時,詳情頁經過轉場變成普通的 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 的 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 是怎麼作的吧!
在下面是自定義的 CoordinatorLayout。一眼看上去它很是普通,可是多了兩個特別的靜態方法 showScene 和 hideScene。讓咱們再更仔細的看一下它:
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。接下來,咱們添加了一些數據(詳情頁的標題和描述)。最後咱們建立了轉場動畫 —— 爲了咱們的目的,我建立了一個單獨的類來保持代碼空間乾淨整潔。第三步建立了一個場景對象 —— 咱們傳遞了渲染好的 detailsLayout 和 containerView (DetailsFragment 主要的 ViewGroup —— 在咱們的項目中,這是覆蓋整個屏幕而且有一個 RecycleView 做爲子元素的 FrameLayout)。咱們只須要調用 TransitionManager.go(scene, transitionSet) 方法就能建立酷炫的效動畫果:
魔法出現了。TransitionManager 是一個當場景發生改變時啓動轉場動畫的類。經過簡單的調用 TransitionManager.go(scene, transitionSet) ,咱們能夠轉到擁有特定轉場動畫的特定的場景。在咱們的項目中,經過使用 TransitionManager 就能夠上面那種展現含有詳情和旅途描述的 DetailsLayout 了。如今讓咱們看一下如何實現 ShowDetailsTransitionSet 吧。
爲了保持代碼整潔,我建立了一個 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();
}
}複製代碼
因此,總結一下上面作的事情。
讓RecyclerView item 的標題執行了 SharedElementTransition 中的 TextResize 動畫(這是一個特定的項目,這裏有詳細解釋)。
整個佈局執行了一個滑動的轉場動畫,實現了某種意義上的延遲加載。
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
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃。