Android動(dòng)畫(huà) —— 過(guò)渡框架

前不久,我寫(xiě)了篇關(guān)于Activity之間的過(guò)渡跳轉(zhuǎn)的文章(有興趣的請(qǐng)戳 Android動(dòng)畫(huà) —— Activity過(guò)渡),算是初窺了Android過(guò)渡Transition)的基本概念,以及它在Activity跳轉(zhuǎn)中的應(yīng)用。今天,我們就來(lái)進(jìn)一步學(xué)習(xí)一下它,然后實(shí)現(xiàn)自定義的過(guò)渡,填補(bǔ)系統(tǒng)內(nèi)置效果的不足。

一、過(guò)渡框架淺談

Android提供過(guò)渡框架,目的是讓?xiě)?yīng)用的界面“動(dòng)起來(lái)”,以提升視覺(jué)吸引力和視覺(jué)上的線索,讓用戶知道應(yīng)用到底是怎么工作的。

好像還是不甚明了?

從開(kāi)發(fā)的角度來(lái)講,根本上,過(guò)渡框架就是實(shí)現(xiàn)了View樹(shù)View Hierarchy)之間的動(dòng)畫(huà)切換 —— 用動(dòng)畫(huà)將兩個(gè)View樹(shù)下的所有View連接起來(lái)。這里的View樹(shù),可能一個(gè)View,也可能是一個(gè)復(fù)雜的ViewGroup。

過(guò)渡框架支持的功能包括:

  • 組級(jí)動(dòng)畫(huà)

對(duì)View樹(shù)中的所有View應(yīng)用一個(gè)或多個(gè)動(dòng)畫(huà)效果

  • 基于過(guò)渡的動(dòng)畫(huà)

根據(jù)起始View和結(jié)束View的屬性值變化來(lái)應(yīng)用動(dòng)畫(huà)

  • 預(yù)置動(dòng)畫(huà)

引入預(yù)置動(dòng)畫(huà)以實(shí)現(xiàn)通用效果,例如漸隱或移動(dòng)

  • 資源文件

從資源文件加載View樹(shù)和內(nèi)置動(dòng)畫(huà)

  • 生命周期

定義了生命周期回調(diào),在動(dòng)畫(huà)和View樹(shù)變化過(guò)程中提供更好的控制

二、場(chǎng)景過(guò)渡

場(chǎng)景

在過(guò)渡框架中,有一個(gè)很重要的概念,稱為場(chǎng)景Scene)。一個(gè)場(chǎng)景,保存了一個(gè)View樹(shù)的狀態(tài)及其所有View的屬性值,也保存了View樹(shù)的父引用,場(chǎng)景的改變與動(dòng)畫(huà)都將發(fā)生在這個(gè)父引用里面。

Android字義了類Scene來(lái)表示一個(gè)場(chǎng)景。

場(chǎng)景的過(guò)渡過(guò)程將涉及起始終止兩個(gè)狀態(tài)。在多數(shù)情況下,我們不用顯示設(shè)置起始場(chǎng)景。為什么不用設(shè)置?第一,如果已經(jīng)有應(yīng)用過(guò)過(guò)渡,那么接下來(lái)的過(guò)渡將以已應(yīng)用過(guò)的過(guò)渡的終止場(chǎng)景作為起始場(chǎng)景。第二,如果從未應(yīng)用過(guò)渡,那么過(guò)渡框架會(huì)從當(dāng)前的屏幕狀態(tài)收集所有View的信息,然后把它們作為過(guò)渡的起始場(chǎng)景。

過(guò)渡

場(chǎng)景過(guò)渡的使用主要包括三個(gè)步驟:

  1. 創(chuàng)建起始場(chǎng)景、終止場(chǎng)景
  2. 設(shè)置過(guò)渡動(dòng)畫(huà)
  3. TransitionManager啟動(dòng)動(dòng)畫(huà)

說(shuō)了這么多,大概還是云里霧里?還是來(lái)個(gè)例子嘗嘗鮮吧。

首先,定義場(chǎng)景所在的View樹(shù)父布局:

    <FrameLayout
        android:id="@+id/scene_root"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingBottom="16dp"
        android:paddingTop="16dp">

        <include layout="@layout/a_scene" />

    </FrameLayout>

前面已經(jīng)說(shuō)到,場(chǎng)景的轉(zhuǎn)換將發(fā)生在它的父引用里面,也就是這里的FrameLayout了。而這里include的布局(a_scene),其實(shí)就是我們的起始場(chǎng)景布局。

a_scene.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/scene_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <!-- Ids here must be added and be the same with those in another_scene.xml as well-->
        <ImageView
            android:id="@+id/sun"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_sun" />
    
        <ImageView
            android:id="@+id/moon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_moon"/>
    </LinearLayout>

然后,定義終止場(chǎng)景布局another_scene。

another_scene.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/scene_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <ImageView
            android:id="@+id/moon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_moon"/>
    
        <ImageView
            android:id="@+id/sun"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_sun" />
    </LinearLayout>

兩個(gè)場(chǎng)景的布局控件相同,只是擺放的位置不同。

場(chǎng)景布局定義完畢,那就要開(kāi)始創(chuàng)建場(chǎng)景了。調(diào)用Scene類的靜態(tài)方法getSceneForLayout (ViewGroup sceneRoot, int layoutId, Context context),第一個(gè)參數(shù)是場(chǎng)景的父引用,第二個(gè)參數(shù)是場(chǎng)景的布局id。

    mSceneRoot = (FrameLayout) findViewById(R.id.scene_root);
    mAScene = Scene.getSceneForLayout(mSceneRoot, R.layout.a_scene, this);
    mAnotherScene = Scene.getSceneForLayout(mSceneRoot, R.layout.another_scene, this);

最后,啟動(dòng)場(chǎng)景過(guò)渡動(dòng)畫(huà)。這里使用ChangeBounds過(guò)渡。調(diào)用TransitionManager的靜態(tài)方法go (Scene scene, Transition transition),第一個(gè)參數(shù)是要切換的目的場(chǎng)景,第二個(gè)參數(shù)是過(guò)渡動(dòng)畫(huà)。

    TransitionManager.go(mAnotherScene, new ChangeBounds());

效果如下:

Scenes.gif.gif

是不是很方便?雖然用一般的屬性動(dòng)畫(huà),也可以做得到這樣的效果,但是利用過(guò)渡框架,可就簡(jiǎn)單多了。

值得注意的是:兩個(gè)場(chǎng)景的“相同”元素,需要使用相同的id,否則沒(méi)有動(dòng)畫(huà)效果。

三、無(wú)場(chǎng)景過(guò)渡

有時(shí)候,我們只是需要在當(dāng)前界面添加或者刪除一個(gè)View —— 也就是說(shuō),變化前后的兩個(gè)View樹(shù)幾乎是一樣的。如果使用場(chǎng)景過(guò)渡來(lái)實(shí)現(xiàn)變化效果,需要維護(hù)兩個(gè)幾乎相同的場(chǎng)景布局,未免小題大做。這種情況,無(wú)場(chǎng)景的過(guò)渡就有了用武之地。

無(wú)場(chǎng)景過(guò)渡僅包含一個(gè)View樹(shù),但是維持兩個(gè)狀態(tài),通過(guò)“延時(shí)過(guò)渡”(Delayed transition)實(shí)現(xiàn)兩個(gè)狀態(tài)的過(guò)渡效果。

無(wú)場(chǎng)景過(guò)渡的使用也主要包括三個(gè)步驟:

  1. 調(diào)用TransitionManager.beginDelayedTransition()保存View樹(shù)狀態(tài)并設(shè)置過(guò)渡
  2. View樹(shù)發(fā)生改變,過(guò)渡框架記錄新的狀態(tài)
  3. 系統(tǒng)重繪,過(guò)渡框架啟動(dòng)過(guò)渡動(dòng)畫(huà)

實(shí)際上,需要自行控制的主要是前兩步,其它的都由過(guò)渡框架完成。

還是來(lái)個(gè)例子。首先,定義一個(gè)過(guò)渡動(dòng)畫(huà),包括ChangeBounds和Fade兩個(gè)效果的組合。然后調(diào)用beginDelayedTransition()保存View樹(shù)狀態(tài),并設(shè)置好前面定義的過(guò)渡動(dòng)畫(huà)。接著,addView或者removeView使得View樹(shù)發(fā)生變化。當(dāng)系統(tǒng)重繪時(shí),將執(zhí)行上述過(guò)渡動(dòng)畫(huà)。這里,不斷地添加與刪除View,觀察過(guò)渡效果。

    mWithoutTransition = new TransitionSet().addTransition(new ChangeBounds()).addTransition(new Fade());
    if (!mIsRemoved) {
        TransitionManager.beginDelayedTransition(mWithoutRoot, mWithoutTransition);
        mWithoutRoot.removeView(mRight);
        mIsRemoved = true;
    } else {
        TransitionManager.beginDelayedTransition(mWithoutRoot, mWithoutTransition);
        mWithoutRoot.addView(mRight);
        mIsRemoved = false;
    }
Without scenes.gif.gif

View的增刪再也不用那么無(wú)趣了。

四、局限性

當(dāng)然,雖然過(guò)渡框架這么有用,它也有使用限制。

在SurfaceView上應(yīng)用過(guò)渡動(dòng)畫(huà),可能就不會(huì)正常實(shí)現(xiàn)。前面已經(jīng)看到,過(guò)渡是與View樹(shù)的變化是息息相關(guān)的,View樹(shù)的變化引起View樹(shù)重繪,然后過(guò)渡才會(huì)執(zhí)行。View的動(dòng)畫(huà)是在UI線程,而SurfaceView實(shí)例的更新卻在非UI線程,所以很可能造成不一致問(wèn)題。

TextureView上應(yīng)用特定的過(guò)渡動(dòng)畫(huà),也可能不能正常實(shí)現(xiàn)。

AdapterView系的類,也不能正確實(shí)現(xiàn)過(guò)渡動(dòng)畫(huà),因?yàn)樗鼈兣c過(guò)渡框架不兼容。

另外,如果在過(guò)渡過(guò)程中改變一個(gè)TextView的大小,文本會(huì)在完全改變大小之前跳到新位置,也沒(méi)法有效實(shí)現(xiàn)過(guò)渡。官方建議,應(yīng)該避免過(guò)渡過(guò)程中改變包含文本TextView的View的尺寸。

五、自定義過(guò)渡

介紹完過(guò)渡框架在實(shí)際應(yīng)用中的基本使用,是時(shí)候切入本文重點(diǎn)了 —— 自定義過(guò)渡。

實(shí)現(xiàn)

該如何實(shí)現(xiàn)呢?

過(guò)渡框架提供了一個(gè)類Transition,它是一個(gè)抽象類,ChangeBoundsChangeImageTransform等等這些都是它的實(shí)現(xiàn)子類。類似地,自定義過(guò)渡動(dòng)畫(huà),只需要繼承Transition,并實(shí)現(xiàn)下面幾個(gè)關(guān)鍵方法即可。

    public class CustomTransition extends Transition {
    
        @Override
        public void captureStartValues(TransitionValues values) {
            //TODO 捕獲需要關(guān)注的起始場(chǎng)景的相關(guān)屬性值
        }
    
        @Override
        public void captureEndValues(TransitionValues values) {
            //TODO 捕獲需要關(guān)注的終止場(chǎng)景的相關(guān)屬性值
        }
    
        @Override
        public Animator createAnimator(ViewGroup sceneRoot,
                                       TransitionValues startValues,
                                       TransitionValues endValues) {
            //TODO 根據(jù)起始、終止場(chǎng)景的屬性值,決定是否生成過(guò)渡動(dòng)畫(huà)及動(dòng)畫(huà)效果的實(shí)現(xiàn)
        }
    }

捕獲屬性值

過(guò)渡框架的動(dòng)畫(huà)來(lái)自屬性動(dòng)畫(huà)系統(tǒng)(Property Animation),因?yàn)閷傩詣?dòng)畫(huà)是根據(jù)View的屬性值在一段時(shí)間的變化來(lái)實(shí)現(xiàn)的,那么,過(guò)渡框架自然也是需要確定起止屬性值的。

對(duì)于一個(gè)過(guò)渡,動(dòng)畫(huà)所需要的屬性是確定的,所以過(guò)渡框架只提供這些需要的屬性到過(guò)渡中(區(qū)別于屬性動(dòng)畫(huà)系統(tǒng)),然后通過(guò)捕獲屬性的回調(diào)方法來(lái)保存屬性值。

起始屬性值

起始屬性值在回調(diào)方法captureStartValues(TransitionValues transitionValues)中設(shè)置,然后將View的起始屬性值傳遞給過(guò)渡框架。

其中,參數(shù)中的TransitionValues類包含兩個(gè)域:

  • view:View類型,保存所關(guān)注的View的引用
  • values:Map類型,保存所有的屬性值

為避免沖突,官方建議values的鍵的命名按如下規(guī)則:

package_name:transition_name:property_name

終止屬性值

終止屬性值在回調(diào)方法captureEndValues(TransitionValues transitionValues)中設(shè)置。此方法之于captureStartValues中的參數(shù),雖然包含相同的view引用,但是卻維護(hù)不同的values值,獨(dú)立存在。

動(dòng)畫(huà)適配器

自定義過(guò)渡動(dòng)畫(huà)需要實(shí)現(xiàn)的第三個(gè)方法就是動(dòng)畫(huà)適配器了。

    Animator createAnimator(ViewGroup sceneRoot,
                               TransitionValues startValues,
                               TransitionValues endValues)

參數(shù)sceneRoot是根View,startValues和endValues分別是前面捕獲生成的起始和終止屬性值。如果確定起始和終止的屬性值有變化,那就可以生成自定義動(dòng)畫(huà),并返回給過(guò)渡框架,框架將添加動(dòng)畫(huà)時(shí)長(zhǎng)、插值器等等并啟動(dòng)此動(dòng)畫(huà)。默認(rèn)返回null,框架不作操作。

createAnimator()被回調(diào)的次數(shù),依賴于起始與終止場(chǎng)景中的“變化數(shù)” —— 也就是說(shuō),到底我們需要改變多少個(gè)目標(biāo)對(duì)象(畢竟,不同目標(biāo),動(dòng)畫(huà)可能并不相同)。比如,起始場(chǎng)景有5個(gè)目標(biāo),其中3個(gè)留至結(jié)束,2個(gè)刪除。終止場(chǎng)景有4個(gè)目標(biāo),3個(gè)來(lái)自于起始場(chǎng)景,1個(gè)是新增,那么,createAnimator()將被回調(diào)3 + 2 + 1=6次。

自定義過(guò)渡動(dòng)畫(huà)

說(shuō)了這么多,我們來(lái)看看具體的代碼實(shí)現(xiàn)。

背景顏色過(guò)渡

首先來(lái)看一個(gè)官方的例子,此過(guò)渡用于改變背景顏色(ChangeColor)。

    /*
     * Copyright (C) 2014 The Android Open Source Project
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    package com.djx.customtransition;
    
    import android.animation.Animator;
    import android.animation.ArgbEvaluator;
    import android.animation.ValueAnimator;
    import android.graphics.drawable.ColorDrawable;
    import android.graphics.drawable.Drawable;
    import android.transition.Transition;
    import android.transition.TransitionValues;
    import android.view.View;
    import android.view.ViewGroup;
    
    public class ChangeColor extends Transition {
    
        /** Key to store a color value in TransitionValues object */
        private static final String PROPNAME_BACKGROUND = "customtransition:change_color:background";
    
        /**
         * Convenience method: Add the background Drawable property value
         * to the TransitionsValues.value Map for a target.
         */
        private void captureValues(TransitionValues values) {
            // Capture the property values of views for later use
            values.values.put(PROPNAME_BACKGROUND, values.view.getBackground());
        }
    
        @Override
        public void captureStartValues(TransitionValues transitionValues) {
            captureValues(transitionValues);
        }
    
        // Capture the value of the background drawable property for a target in the ending Scene.
        @Override
        public void captureEndValues(TransitionValues transitionValues) {
            captureValues(transitionValues);
        }
    
        // Create an animation for each target that is in both the starting and ending Scene. For each
        // pair of targets, if their background property value is a color (rather than a graphic),
        // create a ValueAnimator based on an ArgbEvaluator that interpolates between the starting and
        // ending color. Also create an update listener that sets the View background color for each
        // animation frame
        @Override
        public Animator createAnimator(ViewGroup sceneRoot,
                                       TransitionValues startValues, TransitionValues endValues) {
            // This transition can only be applied to views that are on both starting and ending scenes.
            if (null == startValues || null == endValues) {
                return null;
            }
            // Store a convenient reference to the target. Both the starting and ending layout have the
            // same target.
            final View view = endValues.view;
            // Store the object containing the background property for both the starting and ending
            // layouts.
            Drawable startBackground = (Drawable) startValues.values.get(PROPNAME_BACKGROUND);
            Drawable endBackground = (Drawable) endValues.values.get(PROPNAME_BACKGROUND);
            // This transition changes background colors for a target. It doesn't animate any other
            // background changes. If the property isn't a ColorDrawable, ignore the target.
            if (startBackground instanceof ColorDrawable && endBackground instanceof ColorDrawable) {
                ColorDrawable startColor = (ColorDrawable) startBackground;
                ColorDrawable endColor = (ColorDrawable) endBackground;
                // If the background color for the target in the starting and ending layouts is
                // different, create an animation.
                if (startColor.getColor() != endColor.getColor()) {
                    // Create a new Animator object to apply to the targets as the transitions framework
                    // changes from the starting to the ending layout. Use the class ValueAnimator,
                    // which provides a timing pulse to change property values provided to it. The
                    // animation runs on the UI thread. The Evaluator controls what type of
                    // interpolation is done. In this case, an ArgbEvaluator interpolates between two
                    // #argb values, which are specified as the 2nd and 3rd input arguments.
                    ValueAnimator animator = ValueAnimator.ofObject(new ArgbEvaluator(),
                            startColor.getColor(), endColor.getColor());
                    // Add an update listener to the Animator object.
                    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
                            Object value = animation.getAnimatedValue();
                            // Each time the ValueAnimator produces a new frame in the animation, change
                            // the background color of the target. Ensure that the value isn't null.
                            if (null != value) {
                                view.setBackgroundColor((Integer) value);
                            }
                        }
                    });
                    // Return the Animator object to the transitions framework. As the framework changes
                    // between the starting and ending layouts, it applies the animation you've created.
                    return animator;
                }
            }
            // For non-ColorDrawable backgrounds, we just return null, and no animation will take place.
            return null;
        }

非常簡(jiǎn)單,背景的變化通過(guò)ValueAnimator的更新監(jiān)聽(tīng)進(jìn)行控制。

使用起來(lái)也和系統(tǒng)內(nèi)置過(guò)渡效果一樣。

在AS中生成一個(gè)簡(jiǎn)單工程,在主界面MainActivity添加一個(gè)文本“Hello World”,然后SecondActivity中同樣添加一個(gè)文本“Hello World”,設(shè)置Activity的共享過(guò)渡,并添加過(guò)渡動(dòng)畫(huà)為這個(gè)ChangeColor。

SecondActivity.java

package com.djx.customtransition;

import android.app.SharedElementCallback;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.transition.ChangeBounds;
import android.transition.Transition;
import android.transition.TransitionSet;
import android.view.View;
import android.widget.TextView;

import java.util.List;

public class SecondActivity extends AppCompatActivity {

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

        getWindow().setSharedElementEnterTransition(getTransition());
        setEnterSharedElementCallback(new SharedElementCallback() {
            @Override
            public void onSharedElementStart(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
                TextView target = (TextView) sharedElements.get(0);
                target.setBackground(new ColorDrawable(getColor(R.color.bg_start)));
            }

            @Override
            public void onSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
                TextView target = (TextView) sharedElements.get(0);
                target.setBackground(new ColorDrawable(getColor(R.color.bg_end)));
            }
        });
    }

    private Transition getTransition() {
        TransitionSet set = new TransitionSet();

        ChangeBounds bounds = new ChangeBounds();
        bounds.addTarget(R.id.hello);
        set.addTransition(bounds);

        ChangeColor bg = new ChangeColor();
        bg.addTarget(R.id.hello);
        set.addTransition(bg);

        return set;
    }
}

關(guān)于上面的代碼,需要注意兩點(diǎn)。

第一,為了設(shè)置正確的起始和終止場(chǎng)景屬性值,共享過(guò)渡的回調(diào)SharedElementCallback必須添加,因?yàn)閹讉€(gè)關(guān)鍵方法的回調(diào)順序?yàn)椋?/p>

onSharedElementStart -> captureStartValues -> onSharedElementEnd -> captureEndValues

如果不在onSharedElementStartonSharedElementEnd中設(shè)置正確的始末屬性值,那過(guò)渡無(wú)法知曉屬性值的變化,createAnimator不會(huì)回調(diào),過(guò)渡動(dòng)畫(huà)也就無(wú)從談起。

第二,過(guò)渡動(dòng)畫(huà)不僅添加了自定義的ChangeColor,還添加了ChangeBounds。單獨(dú)一個(gè)ChangeColor是無(wú)法生效的。

在主界面中啟動(dòng)SecondActivity,看下效果

ChangeColor.gif

文本顏色過(guò)渡

前面官方的例子,自然成功實(shí)現(xiàn),沒(méi)有問(wèn)題。現(xiàn)在依葫蘆畫(huà)瓢,來(lái)實(shí)現(xiàn)一個(gè)文本顏色的過(guò)渡動(dòng)畫(huà)。代碼如下:

    package com.djx.customtransition;
    
    import android.animation.Animator;
    import android.animation.ArgbEvaluator;
    import android.animation.ObjectAnimator;
    import android.content.Context;
    import android.transition.Transition;
    import android.transition.TransitionValues;
    import android.util.AttributeSet;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.TextView;
    
    public class ChangeTextColor extends Transition {
    
        private static final String PROPNAME_TEXT_COLOR = "com.djx.customtransition:ChangeTextColor:textColor";
    
        public ChangeTextColor() {
        }
    
        public ChangeTextColor(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        private void captureValues(TransitionValues transitionValues) {
            if (transitionValues.view instanceof TextView) {
                transitionValues.values.put(PROPNAME_TEXT_COLOR,
                        ((TextView) transitionValues.view).getCurrentTextColor());
            }
        }
    
        @Override
        public void captureStartValues(TransitionValues transitionValues) {
            captureValues(transitionValues);
        }
    
        @Override
        public void captureEndValues(TransitionValues transitionValues) {
            captureValues(transitionValues);
        }
    
        @Override
        public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
                                       TransitionValues endValues) {
            if (startValues == null || endValues == null) {
                return null;
            }
            final View view = endValues.view;
            if (view instanceof TextView) {
                TextView textView = (TextView) view;
                int start = (Integer) startValues.values.get(PROPNAME_TEXT_COLOR);
                int end = (Integer) endValues.values.get(PROPNAME_TEXT_COLOR);
                if (start != end) {
                    return ObjectAnimator.ofObject(textView, "textColor",
                            new ArgbEvaluator(), start, end);
                }
            }
            return null;
        }
    }

也非常簡(jiǎn)單,顏色的變化直接使用ObjectAnimator生成了一個(gè)屬性動(dòng)畫(huà)。在SecondActivity中,注釋掉前面的ChangeColor過(guò)渡,添加ChangeTextColor。

    private Transition getTransition() {
            TransitionSet set = new TransitionSet();
            
            ChangeBounds bounds = new ChangeBounds();
            bounds.addTarget(R.id.hello);
            set.addTransition(bounds);
    
    //        ChangeColor bg = new ChangeColor();
    //        bg.addTarget(R.id.hello);
    //        set.addTransition(bg);
    
            ChangeTextColor textColor = new ChangeTextColor();
            textColor.addTarget(R.id.hello);
            set.addTransition(textColor);
            
            return set;
        }

同時(shí),在onSharedElementStartonSharedElementEnd中分別設(shè)置起始和終止的文本顏色值。

        setEnterSharedElementCallback(new SharedElementCallback() {
                @Override
                public void onSharedElementStart(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
                    TextView target = (TextView) sharedElements.get(0);
        //            target.setBackground(new ColorDrawable(getColor(R.color.bg_start)));
                    target.setTextColor(getColor(R.color.colorPrimary));
                }
    
                @Override
                public void onSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
                    TextView target = (TextView) sharedElements.get(0);
    //                target.setBackground(new ColorDrawable(getColor(R.color.bg_end)));
                    target.setTextColor(getColor(R.color.colorAccent));
                }
            });

主界面中啟動(dòng)SecondActivity,效果如下

ChangeTextColor.gif.gif

同時(shí)添加ChangeColor和ChangeTextColor,主界面中啟動(dòng)SecondActivity,效果正常

ChangeColor & ChangeTextColor.gif.gif

文本大小過(guò)渡

既然背景顏色和文本顏色都可以過(guò)渡,那么文本大小呢?

同樣,實(shí)現(xiàn)一個(gè)文本大小過(guò)渡ChangeTextSize

