Android自定义Transition动画

Posted 亓斌

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android自定义Transition动画相关的知识,希望对你有一定的参考价值。

本文已授权微信公众号:鸿洋(hongyangandroid)在微信公众号平台原创首发。

曾经(或者现在)很多人说起Android和ios都会拿Android的UI设计来开黑, “你看看人家iOS的设计, 再来看看Android的, 差距怎么就这么大呢?”, 对于这种说辞, 可以一句话来总结一下”他们还停留在4.X之前的时代”. 自从Android5.0推出Material Design设计规范后, Android在设计上早已甩那个万年不变的iOS好几十条街!

以上纯属个人看法, 请勿开黑~~, 下面进入今天的主题.

还记得我曾经有篇文章(你所不知道的Activity转场动画——ActivityOptions)是来介绍Android新的转场动画的(对于ActivityOptions还不太熟悉的朋友现在可以打开上面的文章先来了解下), 那篇文章中介绍的Android预定义的几个转场虽然在效果上已经很赞了, 但是还是很难满足我们在开发中遇到的各种需求, 那怎么办? View不能满足需求, 我们可以自定义, Transition也是一样~~, 所以这篇文章我们就来介绍一下如何自定义Transition动画.

熟悉原理

在开始自定义之前, 我们首先来简单的了解一下Transition转场动画的原理, 大家在看到你所不知道的Activity转场动画——ActivityOptions这篇文章时, 对Android提供的这种新的转场动画都震撼到了, 但是肯定有很多人对它的原理不是很请求, 尤其是Scene场景动画, 一个ImageView怎么就变着变着跳转到其他的Activity了呢? 其实它的原理很简单,Transition动画其实就是拿着第一页某个view的信息去第二页的某个view上做的动画, 这样我们在视觉上就会产生一个渐变的错觉~

玩玩Transition

在稍微了解了一下原理之后, 我们就来玩玩Transition了, 如何自定义一个Transition呢? 跟自定义view我们需要继承View或者ViewGroup一样, 这里我们需要继承Transition类.

public class MyTransition extends Transition {}

有两个抽象方法必须要要重写,

public class MyTransition extends Transition {

    @Override
    public void captureStartValues(TransitionValues transitionValues) {

    }

    @Override
    public void captureEndValues(TransitionValues transitionValues) {

    }
}

除了这两个必须要重写的方法, 我们还要重写一个createAnimator方法来自定义动画, 于是, 我们要自定义一个Transition, 一个类的结构肯定是肯定是这样的.

public class MyTransition extends Transition {

    @Override
    public void captureStartValues(TransitionValues transitionValues) {

    }

    @Override
    public void captureEndValues(TransitionValues transitionValues) {

    }

    @Override
    public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues) {
    }
}

ok, 下面我们来详细说一下这三个方法都是用来干嘛的.
首先captureStartValues, 从字面上来看是用来收集开始信息的, 什么开始信息? 当然是动画的开始信息了. 那同样的captureEndValues是用来收集动画结束的信息的. 收集完了信息,就要通过createAnimator来创建个Animator供系统调用了.

再来看看TransitionValues这个陌生的类, 这个类其实很简单, 只有两个成员变量viewvalues, view指的是我们要从哪个view上收集信息, values是用来存放我们收集到的信息的. 比如: 在captureStartValues里, transitionValues.view指的就是我们在开始动画的界面上的那个view, 在captureEndValues指的就是在目标界面上的那个view.

好了, 上面几个方法的作用介绍完毕后, 我们马上就来完成一个进入消息内容的动画效果, 还是老规矩, 在开始代码之前, 我们先来看看效果.

唉, 在ubuntu上录屏有点费劲, 效果不咋地, 凑活着看, 或者可以在文章最后的链接自己下载本文的demo源码自己运行看~

仔细观察效果, 我们可以找到两处动画.

  1. 单行内容从它在列表中的位置移动到界面的最上面.
  2. 消息的内容由单行逐渐展开.
  3. 这两个动画是顺序执行的

通过上面的分析, 我们大致可以得出, 下面, 我们需要收集的信息有view在界面的位置view的高度信息, 所以我们先来定义一下需要收集的信息

public class MyTransition extends Transition {

    private static final String TOP = "top";
    private static final String HEIGHT = "height";
    // ...
}

然后我们开始收集动画开始需要的信息

public class MyTransition extends Transition {

    private static final String TOP = "top";
    private static final String HEIGHT = "height";

    @Override
    public void captureStartValues(TransitionValues transitionValues) {
        View view = transitionValues.view;
        Rect rect = new Rect();
        view.getHitRect(rect);

        transitionValues.values.put(TOP, rect.top);
        transitionValues.values.put(HEIGHT, view.getHeight());

        Log.d("qibin", "start:" + rect.top + ";" + view.getHeight());
    }
}

