『Android 技能篇』優雅的轉場動畫之 Transition | 掘金技術徵文-雙節特別篇

前言

先直接上效果圖:java

相信你們在日常開發也會遇到相似的轉場動畫,若是想要要實現上圖的效果有哪些方式呢?android

首先分析一下轉場過程,咱們把起始 View 分別定義爲 startViewendViewstartView 爲常見的列表佈局,左側頭像和右側爲文本介紹;endView 爲詳情頁面,置頂的大圖和詳細的文本介紹。git

不難發現,這些元素都是對應關係,只不過起始狀態的基本屬性不一樣:github

  • 頭像,位置和大小以及 scaleType 發生變化
  • 背景,顏色、位置和大小發生變化
  • 名稱,字體大小、顏色和位置發生變化
  • 描述,字體大小和位置發生變化

對於此效果,有不少辦法能夠實現,綜合其實現成本和預期效果進行最終選擇,我能想到的大概有三種:算法

  1. 直接把上述的每一個對象看作是獨立個體,各自建立獨立的動畫對象,控制其執行和結束狀態。canvas

    這種方式,無疑是最簡單粗暴的,可是實現和維護起來都很困難,更不容易拓展緩存

  2. 使用 MotionLayout,不得不說很強大,是 Google 推崇的動畫組件,基本不用編寫 java 代碼就可完成負責的手勢和動畫,後面有時間會介紹。markdown

  3. 使用 Transition,Google 在 Android 5.0 完整引入,雖沒有 MotionLayout 那麼強大,可是其複用性很強,而且很容易理解,上手也很快。app

今天我們就如下面三個方向並結合對應效果來帶你們瞭解一下 Transition。ide

  1. 原生提供的 Transition
  2. 本身實現 Transition
  3. Scene

原生 Transition

準備

核心關鍵類 TransitionManager, TransitionManager.beginDelayedTransition(ViewGroup viewGroup, Transition transition); 做爲動畫的開始,傳入須要作轉場動畫的父佈局或根佈局,隨後改變 View 的相關屬性,好比 setVisible(),即可自動完成轉場動畫效果。

默認實現的 AutoTransition,內部集成了基礎動畫:

private void init() {
    setOrdering(ORDERING_SEQUENTIAL);
    addTransition(new Fade(Fade.OUT)).
            addTransition(new ChangeBounds()).
            addTransition(new Fade(Fade.IN));
}
複製代碼

Slide、Fade 和 Explode

這三者做爲 Visibility 的三個子類,經過控制 view.setVisible() 的方式來達到具體的效果。

Fade,淡出 出場,淡入 入場

Slide,向下離開屏幕出場,向上進入屏幕入場

Explode,四邊散開出場,四邊匯入入場

一樣,能夠經過:

Fade fade = new Fade();
Slide slide = new Slide();
TransitionSet set = new TransitionSet();
set.addTransition(fade).addTransition(slide).setOrdering(TransitionSet.ORDERING_TOGETHER);
複製代碼

達到組合的效果:

ChangeBounds

此處開始同一個頁面場景的切換,ChangeBounds 當 View 的位置或者大小發生變化時觸發對應的轉場效果。好比:

ChangeBounds transition = new ChangeBounds();
transition.setInterpolator(new AnticipateInterpolator());
TransitionManager.beginDelayedTransition(mRoot, transition);
ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) view3.getLayoutParams();
if (layoutParams.leftMargin == 400) {
    layoutParams.leftMargin = 50;
} else {
    layoutParams.leftMargin = 400;
}
view3.setLayoutParams(layoutParams);
複製代碼

最終的效果:

ChangeClipBounds

當調用 view.setClipBounds() 時會觸發轉場效果:

ChangeClipBounds transition = new ChangeClipBounds();
transition.setInterpolator(new BounceInterpolator());
TransitionManager.beginDelayedTransition(mRoot, transition);
int width = view2.getWidth();
int height = view2.getHeight();
int gap = 140;
Rect rect = new Rect(0, gap, width, height - gap);
if (rect.equals(view2.getClipBounds())) {
    view2.setClipBounds(null);
} else {
    view2.setClipBounds(rect);
}
複製代碼

最終效果:

ChangeScroll

當調用 view.scrollTo() 會觸發轉場效果:

ChangeScroll transition = new ChangeScroll();
transition.setInterpolator(new AnticipateOvershootInterpolator());
TransitionManager.beginDelayedTransition(mRoot, transition);
if (view1.getScrollX() == -100 && view1.getScrollY() == -100) {
    view1.scrollTo(0, 0);
} else {
    view1.scrollTo(-100, -100);
}
複製代碼

最終效果:

ChangeTransform

這個就厲害了,View 的 translationscalerotation 發生改變時都會觸發:

ChangeTransform transition = new ChangeTransform();
transition.setInterpolator(new OvershootInterpolator());
TransitionManager.beginDelayedTransition(mRoot, transition);
if (view1.getTranslationX() == 100 && view1.getTranslationY() == 100) {
    view1.setTranslationX(0);
    view1.setTranslationY(0);
} else {
    view1.setTranslationX(100);
    view1.setTranslationY(100);
}
if (view2.getRotationX() == 30f) {
    view2.setRotationX(0);
} else {
    view2.setRotationX(30);
}
if (view3.getRotationY() == 30f) {
    view3.setRotationY(0);
} else {
    view3.setRotationY(30);
}
if (view4.getScaleX() == 0.5f && view4.getScaleY() == 0.5f) {
    view4.setScaleX(1f);
    view4.setScaleY(1f);
} else {
    view4.setScaleX(0.5f);
    view4.setScaleY(0.5f);
}
複製代碼

最終效果:

自定義 Transition

介紹

其實 Transition 的原理很簡單,大體的邏輯以下:

  1. 記錄當前狀態的屬性值,好比位置大小或者自定義屬性之類
  2. 建立執行動畫,參數爲當前值和目標值,根據對應算法來完成動畫效果
  3. 根據目標狀態的屬性值和記錄的緩存屬性值,調用建立好的動畫對象執行便可

那落實到代碼中,首先先集成 Transition 類,會讓你實現三個方法:captureStartValuescaptureEndValuescreateAnimator

  1. 定義你關心的屬性值

    官方建議屬性定義的規則爲:package_name:transition_class:property_name.

    好比

    private static String PROPNAME_TEXT_COLOR = "xiaweizi:changeTextColor:color";
    複製代碼

    我想在文本顏色發生改變時作轉場動畫,就能夠定義上述的屬性。

  2. 記錄起始狀態的屬性;

    void captureStartValues(TransitionValues transitionValues) void captureEndValues(TransitionValues transitionValues);
    複製代碼

    上述方法分別存儲起始狀態下對應的屬性值:

    transitionValues.values.put(PROPNAME_TEXT_COLOR, view.getCurrentTextColor());
    複製代碼
  3. 建立動畫;

    Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues) 複製代碼

    參數值的 startValuesendValues分別能夠拿到你存儲的屬性值,以後建立動畫並返回便可,後續系統會根據你建立的動畫進行轉場。

是否是很簡單,接下來經過幾個案例帶你們感覺一下:

ChangeTextTransition

ChangeTextTransition.java 該類中定義了:

private static String PROPNAME_TEXT = "xiaweizi:changeText:text";
private static String PROPNAME_TEXT_COLOR = "xiaweizi:changeTextColor:color";
private static String PROPNAME_TEXT_SIZE = "xiaweizi:changeTextSize:size";
private static String PROPNAME_TEXT_LEVEL = "xiaweizi:changeTextTypeface:level";
複製代碼

分別表明文本內容變化、文本顏色變化、文本大小變化和文本字體變化。咱們只挑一個文本顏色來看一下動畫是如何實現的:

// 記錄下起始狀態屬性值
private void captureValues(TransitionValues transitionValues) {
    if (transitionValues == null || !(transitionValues.view instanceof TextView)) return;
    TextView view = (TextView) transitionValues.view;
    transitionValues.values.put(PROPNAME_TEXT, view.getText());
    transitionValues.values.put(PROPNAME_TEXT_COLOR, view.getCurrentTextColor());
    transitionValues.values.put(PROPNAME_TEXT_SIZE, view.getTextSize());
    transitionValues.values.put(PROPNAME_TEXT_LEVEL, view.getTag(R.id.type_face_level));
}

@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues) {
    if (startValues == null || endValues == null) {
        return null;
    }
    if (!(endValues.view instanceof TextView)) {
        return super.createAnimator(sceneRoot, startValues, endValues);
    }
    TextView endView = (TextView) endValues.view;
    int startTextColor = (int) startValues.values.get(PROPNAME_TEXT_COLOR);
    int endTextColor = (int) endValues.values.get(PROPNAME_TEXT_COLOR);
    ObjectAnimator animator = ObjectAnimator.ofArgb(endView, new TextColorProperty(), startTextColor, endTextColor);
    animator.setDuration(300);
    return animator;
}
複製代碼

看一下這四種屬性發生變化時的效果:

ChangeBackgroundColorTransition

相似於文本顏色,只不過針對的是 view.setBackground(),主要的代碼在於建立 Animator

@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues) {
    if (startValues == null || endValues == null) {
        return null;
    }
    final View endView = endValues.view;
    ColorDrawable startColorDrawable = (ColorDrawable) startValues.values.get(PROPNAME_COLOR);
    ColorDrawable endColorDrawable = (ColorDrawable) endValues.values.get(PROPNAME_COLOR);
    if (startColorDrawable == null || endColorDrawable == null) return super.createAnimator(sceneRoot, startValues, endValues);
    final int startColor = startColorDrawable.getColor();
    final int endColor = endColorDrawable.getColor();
    ValueAnimator animator = ValueAnimator.ofObject(new ArgbEvaluator(), startColor, endColor);
    animator.setDuration(300);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            int animatedValue = (int) animation.getAnimatedValue();
            endView.setBackgroundColor(animatedValue);
        }
    });
    return animator;
}
複製代碼

最終效果:

ChangeImageResourceTransition

有的時候發現,在切換圖片的時候過分會很生硬,那能夠經過在對 Viewalpha 屬性從 101 的過程當中替換圖片,這樣顯得很平滑。

@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues) {
    if (startValues == null || endValues == null) {
        return null;
    }
    if (!(endValues.view instanceof ImageView)) {
        return super.createAnimator(sceneRoot, startValues, endValues);
    }
    final ImageView endView = (ImageView) endValues.view;
    final Drawable startDrawable = (Drawable) startValues.values.get(PROPNAME_IMAGE_RESOURCE);
    final Drawable endDrawable = (Drawable) endValues.values.get(PROPNAME_IMAGE_RESOURCE);
    ValueAnimator animator = ValueAnimator.ofFloat(0, 1f);
    animator.setDuration(300);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            float animatedValue = (float) animation.getAnimatedValue();
            if (animatedValue <= 0.5f) {
                endView.setImageDrawable(startDrawable);
                float ratio = (0.5f - animatedValue) / 0.5f;
                endView.setAlpha(ratio);
            } else {
                endView.setImageDrawable(endDrawable);
                float ratio = (animatedValue - 0.5f) / 0.5f;
                endView.setAlpha(ratio);
            }
        }
    });
    return animator;
複製代碼

最終效果:

ChangeCustomTransition

除了 View 原生的屬性,自定義屬性一樣也能夠。

建立 Animator 沒什麼區別:

@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues) {
    if (startValues == null || endValues == null) {
        return null;
    }
    if (!(endValues.view instanceof TransitionView)) {
        return super.createAnimator(sceneRoot, startValues, endValues);
    }
    final TransitionView endView = (TransitionView) endValues.view;
    final float startRatio = (float) startValues.values.get(PROPNAME_CUSTOM_RATIO);
    final float endRatio = (float) endValues.values.get(PROPNAME_CUSTOM_RATIO);
    ObjectAnimator animator = ObjectAnimator.ofFloat(endView, "ratio", startRatio, endRatio);
    animator.setDuration(300);
    return animator;
}
複製代碼

主要在自定義 View 的繪製邏輯:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    // 繪製左邊
    canvas.save();
    mRect.set(0, 0, (int) (getWidth() * mRatio), getHeight());
    canvas.clipRect(mRect);
    mTextPaint.setColor(mStartColor);
    TransitionUtils.drawTextCenter(canvas, "文本三", getWidth() / 2, getHeight() / 2, mTextPaint);
    canvas.restore();

    // 繪製右邊
    canvas.save();
    mRect.set((int) (getWidth() * mRatio), 0, getWidth(), getHeight());
    canvas.clipRect(mRect);
    mTextPaint.setColor(mEndColor);
    TransitionUtils.drawTextCenter(canvas, "三本文", getWidth() / 2, getHeight() / 2, mTextPaint);
    canvas.restore();
}
複製代碼

最終的效果:

Scene

終於開始介紹文章開頭的效果是如何實現的:

有了前面的基礎鋪墊,實現起來就很簡單。

Scene 就是爲這種場景的過分而設計,不須要關注過分過程,只須要傳入先後的佈局,並保證各個元素的 id 保持一致便可。

  1. 建立先後 layoutlayout_scene1.xmllayout_scene2.xml 具體代碼就補貼了
  2. 建立先後 Scene 對象;
    mScene1 = Scene.getSceneForLayout(mRoot, R.layout.layout_scene1, this);
    mScene2 = Scene.getSceneForLayout(mRoot, R.layout.layout_scene2, this);
    複製代碼
  3. 建立轉場 Transition;咱們把以前自定的組合成 TransitionSet
    public class SceneTransition extends TransitionSet {
      public SceneTransition() {
          init();
      }
      public SceneTransition(Context context, AttributeSet attrs) {
          super(context, attrs);
          init();
      }
      private void init() {
          addTransition(new ChangeTextTransition())
                  .addTransition(new ChangeScroll())
                  .addTransition(new ChangeBackgroundColorTransition())
                  .addTransition(new ChangeBounds());
      }
    }
    複製代碼
  4. 開始切換場景;
    TransitionManager.go(mScene1, mTransition);
    TransitionManager.go(mScene2, mTransition);
    複製代碼

總結

到此,先詳細的和你們分享了系統自帶的 Transition,並分析了其實現細節和原理,提供了多個自定義 Transition,接着瞭解了 Scene 建立過程,並經過簡答的 demo 實現了從一個場景到另外一個場景的過分效果,由淺入深,圖文並茂,但願能夠幫助到你們。

文中的項目已上傳到 TransitionDemo

🏆 掘金技術徵文|雙節特別篇

相關文章
相關標籤/搜索