package com.djx.customtransition;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.transition.Transition;
import android.transition.TransitionValues;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class ChangeTextSize extends Transition {

    private static final String PROPNAME_TEXT_SIZE = "com.djx.customtransition:ChangeTextSize:textSize";

    private void captureValues(TransitionValues transitionValues) {
        if (transitionValues.view instanceof TextView) {
            transitionValues.values.put(PROPNAME_TEXT_SIZE,
                    ((TextView) transitionValues.view).getTextSize());
        }
    }

    @Override
    public void captureStartValues(TransitionValues transitionValues) {
        captureValues(transitionValues);
    }

    @Override
    public void captureEndValues(TransitionValues transitionValues) {
        captureValues(transitionValues);
    }

    @Override
    public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
        if (startValues == null || endValues == null) {
            return null;
        }
        final View view = endValues.view;
        if (view instanceof TextView) {
            final TextView textView = (TextView) view;
            float start = (Float) startValues.values.get(PROPNAME_TEXT_SIZE);
            float end = (Float) endValues.values.get(PROPNAME_TEXT_SIZE);
            if (start != end) {
                ValueAnimator animator = ValueAnimator.ofFloat(start, end);
                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        Object value = animation.getAnimatedValue();
                        if (null != value) {
                            textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (Float) value);
                        }
                    }
                });
                return animator;
            }
        }
        return null;
    }
}

修改SecondActivity中的“Hello Wolrd”文本大小,然后把此過(guò)渡添加至前面的過(guò)渡組合中

ChangeTextSize.gif

咦?好像不是那么回事了!仔細(xì)看看,其實(shí)文本的大小,還是按預(yù)期過(guò)渡的,是一個(gè)逐漸變大變小的過(guò)程。但是,主界面到二界面時(shí),直到過(guò)渡的最后一刻,控件的尺寸才變成實(shí)際大小,即文本變大后的實(shí)際控件尺寸。

這就是前面官方已經(jīng)提到過(guò)的關(guān)于包含文本控件時(shí)的過(guò)渡局限性了。

有沒(méi)有辦法解決?

當(dāng)然有!國(guó)外的大牛已經(jīng)搞定了,膜拜之。

問(wèn)題原因已經(jīng)知道,就是過(guò)渡過(guò)程中,文本在變大,但是View本身的尺寸卻沒(méi)有跟隨著變大 —— 因?yàn)檫^(guò)渡框架不知道應(yīng)該改變控件尺寸?。∧敲?,就在設(shè)置終止文本大小值的時(shí)候,也把控件的尺寸修改一下可好?看看代碼上怎么做:

public void onSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
                TextView target = (TextView) sharedElements.get(0);

                // Record the TextView's old width/height.
                int oldWidth = target.getMeasuredWidth();
                int oldHeight = target.getMeasuredHeight();
                
                target.setTextColor(getColor(R.color.colorAccent));
                target.setBackground(new ColorDrawable(getColor(R.color.bg_end)));
                target.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimensionPixelSize(R.dimen.end_text_size));

                // Re-measure the TextView (since the text size has changed).
                int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
                int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
                target.measure(widthSpec, heightSpec);

                // Record the TextView's new width/height.
                int newWidth = target.getMeasuredWidth();
                int newHeight = target.getMeasuredHeight();

                // Set the new bounds
                int widthDiff = newWidth - oldWidth;
                int heightDiff = newHeight - oldHeight;
                target.layout(target.getLeft(), target.getTop(),
                        target.getRight() + widthDiff, target.getBottom() + heightDiff);
            }

首先保存一個(gè)原始尺寸,設(shè)置新的文本大小后重新測(cè)量,然后根據(jù)始末尺寸差值,設(shè)置過(guò)渡結(jié)束時(shí)的控件尺寸。

Threesome.gif

嗯,原生過(guò)渡框架所謂的局限性也不再局限了,完美!

六、小結(jié)

過(guò)渡框架中的場(chǎng)景過(guò)渡和無(wú)場(chǎng)景過(guò)渡,使用起來(lái)非常方便,大家可以發(fā)散思維把它們應(yīng)用到除這里講到的更多的應(yīng)用場(chǎng)景中去。

自定義過(guò)渡時(shí),從原生例子入手,但是過(guò)程中失敗很多,也踩了很多雷,多是因?yàn)槔斫獠煌笍兀阶詈蟾忝靼椎臅r(shí)候發(fā)現(xiàn),其實(shí)官書(shū)API或者文檔已經(jīng)說(shuō)清楚了。

路漫漫其修遠(yuǎn)兮??!

照樣,例子奉上,僅供參考。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容