原文地址: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,效果如下: