『Android 技能篇』優(yōu)雅的轉(zhuǎn)場(chǎng)動(dòng)畫之 Transition

前言

先直接上效果圖:

image

相信大家在平常開發(fā)也會(huì)遇到類似的轉(zhuǎn)場(chǎng)動(dòng)畫,如果想要要實(shí)現(xiàn)上圖的效果有哪些方式呢咬像?

首先分析一下轉(zhuǎn)場(chǎng)過(guò)程算撮,我們把起始 View 分別定義為 startViewendViewstartView 為常見的列表布局县昂,左側(cè)頭像和右側(cè)為文本介紹肮柜;endView 為詳情頁(yè)面,置頂?shù)拇髨D和詳細(xì)的文本介紹倒彰。

不難發(fā)現(xiàn)审洞,這些元素都是對(duì)應(yīng)關(guān)系,只不過(guò)起始狀態(tài)的基本屬性不同:

  • 頭像待讳,位置和大小以及 scaleType 發(fā)生變化
  • 背景芒澜,顏色、位置和大小發(fā)生變化
  • 名稱创淡,字體大小痴晦、顏色和位置發(fā)生變化
  • 描述,字體大小和位置發(fā)生變化

對(duì)于此效果琳彩,有很多辦法可以實(shí)現(xiàn)誊酌,綜合其實(shí)現(xiàn)成本和預(yù)期效果進(jìn)行最終選擇部凑,我能想到的大概有三種:

  1. 直接把上述的每個(gè)對(duì)象看做是獨(dú)立個(gè)體,各自創(chuàng)建獨(dú)立的動(dòng)畫對(duì)象碧浊,控制其執(zhí)行和結(jié)束狀態(tài)涂邀。

    這種方式,無(wú)疑是最簡(jiǎn)單粗暴的箱锐,但是實(shí)現(xiàn)和維護(hù)起來(lái)都很困難比勉,更不容易拓展

  2. 使用 MotionLayout,不得不說(shuō)很強(qiáng)大驹止,是 Google 推崇的動(dòng)畫組件浩聋,基本不用編寫 java 代碼就可完成負(fù)責(zé)的手勢(shì)和動(dòng)畫,后面有時(shí)間會(huì)介紹臊恋。

  3. 使用 Transition赡勘,Google 在 Android 5.0 完整引入,雖沒有 MotionLayout 那么強(qiáng)大捞镰,但是其復(fù)用性很強(qiáng),并且很容易理解毙替,上手也很快岸售。

今天咱們就以下面三個(gè)方向并結(jié)合對(duì)應(yīng)效果來(lái)帶大家了解一下 Transition。

  1. 原生提供的 Transition
  2. 自己實(shí)現(xiàn) Transition
  3. Scene

原生 Transition

準(zhǔn)備

核心關(guān)鍵類 TransitionManager, TransitionManager.beginDelayedTransition(ViewGroup viewGroup, Transition transition); 作為動(dòng)畫的開始厂画,傳入需要做轉(zhuǎn)場(chǎng)動(dòng)畫的父布局或根布局凸丸,隨后改變 View 的相關(guān)屬性,比如 setVisible()袱院,便可自動(dòng)完成轉(zhuǎn)場(chǎng)動(dòng)畫效果屎慢。

默認(rèn)實(shí)現(xiàn)的 AutoTransition,內(nèi)部集成了基礎(chǔ)動(dòng)畫:

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

Slide忽洛、Fade 和 Explode

這三者作為 Visibility 的三個(gè)子類腻惠,通過(guò)控制 view.setVisible() 的方式來(lái)達(dá)到具體的效果。

Fade欲虚,淡出 出場(chǎng)集灌,淡入 入場(chǎng)

image

Slide,向下離開屏幕出場(chǎng)复哆,向上進(jìn)入屏幕入場(chǎng)

image

Explode欣喧,四邊散開出場(chǎng),四邊匯入入場(chǎng)

image

同樣梯找,可以通過(guò):

