原文地址: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圖中看到的那樣,他看起來就像在動畫顯示到正確的位置之前地圖已經加載完畢了,這是不會發生在現實世界中的,他看起來就像:

解決方案
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,效果如下:
