Material Design之定製動畫--觸摸反饋,循環揭露,轉場動畫,共享元素和曲線運動


先貼下官網的API

觸摸反饋:

在按鈕屬性中添加
android:background="?android:attr/selectableItemBackground"
則點擊按鈕的時候會有水波紋(有邊界),波紋不超過設置的控件大小,如果控件爲矩形則波紋不會超過矩形大小
如果添加:
android:background="?android:attr/selectableItemBackgroundBorderless"
則點擊效果爲無界水波紋,範圍是一個圓形
循環揭露:

同樣我們可以用 ViewAnimationUtils.createCircularReveal()方式來實現這種水波紋漸變顯示效果,
這種漸變式擴散的動畫很有用,我們會以一種Activity轉場動畫的形式將其添加,一會演示給大家。
private void doCircle(View myView)
{
    // get the center for the clipping circle
    int cx = (myView.getLeft() + myView.getRight()) / 2;
    int cy = (myView.getTop() + myView.getBottom()) / 2;

    int finalRadius = Math.max(myView.getWidth(), myView.getHeight());

    Animator anim =
            ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0, finalRadius);
    anim.setDuration(3000);
    myView.setVisibility(View.VISIBLE);
    anim.start();
}
同樣點擊控件水波紋收縮隱藏的效果就不說了,跟以上方法的實現一樣,只不過是將半徑逐漸縮小

private void hideCircle(final View myView)
{
    int cx = (myView.getLeft() + myView.getRight()) / 2;
    int cy = (myView.getTop() + myView.getBottom()) / 2;

    int finalRadius = myView.getWidth();

    Animator anim =
            ViewAnimationUtils.createCircularReveal(myView, cx, cy, finalRadius, 0);
    anim.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation)
        {
            super.onAnimationEnd(animation);
            myView.setVisibility(View.INVISIBLE);
        }
    });
    anim.setDuration(3000);
    anim.start();
}
需要注意的是,當按鈕顯示的時候會響應用戶點擊事件,當隨漸變動畫消失之後,再次點擊會失去響應,此外如果我們想要改變波紋的顏色
可以在xml中修改 android:colorControlHighlight 的屬性

轉場動畫:

使用轉場動畫需要在主題中使用 android:windowContentTransitions 屬性啓用窗口內容轉換
或者在代碼中設置,將其置於setContentView()方法之前。
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
比如我們現在要做一個效果是從FirstActivity跳轉到SecondActivity,如果要實現轉場動畫需要怎麼處理呢

首先在FirstActivity中Button的點擊事件中設置跳轉
Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(FirstActivity.this).toBundle());
然後在SecondActivity中的OnCreate()方法中設置進入此Activity的動畫和返回上一個Activity的動畫樣式,如淡入淡出效果
protected void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    //TODO 淡入淡出
    getWindow().setEnterTransition(new Fade().setDuration(2000));
    getWindow().setExitTransition(new Fade().setDuration(2000));
    setContentView(R.layout.activity_second);
    linearLayout = (LinearLayout) this.findViewById(R.id.secondLayout);
}

Ok,就是這麼簡單,一個淡入淡出的轉場動畫就完成了

其次google還提供了其他兩種動畫方式
explode:從場景的中心移入或移出
slide:從場景的邊緣移入或移出

如果想要加快進入轉換的動作,需要調用
getWindow().setAllowEnterTransitionOverlap(true);
他會讓動畫變得更加生動。

我們關鍵看ActivityOptions.makeSceneTransitionAnimation(FirstActivity.this)這個方法,它具體是幹什麼的呢?

它是在創建onCreate()方法的時候ActivityOptions活動之間過渡使用的場景動畫,主要有這幾類:

我們上面提到過這種循環揭露的動畫方式,同樣可以用來做轉場動畫使用,樓主親測,效果不錯

MakeClipRevealAnimation:(圓形循環揭露)
staticActivityOptions makeClipRevealAnimation(View source, int startX, int startY, int width, int height)
Create an ActivityOptions specifying an animation where the new activity is revealed from a small originating area of the screen to its final full representation.
他是從一個點以圓形漸變到滿屏,參數類型,依次爲:操作view,SecondActivity開始漸變點橫座標,SecondActivity開始漸變點縱座標,SecondActivity要擴展的初始圓半徑,SecondActivity要擴展的最終圓半徑。
我們來設置下看看效果

