1. 程式人生 > >利用Transition製作炫酷的切換動畫

利用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 TransitionWith Return Transition
Enter: Fade InEnter: Fade In
Exit: Fade OutExit: Slide out
transition_fadetransition_fade2

二  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 Name
layout/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 享元方式啟動另一個Fragment
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);

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 TrueOverlap False
Fragment_2 appears on top of Fragment_1Fragment_2 waits until Fragment_1 is gone
shared_element_overlapshared_element_no_overlap
五 在同一個Activity中,根據Activity佈局檔案中view元素的變動來進行Transition動畫

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