[譯]Workcation App – 第一部分 . 自定義 Fragment 轉場動畫

Workcation App – 第一部分 . 自定義 Fragment 轉場動畫

歡迎閱讀本系列文章的第一篇,此係列文章和我前一段時間完成的「研發」項目有關。在文章裏,我會針對開發中遇到的動畫問題分享一些解決辦法。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 動畫展現的,須要作的事情有不少。後端

  1. 在點擊底部菜單欄最右方的菜單後,咱們會跳轉到一個新界面。在此界面中,地圖經過縮放和漸顯的轉場動畫在屏幕上方加載,Recycleview 的 item 隨着轉場動畫從底部加載,地圖上的標記點在轉場動畫執行的同時被添加到地圖上.

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

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

  4. 當後退時,詳情頁經過轉場變成普通的 RecycleView Item,全部的地圖標記再次顯示,同時路徑一塊兒消失。

就這麼多啦,這就是我準備在這一系列文章中向你展現的東西。在本文中我會編寫進入地圖 fragment 的轉場動畫。

難點

就像咱們在 GIF 1 裏看到的那樣,看起來好像地圖在移動到正確地點以前已經加載完畢了。這在真實世界裏是不可能的,它其實是這個樣子的:

需求

  1. 預加載地圖

  2. 加載完畢後,使用 Google Map API 得到地圖的快照圖片(bitmap)並保存在緩存中。

  3. 爲地圖編寫一個包含縮放與漸顯的自定義轉場動畫(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 方法裏咱們作了三件事:

  1. 爲地圖提供了 LatLngBounds,他們會被用來設置地圖的邊界。

  2. 在 activity 的佈局里加載了 HomeFragment

  3. 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 類。而後重寫 captureStartValuescaptureEndValues 方法。猜猜發生了啥?

Transition 框架使用了屬性動畫的 API ,經過改變 view 開始和結束時的屬性值來產生動畫。若是你不熟悉屬性動畫,強烈推薦閱讀這篇文章。就像剛纔解釋的那樣,咱們要縮放圖片。開始值是 scaleFactor ,結束值是指望 scaleX 和 scaleY的值,一般狀況下是1。

怎麼傳遞這些值呢?如前所述,很簡單。咱們把 TransitionValues 對象看成參數傳進 captureStartcaptureEnd 方法裏。它包括一個 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 更新。若是有疑問的話,歡迎評論。固然若是發現這些博文頗有趣,不要忘記分享噢。


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

相關文章
相關標籤/搜索