在开发过程中,免不了需要对activity或Fragment的切换动画进行设置,系统自带的切换效果经常不能满足需求,在google推出的Material Design中也定义了一些界面切换的效果
这里对元素共享进行分析,在github上由takahirom推出的PreLollipopTransition控件能很好的满足需求,并且使用十分简单

首先先看看效果

Android 多个共享元素动画 fragment共享元素动画_bundle

使用方法:

引用库:
compile ‘com.kogitune:pre-lollipop-activity-transition:1.0.0’

布局文件中不需要额外的设置,imageview即可
在第一个界面中跳转时增加代码

Android 多个共享元素动画 fragment共享元素动画_android_02

subActivity为需要跳转的Activity

在subActivity中执行

Android 多个共享元素动画 fragment共享元素动画_bundle_03


sub_imageView为需要展示的控件

这样就能实现跳转时元素共享的效果

在Fragment中使用方法类似:

第一个Fragment传入需要共享的imageview

Android 多个共享元素动画 fragment共享元素动画_控件_04

第二个Fragment接收内容并设置到展示的view

Android 多个共享元素动画 fragment共享元素动画_Android 多个共享元素动画_05

同时在ListView中也能实现效果,方法类似,就不细讲了

现在分析源码:

以Activity为例:
执行的代码只有一句:

ActivityTransitionLauncher.with(MainActivity.this).from(v).launch(intent);

看看ActivityTransitionLauncher方法里面执行了什么操作

public class ActivityTransitionLauncher {
    private static final String TAG = "TransitionLauncher";
    private final Activity activity;
    private View fromView;
    private Bitmap bitmap;

    private ActivityTransitionLauncher(Activity activity) {
        this.activity = activity;
    }

    public static ActivityTransitionLauncher with(Activity activity) {
        return new ActivityTransitionLauncher(activity);
    }

    public ActivityTransitionLauncher from(View fromView) {
        this.fromView = fromView;
        return this;
    }

    public ActivityTransitionLauncher image(Bitmap bitmap) {
        this.bitmap = bitmap;
        return this;
    }

    public void launch(Intent intent) {
        Bundle transitionBundle = TransitionBundleFactory.createTransitionBundle(this.activity, this.fromView, this.bitmap);
        intent.putExtras(transitionBundle);
        this.activity.startActivity(intent);
        //取消系统默认的界面切换动画
        this.activity.overridePendingTransition(0, 0);
    }
}

代码很简单,首先传入上下文,得到当前类的对象,然后初始化需要传递的view控件,最后执行launch方法
实例化一个TransitionBundleFactory的自定义bundle类,将数据传入,执行跳转,最后取消系统默认的界面切换动画
这里我们看到了

public ActivityTransitionLauncher image(Bitmap bitmap) {
        this.bitmap = bitmap;
        return this;
    }

这里传入一个bitmap对象,我们猜测也可以将一个传递任意一个图片到下一个界面执行共享动画

现在我们看看TransitionBundleFactory中执行了什么操作

public class TransitionBundleFactory {
    public static final String TEMP_IMAGE_FILE_NAME = "activity_transition_image.png";
    private static final String TAG = "Transition";

    public TransitionBundleFactory() {
    }

    public static Bundle createTransitionBundle(Context context, View fromView, Bitmap bitmap) {
        String imageFilePath = null;
        if(bitmap != null) {
            imageFilePath = saveImage(context, bitmap);
        }

        int[] screenLocation = new int[2];
        fromView.getLocationOnScreen(screenLocation);
        TransitionData transitionData = new TransitionData(context, screenLocation[0], screenLocation[1], fromView.getMeasuredWidth(), fromView.getMeasuredHeight(), imageFilePath);
        return transitionData.getBundle();
    }

    private static String saveImage(Context context, Bitmap bitmap) {
        String imageSavePath = context.getFilesDir().getAbsolutePath() + "/activity_transition/";
        (new File(imageSavePath)).mkdirs();
        File imageFile = new File(imageSavePath, "activity_transition_image.png");
        String imageFilePath = imageFile.getAbsolutePath();
        Boolean isDebug = (Boolean)BuildConfigUtils.getBuildConfigValue(context, "DEBUG");
        BufferedOutputStream bos = null;

        try {
            if(imageFile.exists()) {
                imageFile.delete();
            }

            imageFile.createNewFile();
            bos = new BufferedOutputStream(new FileOutputStream(imageFile));
            bitmap.compress(CompressFormat.JPEG, 50, bos);
        } catch (FileNotFoundException var18) {
            if(isDebug.booleanValue()) {
                Log.i("Transition", "file not found", var18);
            }
        } catch (IOException var19) {
            if(isDebug.booleanValue()) {
                Log.i("Transition", "can\'t create file", var19);
            }
        } finally {
            try {
                bos.close();
            } catch (Exception var17) {
                if(isDebug.booleanValue()) {
                    Log.i("Transition", "fail save image", var17);
                }
            }

        }

        TransitionAnimation.bitmapCache = new WeakReference(bitmap);
        return imageFilePath;
    }
}

果然,在createTransitionBundle方法中判断是否传入一个Bitmap对象,如果传入就将该对象保存到/data/data/包名/activity_transition/ 目录中

在createTransitionBundle中调用TransitionData方法,并且将坐标,控件宽高,图片地址传入,最后返回bundle,很明显就是将数据通过Bundle进行封装,我们看看代码

public class TransitionData {
    public static final String EXTRA_IMAGE_LEFT = ".left";
    public static final String EXTRA_IMAGE_TOP = ".top";
    public static final String EXTRA_IMAGE_WIDTH = ".width";
    public static final String EXTRA_IMAGE_HEIGHT = ".height";
    public static final String EXTRA_IMAGE_PATH = ".imageFilePath";

    public final int thumbnailTop;
    public final int thumbnailLeft;
    public final int thumbnailWidth;
    public final int thumbnailHeight;
    public final String imageFilePath;
    private String appid;


    public TransitionData(Context context,int thumbnailLeft,int thumbnailTop,int thumbnailWidth,int thumbnailHeight,String imageFilePath){
        this.setAppid(context);
        this.thumbnailLeft = thumbnailLeft;
        this.thumbnailTop = thumbnailTop;
        this.thumbnailWidth = thumbnailWidth;
        this.thumbnailHeight = thumbnailHeight;
        this.imageFilePath = imageFilePath;

    }

    public TransitionData(Context context,Bundle bundle){
        this.setAppid(context);
        this.thumbnailTop = bundle.getInt(this.appid+".top");
        this.thumbnailLeft = bundle.getInt(this.appid + ".left");
        this.thumbnailWidth = bundle.getInt(this.appid + ".width");
        this.thumbnailHeight = bundle.getInt(this.appid + ".height");
        this.imageFilePath = bundle.getString(this.appid + ".imageFilePath");
    }


    private void setAppid(Context context){
        this.appid = (String) BuildConfigUtils.getBuildConfigValue(context,"APPLICATION_ID");
    }

    public Bundle getBundle(){
        Bundle bundle= new Bundle();
        if(this.imageFilePath != null){
            bundle.putString(this.appid+".imageFilePath",this.imageFilePath);
        }
        bundle.putInt(this.appid+".left",this.thumbnailLeft);
        bundle.putInt(this.appid + ".top", this.thumbnailTop);
        bundle.putInt(this.appid + ".width", this.thumbnailWidth);
        bundle.putInt(this.appid + ".height", this.thumbnailHeight);
        return bundle;
    }

}

跳转设置的代码已经完毕,现在看看执行跳转时执行动画的代码
同样是一句代码:

start = ActivityTransition.with(getIntent()).to(findViewById(R.id.sub_imageView)).start(savedInstanceState);

传入上个界面传递过来的Bundle,需要展示的View控件,oncreate方法中保存的Bundle

public class ActivityTransition {

    //定义属性动画差值器,当前设置为开始快,然后慢
    private static final TimeInterpolator sDecelerator = new DecelerateInterpolator();

    private Intent fromIntent;
    int duration = 1000;
    View toView;

    public ActivityTransition(Intent intent) {
        this.fromIntent = intent;
    }

    public static ActivityTransition with(Intent intent) {
        return new ActivityTransition(intent);
    }

    public ActivityTransition to(View toView) {
        this.toView = toView;
        return this;
    }

    public ActivityTransition duration(int duration) {
        this.duration = duration;
        return this;
    }

    public ExitActivityTransition start(Bundle savedInstanceState) {
        Context context = this.toView.getContext();
        Bundle bundle = this.fromIntent.getExtras();
        MoveData moveData = TransitionAnimation.startAnimation(context, this.toView, bundle, savedInstanceState, this.duration, sDecelerator);
        return new ExitActivityTransition(moveData);
    }

}

上面一些初始化的方法就不说了,直接看start方法,里面执行startAnimation,并且返回MoveData
并且最后return 一个ExitActivityTransition
我们先看看MoveData方法里有什么

public class MoveData {

