- 原文地址:Workcation App – Part 1. Fragment custom transition
- 原文做者:Mariusz Brona
- 譯文出自:掘金翻譯計劃
- 譯者:龍騎將楊影楓
- 校對者:Vivienmm、張拭心
歡迎閱讀本系列文章的第一篇,此係列文章和我前一段時間完成的「研發」項目有關。在文章裏,我會針對開發中遇到的動畫問題分享一些解決辦法。javascript
Part 1: 自定義 Fragment 轉場html
Part 2: Animating Markers 與 MapOverlayLayout 前端
Part 3: RecyclerView 互動 與 Animated Markersjava
Part 4: 場景(Scenes)和 RecyclerView 的共享元素轉場動畫(Shared Element Transition)react
項目的 Git 地址: Workcation Appandroid
動畫的 Dribbble 地址: dribbble.com/shots/28812…ios
幾個月前咱們開了一個部門會議,在會議上個人朋友 Paweł Szymankiewicz 給我演示了他在本身的「研發」項目上製做的動畫。我很是喜歡這個動畫,會後決定用代碼實現它。我可沒想到到我會攤上啥...git
GIF 1 「動畫效果」github
就像上面 GIF 動畫展現的,須要作的事情有不少。後端
在點擊底部菜單欄最右方的菜單後,咱們會跳轉到一個新界面。在此界面中,地圖經過縮放和漸顯的轉場動畫在屏幕上方加載,Recycleview 的 item 隨着轉場動畫從底部加載,地圖上的標記點在轉場動畫執行的同時被添加到地圖上.
當滑動底部的 RecycleView item 的時候,地圖上的標記會經過閃爍來顯示它們的位置(譯者注:原文是show their position on the map,我的認爲 position 有兩層含義:一表明標記在地圖上的位置,二表明標記所對應的 item 在 RecycleView 裏的位置。)
在點擊一個 item 之後,咱們會進入到新界面。在此界面中,地圖經過動畫方式來顯示出路徑以及起始/結束標記。同時此 RecyclerView 的item 會經過轉場動畫展現一些關於此地點的描述,背景圖片也會放大,還附有更詳細的信息和一個按鈕。
當後退時,詳情頁經過轉場變成普通的 RecycleView Item,全部的地圖標記再次顯示,同時路徑一塊兒消失。
就這麼多啦,這就是我準備在這一系列文章中向你展現的東西。在本文中我會編寫進入地圖 fragment 的轉場動畫。
就像咱們在 GIF 1 裏看到的那樣,看起來好像地圖在移動到正確地點以前已經加載完畢了。這在真實世界裏是不可能的,它其實是這個樣子的:
預加載地圖
加載完畢後,使用 Google Map API 得到地圖的快照圖片(bitmap)並保存在緩存中。
爲地圖編寫一個包含縮放與漸顯的自定義轉場動畫(transition),進入 DetailsFragment 的時候就激活。
爲了實現上述目標,咱們首先從已加載的地圖上拿到一份快照(snapshot)。固然咱們若是想把轉場動畫作的更平滑一點,確定不能等進入 DetailsFragment 後才獲取。因此要怎麼作呢?固然是是悄悄的在 HomeFragment 裏拿到這個圖片(bitmap) 而且保存在緩存裏啦。地圖距離底部還有一點距離(margin),因此咱們拿到的圖片必須知足"未來的"地圖尺寸。
XHTML
<?xml version="1.0"encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:MContext=".screens.main.MainActivity">
<fragment
android:id="@+id/mapFragment"
class="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="@dimen/map_margin_bottom"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/white">
...
...
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>複製代碼
就像上面代碼展現的那樣,MapFragment 被放在佈局的最下方,這樣咱們就能夠在用戶看不到地方加載地圖。
public class MainActivity extends MvpActivity<MainView,MainPresenter> implements MainView,OnMapReadyCallback{
SupportMapFragment mapFragment;
privateLatLngBounds mapLatLngBounds;
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
presenter.provideMapLatLngBounds();
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.container,HomeFragment.newInstance(),HomeFragment.TAG)
.addToBackStack(HomeFragment.TAG)
.commit();
mapFragment=(SupportMapFragment)getSupportFragmentManager().findFragmentById(R.id.mapFragment);
mapFragment.getMapAsync(this);
}
@Override
public void setMapLatLngBounds(final LatLngBounds latLngBounds){
mapLatLngBounds=latLngBounds;
}
@Override
public void onMapReady(final GoogleMap googleMap){
googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(
mapLatLngBounds,
MapsUtil.calculateWidth(getWindowManager()),
MapsUtil.calculateHeight(getWindowManager(),getResources().getDimensionPixelSize(R.dimen.map_margin_bottom)),
MapsUtil.DEFAULT_ZOOM));
googleMap.setOnMapLoadedCallback(()->googleMap.snapshot(presenter::saveBitmap));
}
}複製代碼
MainActivity 繼承自 MvpActivity,而 MvpActivity 是來自 Hannes Dorfmann 寫的 Mosby Framework。個人項目都聽從 MVP 模式,而這個框架是一個 MVP 模式的很是好的實現。
在 onCreate 方法裏咱們作了三件事:
爲地圖提供了 LatLngBounds,他們會被用來設置地圖的邊界。
在 activity 的佈局里加載了 HomeFragment
爲 Mapfragment 設置了 OnMapReadyCallback 的回調。
當地圖加載完畢時,就會調用 onMapReady() 方法,咱們就能夠經過一些操做把當前加載的地圖轉換成 bitmap 圖片。經過 CameraUpdateFactory.newLatLngBounds() 方法,咱們能夠把鏡頭轉到以前提供的 LatLngBounds 上。這樣的話咱們就精確的知道下個頁面的地圖區域,再把屏幕寬度和高度看成參數傳入 onMapReady() 方法,像這樣操做:
public static int calculateWidth(final WindowManager windowManager){
DisplayMetrics metrics=newDisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(metrics);
returnmetrics.widthPixels;
}
public static int calculateHeight(final WindowManager windowManager,finalintpaddingBottom){
DisplayMetrics metrics=newDisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(metrics);
returnmetrics.heightPixels-paddingBottom;複製代碼
很簡單吧?在調用 googleMap.moveCamera() 方法之後,咱們設置 OnMapLoadedCallback 的回調。當鏡頭移動到正確的位置的時候,調用 onMapLoaded() 方法,咱們準備好在此處截圖了。
onMapLoaded() 方法只作一件事 —— 在從地圖上得到快照後調用 presenter.saveBitmap() 方法。多虧 lambda 表達式,咱們能夠縮短代碼到一行。(譯者注:有關 lamb 表達式,推薦搭配此文章一塊兒食用。)
googleMap.setOnMapLoadedCallback(()->googleMap.snapshot(presenter::saveBitmap));複製代碼
此 presenter (譯者注:MVP 裏的 P) 的代碼很是簡單,它只是把圖片保存在緩存裏。
@Override
public void saveBitmap(final Bitmap bitmap){
MapBitmapCache.instance().putBitmap(bitmap);
}
public class MapBitmapCache extends LruCache<String,Bitmap>{
private static final int DEFAULT_CACHE_SIZE=(int)(Runtime.getRuntime().maxMemory()/1024)/8;
public static final String KEY="MAP_BITMAP_KEY";
private static MapBitmapCache sInstance;
/** * @param maxSize for caches that do not override {@link #sizeOf}, this is * the maximum number of entries in the cache. For all other caches, * this is the maximum sum of the sizes of the entries in this cache. */
private MapBitmapCache(final int maxSize){
super(maxSize);
}
public staticMapBitmapCache instance(){
if(sInstance==null){
sInstance=newMapBitmapCache(DEFAULT_CACHE_SIZE);
returnsInstance;
}
returnsInstance;
}
public Bitmap getBitmap(){
return get(KEY);
}
public void putBitmap(Bitmap bitmap){
put(KEY,bitmap);
}
@Override
protected intsizeOf(String key,Bitmap value){
return value==null ? 0 : value.getRowBytes()*value.getHeight()/1024;
}
}複製代碼
此處我使用了 LruCache ,由於這是比較推薦的作法,這裏有詳細解釋。
如今咱們把bitmap 存到了緩存裏,剩下惟一要作的事情就是自定義一個縮放和漸進效果的轉場動畫。
毛毛雨灑灑水啦~(譯者注: 原文爲 Easy peasy lemon squeezy。是一個比較有意思的、以俏皮的語氣表達「垂手可得」或者「手到擒來」概念的短語。)
下面是最有意思的部分,代碼也炒雞簡單!但就是這部分完成了比較炫酷的事情。
public class ScaleDownImageTransition extends Transition{
private static final int DEFAULT_SCALE_DOWN_FACTOR = 8;
private static final String PROPNAME_SCALE_X="transitions:scale_down:scale_x";
private static final String PROPNAME_SCALE_Y="transitions:scale_down:scale_y";
private Bitmap bitmap;
private Context context;
private int targetScaleFactor = DEFAULT_SCALE_DOWN_FACTOR;
public ScaleDownImageTransition(final Context context){
this.context=context;
setInterpolator(newDecelerateInterpolator());
}
public ScaleDownImageTransition(final Context context,final Bitmap bitmap){
this(context);
this.bitmap=bitmap;
}
public ScaleDownImageTransition(final Context context,final AttributeSet attrs){
super(context,attrs);
this.context=context;
TypedArray array=context.obtainStyledAttributes(attrs,R.styleable.ScaleDownImageTransition);
try{
targetScaleFactor=array.getInteger(R.styleable.ScaleDownImageTransition_factor,DEFAULT_SCALE_DOWN_FACTOR);
}finally{
array.recycle();
}
}
public void setBitmap(final Bitmap bitmap){
this.bitmap=bitmap;
}
public void setScaleFactor(final intfactor){
targetScaleFactor=factor;
}
@Override
public Animator createAnimator(final ViewGroup sceneRoot,final TransitionValues startValues,final TransitionValues endValues){
if(null == endValues){
return null;
}
final View view=endValues.view;
if (view instanceof ImageView){
if (bitmap!=null)
view.setBackground(new BitmapDrawable(context.getResources(),bitmap));
float scaleX=(float)startValues.values.get(PROPNAME_SCALE_X);
float scaleY=(float)startValues.values.get(PROPNAME_SCALE_Y);
float targetScaleX=(float)endValues.values.get(PROPNAME_SCALE_X);
float targetScaleY=(float)endValues.values.get(PROPNAME_SCALE_Y);
ObjectAnimator scaleXAnimator = ObjectAnimator.ofFloat(view,View.SCALE_X,targetScaleX,scaleX);
ObjectAnimator scaleYAnimator = ObjectAnimator.ofFloat(view,View.SCALE_Y,targetScaleY,scaleY);
AnimatorSet set=new AnimatorSet();
set.playTogether(scaleXAnimator,scaleYAnimator,ObjectAnimator.ofFloat(view,View.ALPHA,0.f,1.f));
return set;
}
return null;
}
@Override
public void captureStartValues(TransitionValues transitionValues){
captureValues(transitionValues,transitionValues.view.getScaleX(),transitionValues.view.getScaleY());
}
@Override
public void captureEndValues(TransitionValues transitionValues){
captureValues(transitionValues,targetScaleFactor,targetScaleFactor);
}
private void captureValues(final TransitionValues values,final float scaleX,final float scaleY){
values.values.put(PROPNAME_SCALE_X,scaleX);
values.values.put(PROPNAME_SCALE_Y,scaleY);
}
}複製代碼
在轉場動畫中作了什麼事情呢?咱們用 scaleFactor 對傳入的 imageView 進行了 scaleX 和 scaleY 屬性的縮放(默認是8)。換句話說咱們經過 scaleFactor 先把圖片拉伸,而後再把圖片壓縮回須要的大小。
爲了編寫轉場動畫,咱們必須繼承一個 Transition 類。而後重寫 captureStartValues 和 captureEndValues 方法。猜猜發生了啥?
Transition 框架使用了屬性動畫的 API ,經過改變 view 開始和結束時的屬性值來產生動畫。若是你不熟悉屬性動畫,強烈推薦閱讀這篇文章。就像剛纔解釋的那樣,咱們要縮放圖片。開始值是 scaleFactor ,結束值是指望 scaleX 和 scaleY的值,一般狀況下是1。
怎麼傳遞這些值呢?如前所述,很簡單。咱們把 TransitionValues 對象看成參數傳進 captureStart 和 captureEnd 方法裏。它包括一個 view 的引用和一個能夠保存值的 Map 對象,在咱們的項目中須要保存的值就是 scaleX 和 scaleY。
得到這些值之後,咱們須要重寫 createAnimator() 方法。在這個方法中須要返回一個動態改變 view 屬性的 Animator (或者 AnimatorSet )。本項目中返回的是 AnimatorSet 對象,此對象同時改變一個 view 的尺寸和亮度。同時,由於咱們只但願轉場動畫做用在 ImageView 上,因此經過 instanceof 進行了對象類型校驗,以保證傳入的 view 是一個 ImageView。
咱們已經在緩存中保存了 bitmap 圖片,也已經建立了轉場動畫,因此只剩最後一步 —— 就是爲 fragment 添加轉場動畫。我喜歡寫一個靜態工廠方法來建立 fragments 和 activities 。這麼作可讓咱們保持代碼邏輯清晰,因此也應該用這樣的設計模式來編寫轉場動畫的代碼。
public static Fragment newInstance(final Context ctx){
DetailsFragment fragment = new DetailsFragment();
ScaleDownImageTransition transition=new ScaleDownImageTransition(ctx,MapBitmapCache.instance().getBitmap());
transition.addTarget(ctx.getString(R.string.mapPlaceholderTransition));
transition.setDuration(800);
fragment.setEnterTransition(transition);
return fragment;
}複製代碼
瞧,作起來多簡單。咱們爲轉場動畫實例化了一個新的實例,又經過 xml 爲它添加了 transitionName 的屬性。
<ImageView
android:id="@+id/mapPlaceholder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="@dimen/map_margin_bottom"
android:transitionName="@string/mapPlaceholderTransition"/>複製代碼
而後咱們經過 setEnterTransition() 把fragment 傳遞進去, 看吧!效果出現啦:
你看,最終效果已經很接近像 GIF 那樣從本地加載地圖的效果了。可是最後一幀動畫仍然會有那麼一點閃爍,由於地圖的快照仍是與實際的地圖有點差異。
多謝閱讀,下一部分會在星期二的 7.03 更新。若是有疑問的話,歡迎評論。固然若是發現這些博文頗有趣,不要忘記分享噢。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃。