前不久,我寫(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è)步驟:
- 創(chuàng)建起始場(chǎng)景、終止場(chǎng)景
- 設(shè)置過(guò)渡動(dòng)畫(huà)
- 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());
效果如下:

是不是很方便?雖然用一般的屬性動(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è)步驟:
- 調(diào)用TransitionManager.beginDelayedTransition()保存View樹(shù)狀態(tài)并設(shè)置過(guò)渡
- View樹(shù)發(fā)生改變,過(guò)渡框架記錄新的狀態(tài)
- 系統(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;
}

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è)抽象類,ChangeBounds、ChangeImageTransform等等這些都是它的實(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
如果不在onSharedElementStart和onSharedElementEnd中設(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,看下效果

文本顏色過(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í),在onSharedElementStart和onSharedElementEnd中分別設(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,效果如下

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

文本大小過(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ò)渡組合中

咦?好像不是那么回事了!仔細(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í)的控件尺寸。

嗯,原生過(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)兮??!
照樣,例子奉上,僅供參考。