Fragment自定義過渡動畫

原文地址:https://www.thedroidsonroids.com/blog/android/workcation-app-part-1-fragments-custom-transition/

項目地址: https://github.com/panwrona/Workcation

效果圖
效果圖

正如我們上面看到的,有很多東西需要介紹:

1、在點擊底部的菜單條目之后,我們進入了下一個界面,我們可以看到從頂部加載了一些縮放/淡入淡出的動畫,RecyclerView條目從底部加載詳情,標記使用淡入淡出的效果被添加到了地圖上。

2、當滑動RecyclerView條目的時候,標記會顯示他們的位置

3、當點擊某一個條目的時候,我們可以轉換到下一個界面,地圖將會顯示路線和開始/結束標記,Recyclerview的條目被轉換成顯示一些描述信息,更大的圖片,旅行詳細信息和按鈕等。

4、當點擊返回按鈕時,過渡動畫再次發生回到RecyclerView的條目上,所有的標記將再次出現,路線圖消失。

The problem

正如我們在上面那個GIF圖中看到的那樣,他看起來就像在動畫顯示到正確的位置之前地圖已經加載完畢了,這是不會發生在現實世界中的,他看起來就像:

gif2
gif2

解決方案

1、提前加載地圖
2、當地圖已經加載完畢的時候,使用Google地圖api獲取到地圖的bitmap,然后存入到緩存中。
3、當進入到詳情頁面的時候創建自定義的過渡和淡入淡出的動畫。

開始實現

為了實現這個,我們需要從已經加載好的地圖中獲取地圖的快照,當然我們不能再DetailsFragment中做到這一點,如果我們想要在屏幕中平滑過渡,我們要做的是在HomeFragment中獲取位圖并保存到緩存中,正如你看到的那樣,地圖和底部有一些間距,所以我們必須要適應地圖的大小。

<?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會替換上面的layout,它允許我們加載用戶不可見的地圖。

public class MainActivity extends MvpActivity<MainView, MainPresenter> implements MainView, OnMapReadyCallback {
 
    SupportMapFragment mapFragment;
    private LatLngBounds 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(使用了Mosby Framework),整個項目使用了MVP模式,提到的這個庫是非常容易實現MVP的。

在onCreate方法中我們做了三件事:

1、我們為地圖提供了經緯度,將會被用于設置在地圖上。
2、用HomeFragment替換布局中的container
3、我們為MapFragment設置onMapReadyCallback;

map加載完畢之后,onMapReady()方法將會被調用,我們可以做一些操作將正確加載的map保存到bitmap中,我們可以使用CameraUpdateFactory.newLatLngBounds()方法將camera移到最初提供的LatLngBounds,在我們的例子中,我們在下一個界面中需要知道地圖的確切尺寸,因此我們將寬度和高度傳遞給此方法,然后計算他們:

public static int calculateWidth(final WindowManager windowManager) {
    DisplayMetrics metrics = new DisplayMetrics();
    windowManager.getDefaultDisplay().getMetrics(metrics);
    return metrics.widthPixels;
}
public static int calculateHeight(final WindowManager windowManager, final int paddingBottom) {
    DisplayMetrics metrics = new DisplayMetrics();
    windowManager.getDefaultDisplay().getMetrics(metrics);
    return metrics.heightPixels - paddingBottom;
}

很簡單,當googleMap.removeCamera()方法被調用之后,我們將會設置OnMapLoadedCallback方法,當camera移到要求的位置之后,onMapLoaded()方法就會被調用,然后我們將從中獲取bitmap圖片。

獲取位圖并保存到緩存中

onMapLoaded()方法只有一個任務要做,從地圖拍攝快照然后調用presenter.saveBitmap(),使用lambda表達式,我們將代碼簡化為一行。

googleMap.setOnMapLoadedCallback(() -> googleMap.snapshot(presenter::saveBitmap));

presenters代碼非常簡單,只是保存bitmap到緩存中。

@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 static MapBitmapCache instance() {
        if(sInstance == null) {
            sInstance = new MapBitmapCache(DEFAULT_CACHE_SIZE);
            return sInstance;
        }
        return sInstance;
    }
 
    public Bitmap getBitmap() {
        return get(KEY);
    }
 
    public void putBitmap(Bitmap bitmap) {
        put(KEY, bitmap);
    }
 
    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value == null ? 0 : value.getRowBytes() * value.getHeight() / 1024;
    }
}

所以我們將圖片保存到緩存中,我們唯一要做的就是在進入DetailsFragment時為地圖設置縮放和淡入淡出的效果。

為map自定義縮放和淡入淡出的過渡效果

最精彩的部分來了,這些代碼很簡單,會為我們展示很棒的東西:

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(new DecelerateInterpolator());
    }
 
    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 int factor) {
        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);
    }
}

我們在這個轉換中所做的是,我們縮小ImageView的scaleX和scaleY屬性,從scaleFactor到所需的視圖縮放,因此,換句話說,我們首先通過scaleFactor增加寬度和高度,然后我們將他所當道所需的大小。

創建自定義的過渡動畫

為了創建自定義的過渡,我們需要繼承Transition類,然后下一步就是覆蓋captureStartValues和captureEndValues方法,發生了什么呢?

過渡框架使用屬性動畫API在view的開始和結束屬性值之間進行動畫,如果你不熟悉這個,可以看一下這篇文章,如果我們想縮小我們的圖片,所以startValue是scaleFactor,而endValue就是所需的scaleX和scaleY。

如何傳遞這些值,如前所述,我們可以在CaptureStart和captureEnd方法中作為參數傳遞給TransitionValues對象,

使用捕獲的值,我們需要覆蓋createAnimator()方法,在這個方法中,我們返回Animator對象,該對象會在view的屬性值之間變化,所以在我們的例子中,我們返回返回的AnimatorSet將改變view的比例和透明度,我們希望我們的過渡動畫只為ImageView工作,所以我們檢查作為參數傳遞進來了的TransitionValues對象的視圖引用是否是ImageView實例。

應用自定義過渡動畫

我們把位圖存儲在內存中,轉換動畫也已經創建了,所以我們還有最后一步,將過渡動畫應用到我們的fragment上,我喜歡使用靜態方法來創建fragment和activity,他看起來很不錯,并幫我們保持代碼很干凈。

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;
}

正如你看到的一樣實現起來很簡單,我們創建了我們的過渡動畫的實例

<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,效果如下:

效果圖
效果圖
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容