首先, 我们通过transitionValues.view拿到我们要收集信息的目标view, 然后我们可以通过getHitRect可以拿到它在ListView中的上下左右信息, 最后我们通过transitionValues.values.put(TOP, rect.top)来保存一下他距离父布局上面的距离, 当然我们还需要通过transitionValues.values.put(HEIGHT, view.getHeight())来保存动画初始的高度.

收集完动画开始的信息, 我们再来收集动画结束的信息, 依葫芦画瓢, 很快就能写出下面的代码.

public class MyTransition extends Transition {

    private static final String TOP = "top";
    private static final String HEIGHT = "height";

    @Override
    public void captureEndValues(TransitionValues transitionValues) {
        transitionValues.values.put(TOP, 0);
        transitionValues.values.put(HEIGHT, transitionValues.view.getHeight());

        Log.d("qibin", "end:" + 0 + ";" + transitionValues.view.getHeight());
    }
}

这里的代码和上面并无差别, 动画结束后, view距离上面的距离应该是0, 不过这里需要注意的是captureStartValues方法里的transitionValues.view是我们页面跳转开始那个界面上的view, 而captureEndValues方法里的transitionValues.view是我们跳转目标上的view, 所以这两个方法里获取到的view的高度肯定是不一样的.

好了, 在完成信息收集之后, 我们就来写动画效果了,

public class MyTransition extends Transition {
  @Override
  public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues) {
      if (startValues == null || endValues == null) { return null;}

      final View endView = endValues.view;

      final int startTop = (int) startValues.values.get(TOP);
      final int startHeight = (int) startValues.values.get(HEIGHT);
      final int endTop = (int) endValues.values.get(TOP);
      final int endHeight = (int) endValues.values.get(HEIGHT);

      ViewCompat.setTranslationY(endView, startTop);
      endView.getLayoutParams().height = startHeight;
      endView.requestLayout();

      ValueAnimator positionAnimator = ValueAnimator.ofInt(startTop, endTop);
      if (mPositionDuration > 0) { positionAnimator.setDuration(mPositionDuration);}
      positionAnimator.setInterpolator(mPositionInterpolator);

      positionAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
          @Override
          public void onAnimationUpdate(ValueAnimator valueAnimator) {
              int current = (int) valueAnimator.getAnimatedValue();
              ViewCompat.setTranslationY(endView, current);
          }
      });

      ValueAnimator sizeAnimator = ValueAnimator.ofInt(startHeight, endHeight);
      if (mSizeDuration > 0) { sizeAnimator.setDuration(mSizeDuration);}
      sizeAnimator.setInterpolator(mSizeInterpolator);

      sizeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
          @Override
          public void onAnimationUpdate(ValueAnimator valueAnimator) {
              int current = (int) valueAnimator.getAnimatedValue();
              endView.getLayoutParams().height = current;
              endView.requestLayout();
          }
      });

      AnimatorSet set = new AnimatorSet();
      set.play(sizeAnimator).after(positionAnimator);

      return set;
  }
}

在说原理的时候, 我们提到过, 这一系列的动画其实在我们跳转后的界面上完成的, 所以这里的动画我们也是在目标view上完成. 上面两个方法中收集到的信息, 我们需要在这里用到, 所以我们通过以下代码来获取收集到的信息.

final int startTop = (int) startValues.values.get(TOP);
final int startHeight = (int) startValues.values.get(HEIGHT);
final int endTop = (int) endValues.values.get(TOP);
final int endHeight = (int) endValues.values.get(HEIGHT);

startValuesendValues都是createAnimator的参数.
接着几行莫名奇妙的代码

ViewCompat.setTranslationY(endView, startTop);
endView.getLayoutParams().height = startHeight;
endView.requestLayout();

是因为我们的动画顺序是先移动, 后展开, 首先把view的高度设置为前一个界面上view的高度是为了防止在移动的过程中view的高度是他自身的高度的.
接着我们创建了两个动画, 这两个动画很好理解, 一个位移的,一个是展开的, 不过这里我们给了动画一个时长和插值器, 这两个信息是公开给调用者去设置的.
最后我们创建一个AnimatorSet, 在这个动画集合中, 我们先来完成sizeAnimator然后开始positionAnimator, 最后返回该动画集合. 自定义Transition完毕.

使用自定义Transition

上面我们完成了Transition的自定义, 这里我们就来用一下它, 首先我们要在应用的主题中指定可以使用场景过度动画.

<item name="android:windowContentTransitions">true</item>

看过你所不知道的Activity转场动画——ActivityOptions这篇文章的朋友都应该清楚, 我们还需要给我们两个activity中的view一个transitionName, 这里就不贴代码了, 然后我们就来看看如何做跳转.

public class MainActivity extends AppCompatActivity {

    private ListView mListView;
    private Adapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mListView = (ListView) findViewById(R.id.list);
        mAdapter = new Adapter();
        mListView.setAdapter(new Adapter());
        mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
                startActivity(view, mAdapter.getItem(position));
            }
        });
    }
       public void startActivity(View view, String content) {
           Intent intent = new Intent(this, MessageActivity.class);
           intent.putExtra("msg", content);

           ActivityOptionsCompat compat = ActivityOptionsCompat
                   .makeSceneTransitionAnimation(this, view, view.getTransitionName());
           ActivityCompat.startActivity(this, intent, compat.toBundle());
       }
}

跳转的代码大家都可以在你所不知道的Activity转场动画——ActivityOptions这篇文章中找到, 这里就不解释了, 我们主要还是来看看在目标activity中怎么应用动画.

public class MessageActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.message_layout);
        setTitle("Content");

        TextView msgTextView = (TextView) findViewById(R.id.msg);
        msgTextView.setText(getIntent().getStringExtra("msg"));

        executeTransition();
    }

    public void executeTransition() {
        MyTransition transition = new MyTransition();
        transition.setPositionDuration(300);
        transition.setSizeDuration(300);
        transition.setPositionInterpolator(new FastOutLinearInInterpolator());
        transition.setSizeInterpolator(new FastOutSlowInInterpolator());
        transition.addTarget("message");

        getWindow().setSharedElementEnterTransition(transition);
    }

    @Override
    public void onBackPressed() {
        finish();
    }
}

来看executeTransition方法, 在这个方法中, 首页我们构建了一个我们自定义的transition, 然后各种配置, 解析来的一行代码,

transition.addTarget("message");

这个message就是我们前面提到的transitionName, 最后我们通过

getWindow().setSharedElementEnterTransition(transition);

来设置进入的动画.
ok, 现在我们来看看效果

闪烁问题

看到效果后, 细心的朋友可能发现, 在动画执行的过程中我们的NavigationBar会产生一个闪烁的效果, 这个效果不是我们想要的,出现这个问题的原因是共享元素动画是在整个窗口的view上执行的, 在这里找到了解决方案. 他的解决办法是: 首先将NavigationBar也作为动画的一部分, 然后在目标activity中延迟动画的执行. google给我们提供了两个方法来用, postponeEnterTransition()startPostponedEnterTransition()方法来延迟动画的执行.

所以, 现在我们的跳转代码应该是这样的.

public void startActivity(View view, String content) {
    View statusBar = findViewById(android.R.id.statusBarBackground);
    View navigationBar = findViewById(android.R.id.navigationBarBackground);

    List<Pair<View, String>> pairs = new ArrayList<>();
    pairs.add(Pair.create(statusBar, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME));
    pairs.add(Pair.create(navigationBar, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME));
    pairs.add(Pair.create(view, view.getTransitionName()));

    Intent intent = new Intent(this, MessageActivity.class);
    intent.putExtra("msg", content);

    ActivityOptionsCompat compat = ActivityOptionsCompat
            .makeSceneTransitionAnimation(this, pairs.toArray(new Pair[pairs.size()]));
    ActivityCompat.startActivity(this, intent, compat.toBundle());
}

在目标activity中执行的动画的代码也应该是这样的.

public void executeTransition() {
    postponeEnterTransition();

    final View decorView = getWindow().getDecorView();
    getWindow().getDecorView().getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            decorView.getViewTreeObserver().removeOnPreDrawListener(this);
            supportStartPostponedEnterTransition();
            return true;
        }
    });

    MyTransition transition = new MyTransition();
    transition.setPositionDuration(300);
    transition.setSizeDuration(300);
    transition.setPositionInterpolator(new FastOutLinearInInterpolator());
    transition.setSizeInterpolator(new FastOutSlowInInterpolator());
    transition.addTarget("message");

    getWindow().setSharedElementEnterTransition(transition);
}

到现在, 我们就完美解决了闪烁的问题~. ok, 到这里, 大家应该可以随意的自定义Transition动画啦~最后需要demo的朋友可以到https://github.com/qibin0506/TransitionAnimator来下载.

以上是关于Android自定义Transition动画的主要内容,如果未能解决你的问题,请参考以下文章

Android自定义Transition动画

怎么自定义android 下拉刷新动画

在Activity切换之间实现Transition动画

(transition动画组件)

再谈属性动画——介绍以及自定义Interpolator插值器

如何自定义模态视图控制器呈现动画?