Fade fade = new Fade();
Slide slide = new Slide();
TransitionSet set = new TransitionSet();
set.addTransition(fade).addTransition(slide).setOrdering(TransitionSet.ORDERING_TOGETHER);

達(dá)到組合的效果:

image

ChangeBounds

此處開始同一個(gè)頁(yè)面場(chǎng)景的切換唆阿,ChangeBounds 當(dāng) View 的位置或者大小發(fā)生變化時(shí)觸發(fā)對(duì)應(yīng)的轉(zhuǎn)場(chǎng)效果。比如:

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);

最終的效果:

image

ChangeClipBounds

當(dāng)調(diào)用 view.setClipBounds() 時(shí)會(huì)觸發(fā)轉(zhuǎn)場(chǎng)效果:

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);
}

最終效果:

image

ChangeScroll

當(dāng)調(diào)用 view.scrollTo() 會(huì)觸發(fā)轉(zhuǎn)場(chǎng)效果:

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);
}

最終效果:

image

ChangeTransform

這個(gè)就厲害了锈锤,View 的 translation驯鳖、scalerotation 發(fā)生改變時(shí)都會(huì)觸發(fā):

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);
}

最終效果:

image

自定義 Transition

介紹

其實(shí) Transition 的原理很簡(jiǎn)單闲询,大致的邏輯如下:

  1. 記錄當(dāng)前狀態(tài)的屬性值,比如位置大小或者自定義屬性之類
  2. 創(chuàng)建執(zhí)行動(dòng)畫臼隔,參數(shù)為當(dāng)前值和目標(biāo)值嘹裂,根據(jù)對(duì)應(yīng)算法來(lái)完成動(dòng)畫效果
  3. 根據(jù)目標(biāo)狀態(tài)的屬性值和記錄的緩存屬性值,調(diào)用創(chuàng)建好的動(dòng)畫對(duì)象執(zhí)行即可

那落實(shí)到代碼中摔握,首先先集成 Transition 類寄狼,會(huì)讓你實(shí)現(xiàn)三個(gè)方法:captureStartValuescaptureEndValuescreateAnimator氨淌。

  1. 定義你關(guān)心的屬性值泊愧;

    官方建議屬性定義的規(guī)則為:package_name:transition_class:property_name.

    比如

    private static String PROPNAME_TEXT_COLOR = "xiaweizi:changeTextColor:color";
    

    我想在文本顏色發(fā)生改變時(shí)做轉(zhuǎn)場(chǎng)動(dòng)畫,就可以定義上述的屬性盛正。

  2. 記錄起始狀態(tài)的屬性删咱;

    void captureStartValues(TransitionValues transitionValues)
    void captureEndValues(TransitionValues transitionValues);
    
  上述方法分別存儲(chǔ)起始狀態(tài)下對(duì)應(yīng)的屬性值:

  ```java
  transitionValues.values.put(PROPNAME_TEXT_COLOR, view.getCurrentTextColor());
  ```
3. 創(chuàng)建動(dòng)畫;

  ```java
  Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues)
參數(shù)值的 `startValues`和`endValues`分別可以拿到你存儲(chǔ)的屬性值豪筝,之后創(chuàng)建動(dòng)畫并返回即可痰滋,后續(xù)系統(tǒng)會(huì)根據(jù)你創(chuàng)建的動(dòng)畫進(jìn)行轉(zhuǎn)場(chǎng)。

是不是很簡(jiǎn)單续崖,接下來(lái)通過(guò)幾個(gè)案例帶大家感受一下:

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";

分別代表文本內(nèi)容變化敲街、文本顏色變化、文本大小變化和文本字體變化严望。我們只挑一個(gè)文本顏色來(lái)看一下動(dòng)畫是如何實(shí)現(xiàn)的:

// 記錄下起始狀態(tài)屬性值
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;
}

看一下這四種屬性發(fā)生變化時(shí)的效果:

image

ChangeBackgroundColorTransition

類似于文本顏色多艇,只不過(guò)針對(duì)的是 view.setBackground(),主要的代碼在于創(chuàng)建 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;
}

最終效果:

image

ChangeImageResourceTransition

有的時(shí)候發(fā)現(xiàn)像吻,在切換圖片的時(shí)候過(guò)度會(huì)很生硬峻黍,那可以通過(guò)在對(duì) Viewalpha 屬性從 101 的過(guò)程中替換圖片,這樣顯得很平滑拨匆。

@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;

最終效果:

image

ChangeCustomTransition

除了 View 原生的屬性姆涩,自定義屬性同樣也可以。

創(chuàng)建 Animator 沒什么區(qū)別:

@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();
}

最終的效果:

image

Scene

終于開始介紹文章開頭的效果是如何實(shí)現(xiàn)的:

image

有了前面的基礎(chǔ)鋪墊涮雷,實(shí)現(xiàn)起來(lái)就很簡(jiǎn)單阵面。

Scene 就是為這種場(chǎng)景的過(guò)度而設(shè)計(jì),不需要關(guān)注過(guò)度過(guò)程洪鸭,只需要傳入前后的布局样刷,并保證各個(gè)元素的 id 保持一致即可。

  1. 創(chuàng)建前后 layout览爵;layout_scene1.xmllayout_scene2.xml 具體代碼就補(bǔ)貼了
  2. 創(chuàng)建前后 Scene 對(duì)象置鼻;
    mScene1 = Scene.getSceneForLayout(mRoot, R.layout.layout_scene1, this);
    mScene2 = Scene.getSceneForLayout(mRoot, R.layout.layout_scene2, this);
    
3. 創(chuàng)建轉(zhuǎn)場(chǎng) `Transition`;我們把之前自定的組合成 `TransitionSet`:
  ```java
  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());
    }
  }
  1. 開始切換場(chǎng)景蜓竹;
    TransitionManager.go(mScene1, mTransition);
    TransitionManager.go(mScene2, mTransition);
    

總結(jié)

到此箕母,先詳細(xì)的和大家分享了系統(tǒng)自帶的 Transition储藐,并分析了其實(shí)現(xiàn)細(xì)節(jié)和原理,提供了多個(gè)自定義 Transition嘶是,接著了解了 Scene 創(chuàng)建過(guò)程钙勃,并通過(guò)簡(jiǎn)答的 demo 實(shí)現(xiàn)了從一個(gè)場(chǎng)景到另一個(gè)場(chǎng)景的過(guò)度效果,由淺入深聂喇,圖文并茂辖源,希望可以幫助到大家。

文中的項(xiàng)目已上傳到 TransitionDemo希太。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末克饶,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子誊辉,更是在濱河造成了極大的恐慌矾湃,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件堕澄,死亡現(xiàn)場(chǎng)離奇詭異邀跃,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)蛙紫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門坞嘀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人惊来,你說(shuō)我怎么就攤上這事」字停” “怎么了裁蚁?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)继准。 經(jīng)常有香客問(wèn)我枉证,道長(zhǎng),這世上最難降的妖魔是什么移必? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任室谚,我火速辦了婚禮,結(jié)果婚禮上崔泵,老公的妹妹穿的比我還像新娘秒赤。我一直安慰自己,他們只是感情好憎瘸,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布入篮。 她就那樣靜靜地躺著,像睡著了一般幌甘。 火紅的嫁衣襯著肌膚如雪潮售。 梳的紋絲不亂的頭發(fā)上痊项,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音酥诽,去河邊找鬼肛鹏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛畔柔,可吹牛的內(nèi)容都是我干的哼丈。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼泪姨,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼游沿!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起肮砾,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤诀黍,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后仗处,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體眯勾,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年婆誓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吃环。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡洋幻,死狀恐怖郁轻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情文留,我是刑警寧澤好唯,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站燥翅,受9級(jí)特大地震影響骑篙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜森书,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一靶端、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧凛膏,春花似錦杨名、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至鄙麦,卻和暖如春典唇,著一層夾襖步出監(jiān)牢的瞬間镊折,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工介衔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留恨胚,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓炎咖,卻偏偏與公主長(zhǎng)得像赃泡,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子乘盼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345