    public int leftDelta;
    public int topDelta;
    public float widthScale;
    public float heightScale;
    public int duration = 1000;
    public View toView;

    public MoveData(){};

}

很明显就是个实体类,保存执行动画时设置的参数

现在看startAimation方法是怎么执行的

public class TransitionAnimation {
    private static final String TAG = "Transition";
    public static WeakReference<Bitmap> bitmapCache;

    public TransitionAnimation() {
    }

    public static MoveData startAnimation(Context context, final View toView, Bundle transitionBundle, Bundle savedInstanceState, int duration, final TimeInterpolator interpolator) {
        final TransitionData transitionData = new TransitionData(context, transitionBundle);
        if(transitionData.imageFilePath != null) {
            setImageToView(toView, transitionData.imageFilePath);
        }

        final MoveData moveData = new MoveData();
        moveData.toView = toView;
        moveData.duration = duration;
        if(savedInstanceState == null) {
            ViewTreeObserver observer = toView.getViewTreeObserver();
            observer.addOnPreDrawListener(new OnPreDrawListener() {
                public boolean onPreDraw() {
                    toView.getViewTreeObserver().removeOnPreDrawListener(this);
                    int[] screenLocation = new int[2];
                    toView.getLocationOnScreen(screenLocation);
                    moveData.leftDelta = transitionData.thumbnailLeft - screenLocation[0];
                    moveData.topDelta = transitionData.thumbnailTop - screenLocation[1];
                    moveData.widthScale = (float)transitionData.thumbnailWidth / (float)toView.getWidth();
                    moveData.heightScale = (float)transitionData.thumbnailHeight / (float)toView.getHeight();
                    TransitionAnimation.runEnterAnimation(moveData, interpolator);
                    return true;
                }
            });
        }

        return moveData;
    }

    private static void runEnterAnimation(MoveData moveData, TimeInterpolator interpolator) {
        View toView = moveData.toView;
        toView.setPivotX(0.0F);
        toView.setPivotY(0.0F);
        toView.setScaleX(moveData.widthScale);
        toView.setScaleY(moveData.heightScale);
        toView.setTranslationX((float)moveData.leftDelta);
        toView.setTranslationY((float)moveData.topDelta);
        toView.animate().setDuration((long)moveData.duration).scaleX(1.0F).scaleY(1.0F).translationX(0.0F).translationY(0.0F).setInterpolator(interpolator);
    }

    private static void setImageToView(View toView, String imageFilePath) {
        Bitmap bitmap;
        if(bitmapCache != null && (bitmap = (Bitmap)bitmapCache.get()) != null) {
            bitmapCache.clear();
        } else {
            bitmap = BitmapFactory.decodeFile(imageFilePath);
        }

        if(toView instanceof ImageView) {
            ImageView toImageView = (ImageView)toView;
            toImageView.setImageBitmap(bitmap);
        } else if(VERSION.SDK_INT > 16) {
            toView.setBackground(new BitmapDrawable(toView.getResources(), bitmap));
        } else {
            toView.setBackgroundDrawable(new BitmapDrawable(toView.getResources(), bitmap));
        }

    }

    public static void startExitAnimation(MoveData moveData, Runnable endAction) {
        View view = moveData.toView;
        int duration = moveData.duration;
        int leftDelta = moveData.leftDelta;
        int topDelta = moveData.topDelta;
        float widthScale = moveData.widthScale;
        float heightScale = moveData.heightScale;
        view.animate().setDuration((long)duration).scaleX(widthScale).scaleY(heightScale).translationX((float)leftDelta).translationY((float)topDelta);
        view.postDelayed(endAction, (long)duration);
    }
}

代码很简单,判断是否有设置图片保存路径,有的话直接将图片设置到需要显示的View控件上,这里我们也能看出,元素共享并不仅限于ImageView

然后如果是首次进入界面,则定义一个观察者,在View被绘制时调用
runEnterAnimation方法就是执行元素共享的动画效果关键代码了

到这里动画效果就差不多了,还有最后一点,就是在返回到之前界面时,反向执行一次之前的动画效果
也就是在上面写过的

@Override
    public void onBackPressed() {
        start.exit(this);
    }
public class ExitActivityTransition {

    private final MoveData moveData;

    public ExitActivityTransition(MoveData moveData){
        this.moveData = moveData;
    }

    public void exit(final Activity activity){
        TransitionAnimation.startExitAnimation(this.moveData,new Runnable(){

            @Override
            public void run() {
                activity.finish();
                activity.overridePendingTransition(0,0);
            }
        });
    }
}