利用Transition製作炫酷的切換動畫
前言
使用Transition動畫框架,可以幫你做到:
1不同Activity切換的時候,根據每個activity對應的layout內容的不同做整體的場景變換的動畫。
2 不同activity切換的時候,不同activity對應的layout有相同的元素,比如activity1中有一個button,activity2有一個相同的button,transition框架可以在整體場景變換的同時,特別照顧到這個button,讓動畫變換的過程中,這個button享有視覺連貫性。
3 在相同的activity裡,當同一個view內容發生變化,比如在程式碼中removie或者add了某個ui元素,或者更改了某個已有元素的尺寸,顏色資訊,Transition動畫框架也可以根據這個變化做動畫變換。
一 不同Activity切換的Transition動畫
當從activity1跳轉到activity2的時候,1會執行exitTransition, 而2會執行enterTransition,你可以在程式碼中使用
getWindow().setExitTransition(slide/fade...);
getWindow().setEnterTransition(slide/fade...);
為這個activity設定它進入和退出時需要用到的過場動畫(就是方法引數),谷歌為我們預設了幾個transition,分別是Explode,Slide,Fade。可以看一下他們的效果
在定義Explode,Slide,Fade這幾個動畫效果的時候,可以使用xml也可以直接在程式碼中定義
1 使用xml
在res/transtion資料夾下定義一個xml檔案
res/transition/activity_fade.xml
<?xml version="1.0" encoding="utf-8"?>
<fade xmlns:android="http://schemas.android.com/apk/res/"
android:duration="1000"/>
res/transition/activity_slide.xml <?xml version="1.0" encoding="utf-8"?> <slide xmlns:android="http://schemas.android.com/apk/res/" android:duration="1000"/>
然後需要在程式碼中使用TransitionInflator方法來載入這個xml檔案生成對應的動畫效果。
MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transition);
setupWindowAnimations();
}
private void setupWindowAnimations() {
Slide slide = TransitionInflater.from(this).inflateTransition(R.transition.activity_slide);
getWindow().setExitTransition(slide);
}
TransitionActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transition);
setupWindowAnimations();
}
private void setupWindowAnimations() {
Fade fade = TransitionInflater.from(this).inflateTransition(R.transition.activity_fade);
getWindow().setEnterTransition(fade);
}
2 在程式碼中定義MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transition);
setupWindowAnimations();
}
private void setupWindowAnimations() {
Slide slide = new Slide();
slide.setDuration(1000);
getWindow().setExitTransition(slide);
}
TransitionActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transition);
setupWindowAnimations();
}
private void setupWindowAnimations() {
Fade fade = new Fade();
fade.setDuration(1000);
getWindow().setEnterTransition(fade);
}
效果如下在這個過程中
首先Actvity1啟動了Activity2
Transition框架發現Activity1退出,為activity1裡的可見ui元件執行Slide動畫
Transition框架發現Activity2退出,為activity2裡的可見ui元件執行Fade動畫
當按下返回鍵,如果我們沒有為其設定returnTransition和reenterTransition選項的話,Transition框架會對應的activity執行我們為其設定的enter和exit動畫運動過程相反的動畫作為預設選項。
3 ReturnTransition & ReenterTransition
他們是enterTransition和exitTransition的對應我們把之前那個例子改一下,為TransitionActivity新增一個return動畫,用的是Slide,那麼當我們按下返回鍵的時候,會看到一個slide效果,而不是fade(之前設定的enter動畫的倒放)效果。
TransitionActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transition);
setupWindowAnimations();
}
private void setupWindowAnimations() {
Fade fade = new Fade();
fade.setDuration(1000);
getWindow().setEnterTransition(fade);
Slide slide = new Slide();
slide.setDuration(1000);
getWindow().setReturnTransition(slide);
}
下面可以看到 沒有設定return動畫和設定了return動畫的區別Without Return Transition | With Return Transition |
---|---|
Enter: Fade
In | Enter: Fade In |
Exit: Fade
Out | Exit: Slide out |
二 Activity之間的享元切換
你可以通過為兩個不同activity中某個view設定一個唯一的統一的標識,告訴Transition這個view是這兩aictivity共享的元素,變換的時候要額外照顧到,那麼transition動畫框架就會在場景切換的時候,讓這個view順滑的從前一個activity轉移到另一個activity(注意,這裡的轉移並不是真正的轉移,這兩個item並不是同一個物件,他們是彼此獨立的兩個view)
1 設定windowContentTransition
首先要對app的style檔案做一些修改
values/styles.xml
<style name="MaterialAnimations" parent="@style/Theme.AppCompat.Light.NoActionBar">
...
<item name="android:windowContentTransitions">true</item
...
</style>
還可以在這裡定義整個app預設的enter, exit 和 shared element transitions動畫<style name="MaterialAnimations" parent="@style/Theme.AppCompat.Light.NoActionBar">
...
<!-- specify enter and exit transitions -->
<item name="android:windowEnterTransition">@transition/explode</item>
<item name="android:windowExitTransition">@transition/explode</item>
<!-- specify shared element transitions -->
<item name="android:windowSharedElementEnterTransition">@transition/changebounds</item>
<item name="android:windowSharedElementExitTransition">@transition/changebounds</item>
...
</style>
2在佈局檔案中為享元view定義相同的Transition Name,通過android:transitioName來定義,享元之間id和屬性可以不同,但是TransitionName必須相同。
layout/activity_a.xml
<ImageView
android:id="@+id/small_blue_icon"
style="@style/MaterialAnimations.Icon.Small"
android:src="@drawable/circle"
android:transitionName="@string/blue_name" />
layout/activity_b.xml
<ImageView
android:id="@+id/big_blue_icon"
style="@style/MaterialAnimations.Icon.Big"
android:src="@drawable/circle"
android:transitionName="@string/blue_name" />
c 通過享元方式啟動另一個activity
通過ActivityOptions.makeSceneTransitionAnimation()方法來定義享元具體是哪個view和Transition Name
MainActivity.java
blueIconImageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent i = new Intent(MainActivity.this, SharedElementActivity.class);
View sharedView = blueIconImageView;
String transitionName = getString(R.string.blue_name);
ActivityOptions transitionActivityOptions = ActivityOptions.makeSceneTransitionAnimation(MainActivity.this, sharedView, transitionName);
startActivity(i, transitionActivityOptions.toBundle());
}
});
最終效果如下圖三 在Fragment之間進行享元切換動畫
前兩步和上面基本相同
1 設定windowContentTransition
values/styles.xml
<style name="MaterialAnimations" parent="@style/Theme.AppCompat.Light.NoActionBar">
...
<item name="android:windowContentTransitions">true</item>
...
</style>
2 在佈局檔案中為享元view定義相同的Transition Namelayout/fragment_a.xml
<ImageView
android:id="@+id/small_blue_icon"
style="@style/MaterialAnimations.Icon.Small"
android:src="@drawable/circle"
android:transitionName="@string/blue_name" />
layout/fragment_b.xml
<ImageView
android:id="@+id/big_blue_icon"
style="@style/MaterialAnimations.Icon.Big"
android:src="@drawable/circle"
android:transitionName="@string/blue_name" />
3 享元方式啟動另一個FragmentFragmentB fragmentB = FragmentB.newInstance(sample);
// Defines enter transition for all fragment views
Slide slideTransition = new Slide(Gravity.RIGHT);
slideTransition.setDuration(1000);
sharedElementFragment2.setEnterTransition(slideTransition);
// Defines enter transition only for shared element
ChangeBounds changeBoundsTransition = TransitionInflater.from(this).inflateTransition(R.transition.change_bounds);
fragmentB.setSharedElementEnterTransition(changeBoundsTransition);
getFragmentManager().beginTransaction()
.replace(R.id.content, fragmentB)
.addSharedElement(blueView, getString(R.string.blue_name))
.commit();
最終結果如下四 疊加Transition動畫效果
你可以設定是否讓exit動畫和enter動畫疊加在一起執行
如果設定為true 那麼enter動畫會立刻執行
如果設定為false 那麼exter動畫會等到exit動畫執行完畢之後再執行
這對Fragment和Activity之間的享元切換都有效
FragmentB fragmentB = FragmentB.newInstance(sample);
// Defines enter transition for all fragment views
Slide slideTransition = new Slide(Gravity.RIGHT);
slideTransition.setDuration(1000);
sharedElementFragment2.setEnterTransition(slideTransition);
// Defines enter transition only for shared element
ChangeBounds changeBoundsTransition = TransitionInflater.from(this).inflateTransition(R.transition.change_bounds);
fragmentB.setSharedElementEnterTransition(changeBoundsTransition);
// Prevent transitions for overlapping
fragmentB.setAllowEnterTransitionOverlap(overlap);
fragmentB.setAllowReturnTransitionOverlap(overlap);
getFragmentManager().beginTransaction()
.replace(R.id.content, fragmentB)
.addSharedElement(blueView, getString(R.string.blue_name))
.commit();
動畫是否疊加的具體區別見下面的動圖
Overlap True | Overlap False |
---|---|
Fragment_2 appears on top of Fragment_1 | Fragment_2 waits until Fragment_1 is gone |
1 Scene
在activity對應的佈局檔案中,定義一個佔位用的檢視,一般用framelayout,稱之為稍後場景變換動畫執行
所在的根檢視rootView,然後在layout資料夾下,新建幾個layout檔案,每一個layout檔案對應的檢視都是一個scene,
可以這麼理解,我們打算在Activity對應的介面檢視的上半部分放置即將執行的動畫,這個上半部分我們用幀佈局
佔位,稱之為rootView,然後動畫效果是一個小球從左邊移動到右邊,那麼我們只需要在layout資料夾下面
定義兩個layout檔案,一個layout檔案中,小球在左邊,另一個檔案中小球在右邊。這兩個layout,在程式碼中通過
Scene.getSceneForLayout(rootView,R.layout.***,this)生成一個Scene,此時,rootView和每一個scene的連結關係
就建立起來了。在通過TransitionManager.go方法就可以讓動畫在rootView中執行起來。
還是看一下程式碼吧
scene1 = Scene.getSceneForLayout(sceneRoot, R.layout.activity_animations_scene1, this);
scene2 = Scene.getSceneForLayout(sceneRoot, R.layout.activity_animations_scene2, this);
scene3 = Scene.getSceneForLayout(sceneRoot, R.layout.activity_animations_scene3, this);
scene4 = Scene.getSceneForLayout(sceneRoot, R.layout.activity_animations_scene4, this);
(...)
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button1:
TransitionManager.go(scene1, new ChangeBounds());
break;
case R.id.button2:
TransitionManager.go(scene2, TransitionInflater.from(this).inflateTransition(R.transition.slide_and_changebounds));
break;
case R.id.button3:
TransitionManager.go(scene3, TransitionInflater.from(this).inflateTransition(R.transition.slide_and_changebounds_sequential));
break;
case R.id.button4:
TransitionManager.go(scene4, TransitionInflater.from(this).inflateTransition(R.transition.slide_and_changebounds_sequential_with_interpolators));
break;
}
}
上面的程式碼就會在同一個Activity中的四個Scene中間生成對應的動畫,而且每個動畫都不同2 佈局檔案中View屬性的變動
Transition動畫還可以檢測到同一個佈局檔案中某些檢視元素的屬性變化,然後做相應的動畫效果,你可以隨意在程式碼中修改
你需要的一切變動,比如顏色,大小,位置之類的,之後的動畫Transition會自動幫你實現。
做到這些 只需要
a 先呼叫BeginDelayTransition方法
TransitionManager.beginDelayedTransition(sceneRoot);
b 更改layout佈局中的view屬性ViewGroup.LayoutParams params = greenIconView.getLayoutParams();
params.width = 200;
greenIconView.setLayoutParams(params);
更改greenIconView的width時,會觸發view的onMeasure()方法,此時Transition框架會記錄下該view的起始width值並做出相應的動畫
六 享元動畫+circle Reveal動畫
享元方式的過場動畫前面已經提到了,而CircleReveal是屬性動畫的一種,和享元切換一樣都是安卓5.0之後引入的新特性
,對了忘了提,上面享元切換和這個circleReveal動畫在執行前最好都加上一個版本判斷
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
}
CircleReveal實現圓形縮放效果,可用來突出顯示某個部分。他和享元切換一起使用可以引導使用者的視覺焦點,給他的點選以反饋。上面的動畫 依次執行了
從MainActivity到RevealActivity的一個享元切換動畫
在RevealActivity中有一個監聽器,當它監聽到RevealActivity中的享元切換動畫執行完之後,為Toobar執行了一個CircleReveal屬性動畫
同時為RevealActivity中其他檢視執行了一個放大的屬性動畫
Listen to shared element enter transition end
Transition transition = TransitionInflater.from(this).inflateTransition(R.transition.changebounds_with_arcmotion);
getWindow().setSharedElementEnterTransition(transition);
transition.addListener(new Transition.TransitionListener() {
@Override
public void onTransitionEnd(Transition transition) {
animateRevealShow(toolbar);
animateButtonsIn();
}
(...)
});
Reveal Toolbar
private void animateRevealShow(View viewRoot) {
int cx = (viewRoot.getLeft() + viewRoot.getRight()) / 2;
int cy = (viewRoot.getTop() + viewRoot.getBottom()) / 2;
int finalRadius = Math.max(viewRoot.getWidth(), viewRoot.getHeight());
Animator anim = ViewAnimationUtils.createCircularReveal(viewRoot, cx, cy, 0, finalRadius);
viewRoot.setVisibility(View.VISIBLE);
anim.setDuration(1000);
anim.setInterpolator(new AccelerateInterpolator());
anim.start();
}
Scale up activity layout views
private void animateButtonsIn() {
for (int i = 0; i < bgViewGroup.getChildCount(); i++) {
View child = bgViewGroup.getChildAt(i);
child.animate()
.setStartDelay(100 + i * DELAY)
.setInterpolator(interpolator)
.alpha(1)
.scaleX(1)
.scaleY(1);
}
}
更多CircleReveal動畫的例子你可以隨意設定你想要的Reveal動畫,但前提是這些動畫可以有效的告知使用者 app對使用者的點選都做了什麼反饋
1 從目標檢視中心開始動畫
int cx = (viewRoot.getLeft() + viewRoot.getRight()) / 2;
int cy = viewRoot.getTop();
int finalRadius = Math.max(viewRoot.getWidth(), viewRoot.getHeight());
Animator anim = ViewAnimationUtils.createCircularReveal(viewRoot, cx, cy, 0, finalRadius);
viewRoot.setBackgroundColor(color);
anim.start();
2 從目標檢視頂部開始 並結合其他屬性動畫
int cx = (viewRoot.getLeft() + viewRoot.getRight()) / 2;
int cy = (viewRoot.getTop() + viewRoot.getBottom()) / 2;
int finalRadius = Math.max(viewRoot.getWidth(), viewRoot.getHeight());
Animator anim = ViewAnimationUtils.createCircularReveal(viewRoot, cx, cy, 0, finalRadius);
viewRoot.setBackgroundColor(color);
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
animateButtonsIn();
}
});
anim.start();
3 從點選位置開始動畫
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
if (view.getId() == R.id.square_yellow) {
revealFromCoordinates(motionEvent.getRawX(), motionEvent.getRawY());
}
}
return false;
}
private Animator animateRevealColorFromCoordinates(int x, int y) {
float finalRadius = (float) Math.hypot(viewRoot.getWidth(), viewRoot.getHeight());
Animator anim = ViewAnimationUtils.createCircularReveal(viewRoot, x, y, 0, finalRadius);
viewRoot.setBackgroundColor(color);
anim.start();
}
4 結合Transition動畫
Transition transition = TransitionInflater.from(this).inflateTransition(R.transition.changebounds_with_arcmotion);
transition.addListener(new Transition.TransitionListener() {
@Override
public void onTransitionEnd(Transition transition) {
animateRevealColor(bgViewGroup, R.color.red);
}
(...)
});
TransitionManager.beginDelayedTransition(bgViewGroup, transition);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
btnRed.setLayoutParams(layoutParams);