tiaozhuan.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v)
    {
        Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
        startActivity(intent, ActivityOptions.makeClipRevealAnimation(v,0,0,v.getRight(),v.getBottom()).toBundle());

    }
});
給個效果:



這個實例不難,最主要的是要設置第二個Activity的主題背景爲透明
MakeCustomAnimation:(自定義轉場動畫)
staticActivityOptions makeCustomAnimation(Context context, int enterResId, int exitResId)
Create an ActivityOptions specifying a custom animation to run when the activity is displayed.
這個方法是需要傳入傳統的自定義動畫,以xml動畫的形式來實現跳轉效果,enterResId指的是要跳轉的Activity以怎麼樣的動畫形式出現,exitResId這個xml動畫指的是,當前Activity以哪種動畫方式消失
tiaozhuan.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v)
    {
        Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
        startActivity(intent, ActivityOptions.makeCustomAnimation(FirstActivity.this,R.anim.bottom_to_center,R.anim.center_to_left).toBundle());

    }
});

xml動畫是我事先定義好的,不出意外當前Activity會由中心到屏幕左邊緣消失,要跳轉的Activity則會從屏幕底部彈出

來看下效果:

MakeScaleUpAnimation:(漸變縮放轉場動畫)
staticActivityOptions makeScaleUpAnimation(View source, int startX, int startY, int width, int height)

Create an ActivityOptions specifying an animation where the new activity is scaled from a small originating area of the screen to its final full representation.


漸變縮放的轉場動畫與上面提到的圓形循環揭露效果makeClipRevealAnimation在參數上沒有變化,效果上也很相似
tiaozhuan.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v)
    {
        Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
        startActivity(intent, ActivityOptions.makeScaleUpAnimation(v,v.getLeft(),v.getTop(),v.getRight(),v.getBottom()).toBundle());
    }
});
看下效果:

MakeThumbnailScaleUpAnimation:(縮略圖形縮放轉場動畫)
staticActivityOptions makeThumbnailScaleUpAnimation(View source, Bitmap thumbnail, int startX, int startY)
Create an ActivityOptions specifying an animation where a thumbnail is scaled from a given position to the new activity window that is being started.
指定縮略圖從給定位置縮放到正在啓動的新活動窗口的動畫,這種使用方法很簡單也沒啥子特別的就不扯了,下面是使用方法。

tiaozhuan.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v)
    {
        v.setDrawingCacheEnabled(true);
        bitmap=v.getDrawingCache();
        Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
        startActivity(intent, ActivityOptions.makeThumbnailScaleUpAnimation(v,bitmap,10,10).toBundle());
    }
});
共享元素動畫:

在解析共享元素之前,我們來承接一下上文的內容,做個歸類:

Activity Transition提供了兩種Transition類型:
Enter(進入):進入一個Activity的效果
Exit(退出):退出一個Activity的效果

而這每種類型又分爲普通Transition和共享元素Transition:

普通Transition:(上面提到過)

explode:從場景的中心移入或移出
slide:從場景的邊緣移入或移出
fade:淡入淡出的效果

除了上面三種系統提供的轉場動畫之外,google還爲我們提供了可簡單自定義的多種動畫形式,如:

圓形循環揭露
自定義轉場動畫(跟我們傳統的設置方式overridePendingTransition(int enterAnim, int exitAnim))近似
漸變縮放轉場動畫
縮略圖形縮放動畫

Shared Elements Transition 共享元素轉換:

它的作用就是共享兩個acitivity種共同的元素,支持如下效果,我們會在後面解析
changeBounds -  改變目標視圖的佈局邊界
changeClipBounds - 裁剪目標視圖邊界
changeTransform - 改變目標視圖的縮放比例和旋轉角度
changeImageTransform - 改變目標圖片的大小和縮放比例
changeScroll 滾動變化


如果要在代碼中指定轉換,以 Transition 對象調用這些方法:

staticActivityOptions makeSceneTransitionAnimation(Activity activity, View sharedElement, String sharedElementName)

Create an ActivityOptions to transition between Activities using cross-Activity scene animations.

staticActivityOptions makeSceneTransitionAnimation(Activity activity, Pair...<ViewString> sharedElements)

Create an ActivityOptions to transition between Activities using cross-Activity scene animations.


這兩個方法是實現共享元素的關鍵,第一個是設置單個共享元素,第二個方法是設置多個共享元素

共享元素的元件是窗口繪製在整個視圖層次頂部的ViewOverlay上的,他相當於一個圖層,在View中所有其他的內容繪製完成之後繪製,以下是它所提供的公有方法

Public methods

void add(Drawable drawable)

Adds a Drawable to the overlay.

void clear()

Removes all content from the overlay.

void remove(Drawable drawable)

Removes the specified Drawable from the overlay.

從上面可以看出,他同時也支持添加和移除drawable

單個共享元素:

首先我們來看單個共享元素的實現:
既然元素共享,說明我們當前Activity中的view和要跳轉的Activity中的view中有共享的標籤,所以我們需要在FirstActivity中的ImageView的xml屬性中設置
android:transitionName="button"
同樣需要在SecondActivity中的ImageView的xml屬性中設置android:transitionName="button",這樣就實現了元素共享
接下來在FirstActivity中啓用就ok了
tiaozhuan.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v)
    {
        Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
        startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(FirstActivity.this, v, "button").toBundle());
    }
});

來看下效果:

仔細觀察左上角,兩個界面的切換就在一瞬間,是不是很流暢,就是這麼簡單,如果想要在第二個元件執行效果結束後反轉,可以調用 Activity.finishAfterTransition() 這個方法。

多個共享元素:

多個元素共享顧名思義就是多個控件聯動:
        img.setOnClickListener(new View.OnClickListener() {
            @TargetApi(Build.VERSION_CODES.M)
            @Override
            public void onClick(View v)
            {
                Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
                Pair<View, String> pairOne = new Pair<View, String>(img, "img");
                Pair<View, String> pairTwo = new Pair<View, String>(button1, "button1");
                Pair<View, String> pairThreee = new Pair<View, String>(button2, "button2");
                Pair<View, String> pairFour = new Pair<View, String>(button3, "button3");
                Pair<View, String> pairFive = new Pair<View, String>(button4, "button4");
                ActivityOptions activityOptions = ActivityOptions.makeSceneTransitionAnimation(FirstActivity.this, pairOne, pairTwo, pairThreee, pairFour, pairFive);
                startActivity(intent, activityOptions.toBundle());
            }

多個共享元素的設置和單個共享元素的設置一樣,有必要可以在SecondActivity中也進行元素綁定,不過不綁定也可以實現效果,在這裏我採用了彈性動畫
  getWindow().setEnterTransition(new Explode().setDuration(2000).setInterpolator(new BounceInterpolator()));
  getWindow().setExitTransition(new Explode().setDuration(2000));
ViewCompat.setTransitionName(img,"img");
ViewCompat.setTransitionName(button1,"button1");
……
來看下效果:

我們也可以通過定義transitionSet的方式來融合更多的共享元素動畫,來使效果更加絢麗,個人也喜歡這種比較靈活的方式:
//TODO 共享元素實現方式
private void ShareElements_Two(Intent intent)
{
    TransitionSet transitionSet=new TransitionSet();
    transitionSet.addTransition(new ChangeImageTransform());
 
    getWindow().setSharedElementEnterTransition(transitionSet);
    getWindow().setSharedElementExitTransition(transitionSet);

    Pair<View,String> pairOne=new Pair<View, String>(meinv,"meinv");
    Pair<View,String> pairTwo=new Pair<View, String>(tiaozhuan,"button");
    ActivityOptions activityOptions=ActivityOptions.makeSceneTransitionAnimation(FirstActivity.this,pairOne,pairTwo);
    startActivity(intent,activityOptions.toBundle());
}
上文我們提到了爲共享元素添加剪裁方式的動畫:
ChangeBounds This transition captures the layout bounds of target views before and after the scene change and animates those changes during the transition. 
ChangeClipBounds ChangeClipBounds captures the getClipBounds() before and after the scene change and animates those changes during the transition. 
ChangeImageTransform This Transition captures an ImageView's matrix before and after the scene change and animates it during the transition. 
ChangeScroll This transition captures the scroll properties of targets before and after the scene change and animates any changes. 
ChangeTransform This Transition captures scale and rotation for Views before and after the scene change and animates those changes during the transition. 
CircularPropagation A propagation that varies with the distance to the epicenter of the Transition or center of the scene if no epicenter exists. 
首先我們來看下ChangeClipBounds,剛開始接觸還摸不着頭腦,不知道怎麼用啊,後來看了源碼發現其實很簡單:
最主要是下面三個方法
void captureEndValues(TransitionValues transitionValues)

Captures the values in the end scene for the properties that this transition monitors.

void captureStartValues(TransitionValues transitionValues)

Captures the values in the start scene for the properties that this transition monitors.

Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues)
This method creates an animation that will be run for this transition given the information in the startValues and endValues structures captured earlier for the start and end scenes.
我們使用剪裁的時候,需要注意這種剪裁動畫是伴隨着共享元素的擴展而發生的,所以必須結合共享元素的使用
通過設置TransitionValues來定義我們的初始場景值,包括view,和剪裁區域Rect,他是通過map集合去設置值的,另外captureEndValues()設置動畫值之前必須調用setClipBounds()方法,否則會報空指針
看下源碼就一目瞭然了
//TODO 這兩個方法都調用了captureValues()方法,不同的是一個傳的是初始值,一個是結束值
public void captureStartValues(TransitionValues transitionValues) {
    captureValues(transitionValues);
}

@Override
public void captureEndValues(TransitionValues transitionValues) {
    captureValues(transitionValues);
}
//具體這個方法是做什麼的呢?因爲transitionValues封裝了我們要操作的view,所以我們要將他剪裁的區域也封裝起來,看標紅部分,所以在
這個方法之前我們必須剪裁視圖的可視區域,即setClipBounds(),否則將不起作用
private void captureValues(TransitionValues values) {
    View view = values.view;
    if (view.getVisibility() == View.GONE) {
        return;
    }

    Rect clip = view.getClipBounds();
    values.values.put(PROPNAME_CLIP, clip);
    if (clip == null) {
        Rect bounds = new Rect(0, 0, view.getWidth(), view.getHeight());
        values.values.put(PROPNAME_BOUNDS, bounds);
    }
}

再來看我們剪裁動畫執行的方法:
public Animator createAnimator(final ViewGroup sceneRoot, TransitionValues startValues,
        TransitionValues endValues) {
    //可以看出在這之前一定要先設置captureStartValues和captureEndValues方法,否則就會返回null
    if (startValues == null || endValues == null
            || !startValues.values.containsKey(PROPNAME_CLIP)
            || !endValues.values.containsKey(PROPNAME_CLIP)) {
        return null;
    }
   //獲取開始和剪裁區域
    Rect start = (Rect) startValues.values.get(PROPNAME_CLIP);
    Rect end = (Rect) endValues.values.get(PROPNAME_CLIP);
    if (start == null && end == null) {
        return null; // No animation required since there is no clip.
    }

    if (start == null) {
        start = (Rect) startValues.values.get(PROPNAME_BOUNDS);
    } else if (end == null) {
        end = (Rect) endValues.values.get(PROPNAME_BOUNDS);
    }
    if (start.equals(end)) {
        return null;
    }
    //這句話很關鍵,因爲我們這個方法最主要操作的對象是endValues中封裝的view,也就是下面我們屬性動畫真正要作用的對象
    endValues.view.setClipBounds(start);
    //我們屬性動畫是按照矩形拓展的方式演變
    RectEvaluator evaluator = new RectEvaluator(new Rect());
    return ObjectAnimator.ofObject(endValues.view, "clipBounds", evaluator, start, end);
}
熟悉了源碼的流程我們來使用下看看效果:
    private void ShareElements_Two(Intent intent)
    {
        ChangeClipBounds changeClipBounds=new ChangeClipBounds();
        TransitionValues values_start = new TransitionValues();
        TransitionValues values_end = new TransitionValues();

        values_start.view=tiaozhuan;
        values_end.view=meinv;
        //TODO setClipBounds(Rect rect),直接指定當前view的可視區域,當前的Rect使用的view的自身的座標系。
        //TODO startView原始大小200*200,endView原始大小600*600
        values_start.view.setClipBounds(new Rect(0, 0, 0, 0));
        //TODO 通過分析源碼我們知道這個方法雖然可省,但是之建立在要擴展的視圖顯示區域是本身大小的基礎上
        values_end.view.setClipBounds(new Rect(0, 0, 600, 600));
        changeClipBounds.captureStartValues(values_start);
        changeClipBounds.captureEndValues(values_end);
        changeClipBounds.createAnimator(layout, values_start, values_end).setDuration(3000).start();
        TransitionSet set=new TransitionSet();
        set.addTransition(changeClipBounds);

        set.addTransition(new Slide());
        set.addTransition(new Fade());
        set.setDuration(3000);
        //TODO 順序播放時1,同時播放是0
        set.setOrdering(0);
//        //TODO 普通轉場動畫進入效果
//        getWindow().setEnterTransition(set);
//        //TODO 普通轉場動畫退出效果
//        getWindow().setExitTransition(set);
        //TODO 共享元素進入效果
//        getWindow().setSharedElementEnterTransition(set);
        //TODO 共享元素退出效果
//        getWindow().setSharedElementExitTransition(set);
        //TODO 我們的動畫是建立在硬件加速的基礎上的,如果關閉你會看到慘不忍睹的畫面
        Pair<View,String> pairOne=new Pair<View, String>(meinv,"meinv");
        Pair<View,String> pairTwo=new Pair<View, String>(tiaozhuan,"button");
        ActivityOptions activityOptions=ActivityOptions.makeSceneTransitionAnimation(FirstActivity.this,pairOne,pairTwo);
        startActivity(intent,activityOptions.toBundle());
    }

直接看下效果:代碼都有註釋不做過多講解



曲線運動:

曲線運動是動畫利用曲線實現時間內插與空間移動模式, PathInterpolator是一個基於貝塞爾曲線或path對象的全新插入器,這個插入器會在1*1的正方形內指定一個
運動曲線,定位點位於(0,0)以及(1,1),而控制點由構造函數參數指定。
屬性動畫類別給出了新的構造函數,可以使用path對象爲視圖的X,Y屬性添加動畫,系統提供了三種基本曲線:

  • @interpolator/fast_out_linear_in.xml
  • @interpolator/fast_out_slow_in.xml
  • @interpolator/linear_out_slow_in.xml
除此之外我們可以自己定義:根據你需要的曲線去繪製,其實跟我們使用的貝塞爾曲線去不斷刷新視圖的運動軌跡是一樣的。

//TODO 使用曲線運動
private void doCurdAnimation(View view)
{
    ObjectAnimator objectAnimator;
    PathInterpolator pathInterpolator=new PathInterpolator(0.5f,0.9f);
    Path path=new Path();
    path.arcTo(1f,1f,400f,800f,0,120,false);
    objectAnimator=ObjectAnimator.ofFloat(view,View.X,View.Y,path);
    objectAnimator.setInterpolator(pathInterpolator);
    objectAnimator.setDuration(3000);
    objectAnimator.start();
}


視圖狀態動畫:

 StateListAnimator 這個類能夠在視圖狀態改變的同時執行動畫,舉個例子,比如我們點擊button,按下的時候讓button拉長,鬆開的時候恢復原位
通常我們的做法是自定義ValueAnimator動畫不斷改變刷新button的長度,有了StateListAnimator就很簡單了,它可以通過定義xml文件的形式,給button設置屬性,文件可以放在drawable下,也可以放到anim文件夾下。
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <set>
            <objectAnimator android:duration="500" android:propertyName="scaleX" android:valueTo="1.5" android:valueType="floatType" />
        </set>
    </item>

    <item android:state_pressed="false">
        <set>
            <objectAnimator android:duration="500" android:propertyName="scaleX" android:valueTo="1" android:valueType="floatType" />
        </set>
    </item>
</selector>

然後在xml中給button引用
<Button
    android:id="@+id/state_animte"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="跳轉"
    android:stateListAnimator="@drawable/click_selector"
    />

或者在Activity中用代碼定義添加:
StateListAnimator stateListAnimator= AnimatorInflater.loadStateListAnimator(this,R.drawable.click_selector);
state_animte.setStateListAnimator(stateListAnimator);
這樣一個簡單的視圖狀態動畫就完成了,看下效果


















真正需要自定義的一些效果來滿足需求的,都需要花時間去研究,歡迎大家到交流羣裏面分享一些驚天地泣鬼神的代碼,或者編程思想,貼下,Android代碼藝術:128672634,如果你不僅僅是把安卓當作一份工作,而是飽含堅持和熱愛!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章