動畫篇(二)——android屬性動畫
本文講介紹android在3.0之後推出的一種新的動畫機制,屬性動畫,對動畫不瞭解的同學,可以先去看看動畫篇(一)——android動畫基礎這篇文章。
本人水平有限,文章中如果出現什麼不正確或者模糊的地方,還請各位小夥伴留下評論,多多指教 : )
好了,現在我們進入正題。
基本概念
【android傳統動畫Animation與屬性動畫Animator的工作原理】
簡單來說,傳統動畫就是不斷的去呼叫系統的onDraw方法,去重新繪製元件,而屬性動畫則是通過呼叫屬性的get和set方法重新設定控制元件的屬性值,實現動畫的效果
【Animator出現的原因】
既然已經存在了可以實現各種動畫的方法了,為什麼谷歌還要推出新的動畫框架呢?為了解釋這個問題,我們寫一個小栗子。
首先是佈局檔案,很簡單,就是一個imageView
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/image"
android:layout_width ="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"/>
</RelativeLayout>
接下來是java程式碼,我們找到這個view,為其新增點選事件,這裡直接簡單的Toast一下,表示view被點選;然後我們為這個view新增一個位移動畫。程式碼如下:
package com.example.dell.myapplication;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.animation.TranslateAnimation;
import android.widget.ImageView;
import android.widget.Toast;
public class MainActivity extends Activity {
private ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView= (ImageView) findViewById(R.id.image);
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this,"ImageView",Toast.LENGTH_SHORT).show();
}
});
//引數1:X開始位置 引數2:x結束的位置
//引數3:y開始位置 引數4:y結束的位置
TranslateAnimation animation =new TranslateAnimation(0f,500f,0f,0f);
animation.setDuration(2000);
//動畫結束後應用動畫
animation.setFillAfter(true);
imageView.startAnimation(animation);
}
}
我們來看看演示效果:
可以看到,當我們進入到Activity當中後,動畫就已經完成了,大家注意一下,這段程式碼其實是有問題的,因為我們只看到了動畫的結果,而沒看到動畫的過程,這樣一來,動畫設定的就毫無意義。為什麼會出現這種情況呢?這涉及到了Activity生命週期的內容,答案將在本節最後揭曉。
現在這時我先點的imageview完成動畫後的位置(0f,100f)發現並沒有彈出Toast,然後我們再去點選imageView的初始位置(0f,0f),有意思的事情發生了,Toast被彈出來了
那麼這些說明了什麼呢?
說明了我們在使用Animation的時候,雖然可以改變動畫在介面上顯示的位置,但是卻不能改變點選事件所在的位置。
到這裡,傳統Animation一個很大的侷限性,它只是重繪了動畫,改變了顯示的位置,但是真正事件響應的位置,卻沒有發生任何改變。所以,Animation並不適合製作具有互動的動畫效果。它只能用來完成一些顯示性的效果。
那麼下面,我們就來列一下傳統Animation的侷限性
【Animation的侷限性】
- 第一點就是上面所說的內容,不在囉嗦了。
- 因為Animation的工作原理,上面介紹了,是不斷呼叫系統的onDraw()方法去繪製圖像,那麼必然十分耗費GPU,效率不高
- Animation提供了位移、旋轉、透明度、縮放這四種動畫,雖然經過組合,可以創造出很多的動畫效果,但是有時候依然無法制作出複雜好看的動畫,即存在動畫效果上的侷限。
為解決以上的侷限性,Google在android3.0後推出了屬性動畫。那麼接下來我們就正式進入屬性動畫的內容。
在這之前,我們似乎還有一個小問題沒有解決,那就是為什麼上一個栗子的動畫沒有顯示出來呢?
答案是,動畫的啟動寫在了onCreate()方法當中,而在這之後,還有onStart(),onResume()方法,而使用者真正看到介面的時候,onCreate方法早已經呼叫完畢了,此時如果動畫的持續時間過短,那麼使用者看到介面時動畫自然已經結束了。解決的辦法也非常簡單,只需要把動畫的啟動邏輯放在onResume() (活動準備好和使用者進行互動的時候 )方法當中即可
補充:經過測試,發現在onResume()方法當中也會出現一些延時,這因為機器效能的問題,啟動一個活動的時間長短不一,即便呼叫了onResume()方法,距離Activity的啟動可能還有一段時間。但是這種寫法肯定要比在onCreate()方法中啟動動畫要好一些。
屬性動畫——ObjectAnimator
【“translationX”】
現在我們在上面栗子的基礎上增加一個button,將動畫的啟動邏輯放在裡面,這樣就能避免剛才動畫顯示的問題。
更改後的佈局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"/>
<Button
android:id="@+id/bt_move"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="move"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true" />
</RelativeLayout>
接下來是java程式碼
public class MainActivity extends Activity {
private ImageView imageView;
private TranslateAnimation animation;
private Button bt_move;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView= (ImageView) findViewById(R.id.image);
bt_move= (Button) findViewById(R.id.bt_move);
//view的點選事件
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this,"ImageView",Toast.LENGTH_SHORT).show();
}
});
//button的點選事件
bt_move.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//使用ObjectAnimator
//第一個引數是被操縱的view物件;第二個引數是被操作的屬性
//第三,第四個引數是可變長的數,表示熟悉所變化的範圍
ObjectAnimator objectAnimator= ObjectAnimator.ofFloat(imageView,"translationX",0f,200f);
//設定顯示時長
objectAnimator.setDuration(2000);
//讓動畫開始
objectAnimator.start();
}
});
}
}
我們看一下效果
這裡很明顯,view的點選事件也發生了相應的變化。
【“translationY”】
想要改變Y上的屬性,也很簡單,程式碼如下:
ObjectAnimator objectAnimator= ObjectAnimator.ofFloat(imageView,"translationY",0f,200f);
//設定顯示時長
objectAnimator.setDuration(2000);
//讓動畫開始
objectAnimator.start();
看到這裡,有的小夥伴可能會有疑問,到底那些屬性是我們可以去設定的呢?其實只要是Google定義了的可以通過set和get去操縱的屬性,我們都可以在屬性動畫中對其進行設定。
【“X和Y”】
前面已經講了translationX和translationY,那麼現在出現的X和Y又是什麼東西呢?
translationX是指的是物體的偏移量,而X是指物體最後到達的一個絕對值,即具體移動到的座標
【“rotation”】
旋轉屬性,旋轉360度
ObjectAnimator objectAnimator= ObjectAnimator.ofFloat(imageView,"rotation",0f,360f);
//設定顯示時長
objectAnimator.setDuration(2000);
//讓動畫開始
objectAnimator.start();
以此類推,可以操作的屬性還有很多,凡是可以通過get或者set設定的屬性,都可以設定成屬性動畫。
【組合動畫】
現在我們使用ObjectAnimator來進行多個動畫效果的組合
ObjectAnimator.ofFloat(imageView,"translationY",0f,100f).setDuration(1000).start();
ObjectAnimator.ofFloat(imageView,"translationX",0f,100f).setDuration(1000).start();
ObjectAnimator.ofFloat(imageView,"rotation",0f,360f).setDuration(1000).start();
效果圖:
可以發現,這三個動畫的效果是同時完成的,因為在呼叫start()方法之後,實際上是一個非同步的過程。
實際上,我們還有更好的方法去實現這種組合效果,我們對上面的寫法進行優化:
PropertyValuesHolder p1=PropertyValuesHolder.ofFloat("translationY",0f,100f);
PropertyValuesHolder p2=PropertyValuesHolder.ofFloat("translationX",0f,100f);
PropertyValuesHolder p3=PropertyValuesHolder.ofFloat("rotation",0f,360f);
//通過ObjectAnimator來呼叫ofPropertyValuesHolder()方法
//第一個引數傳遞view,後續的引數為可變長的陣列
ObjectAnimator.ofPropertyValuesHolder(imageView,p1,p2,p3).setDuration(1000).start();
這裡我們使用了一個PropertyValuesHolder的容器來容納3個動畫效果,然後在最後呼叫ObjectAnimator的ofPropertyValuesHolder()方法來載入之前定義的三個holder。那麼這樣寫的好處是什麼呢?Google在PropertyValuesHolder這個類中對動畫進行了一些優化,這些優化使得我們在使用多個動畫屬性的時候能夠更加有效率,更加節省系統資源。
在前面的動畫基礎當中,有一個動畫集合的概念,那麼在屬性動畫當中其實也有這麼一個集合的概念。下面我們就用AnimatorSet來實現上述的效果。
程式碼如下:
AnimatorSet animatorSet=new AnimatorSet();
//接下來將單個動畫新增到AnimatorSet當中
animatorSet.playTogether(animator1,animator2,animator3);
animatorSet.setDuration(1000);
animatorSet.start();
當然在AnimatorSet方法當中我們還有更多的選擇去控制動畫。從animatorSet.playTogether()這個方法的名字中就能看出,該方法是讓所有的動畫同時起效果,我們才看到了和剛才幾種方法實現的同樣的效果。這裡google還提供了playSequentially()方法,該方法則是按順序去播放動畫。大家可以試一下。
我們還可以使用play()方法,這裡我們實現view在X和Y軸上同時平移,結束之後再旋轉360度。
程式碼如下:
ObjectAnimator animator1= ObjectAnimator.ofFloat(imageView,"translationY",0f,100f);
ObjectAnimator animator2= ObjectAnimator.ofFloat(imageView,"translationX",0f,100f);
ObjectAnimator animator3= ObjectAnimator.ofFloat(imageView,"rotation",0f,360f);
AnimatorSet animatorSet=new AnimatorSet();
//animator1和Animator2同時播放
animatorSet.play(animator1).with(animator2);
//Animator3在Animator1或者Animator2結束後播放
animatorSet.play(animator3).after(animator2);
animatorSet.setDuration(1000);
animatorSet.start();
這樣一來就實現了對每個動畫更加細緻的控制。通過play(),with(),after(),before()方法,我們就能做到對一個屬性集合的詳細的順序控制。這種方式,也是屬性動畫框架中使用最多的一種配和。
接下來總結一下
(1)通過ObjectAnimator進行更精細的控制,只控制一個物件的一個屬性。
(2)同時多個ObjectAnimator組合到AnimatorSet當中,可以形成一個完整的動畫效果。
(3)而且AnimatorSet可以自動驅動,可以去呼叫play(),with(),after(),before,playTogether(),playSequentially()實現更為豐富的動畫效果。
動畫事件監聽
顧名思義,和一般的點選事件差不多,我們為ObjectAnimator設定監聽事件,以滿足實際開發當中的需求。
bt_move.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
………………
ObjectAnimator bt_animator=ObjectAnimator.ofFloat(view,"alpha",0f,1f);
bt_animator.setDuration(1000);
bt_animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
//動畫開始前呼叫
}
@Override
public void onAnimationEnd(Animator animator) {
//動畫結束後呼叫,為簡單起見,這裡我們只簡單的提示一行字
Toast.makeText(MainActivity.this,"anim is end",Toast.LENGTH_SHORT).show();
}
@Override
public void onAnimationCancel(Animator animator) {
//動畫被取消後
}
@Override
public void onAnimationRepeat(Animator animator) {
//動畫重複時呼叫
}
});
bt_animator.start();
…………
}
});
我們在上一個栗子的基礎上為我們的Button新增一個alpha動畫,讓其從透明變成不透明,然後在這個動畫裡面加入了一個監聽事件,在監聽事件當中,我們看到了4個需要重寫的方法,通過這4個方法,我們就可以監聽動畫在不同事件段所需要完成的操作。
演示效果:
現在我們在考慮一種情況,那就是如果我們並不需要重寫那麼多的方法該怎麼辦呢?這時可以使用android系統提供的一個更方便的介面AnimatorListenerAdapter(),大家可以發現,系統幫我們實現了很多方法,這裡我們只需要新增需要重寫的方法即可
程式碼如下:
bt_animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
Toast.makeText(MainActivity.this,"anim is end",Toast.LENGTH_SHORT).show();
}
});
這樣我們就實現了對某一個事件的監聽,而不需要寫出所有的事件。
總結一下
看到出來動畫監聽事件還是比較簡單的,我們只需要呼叫ObjectAnimator的addListener()方法,就能為我們的Animator增加一個監聽事件,接著我們可以通過 AnimatorListenerAdapter這個類,去有選擇的去選取我們需要監聽的事件。
小栗子
這裡的栗子來自於慕課網的課程,有興趣的小夥伴可以去看看。
在開始寫這個栗子之前,請各位小夥伴去下載一下素材,其中a.png的尺寸有點問題,改為58*58即可
演示效果:
接下來讓讓我們看看佈局檔案
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/view_b"
android:src="@drawable/b"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/view_c"
android:src="@drawable/c"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/view_d"
android:src="@drawable/d"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/view_e"
android:src="@drawable/e"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/view_f"
android:src="@drawable/f"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/view_g"
android:src="@drawable/g"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/view_h"
android:src="@drawable/h"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/view_a"
android:src="@drawable/a"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</FrameLayout>
裡面定義了我們需要用到的imageView
接下來是比較關鍵的java程式碼:
public class MainActivity extends Activity implements View.OnClickListener {
//定義每一個圖片資源
private int[] res = {R.id.view_a, R.id.view_b, R.id.view_c, R.id.view_d, R.id.view_e, R.id.view_f, R.id.view_g,
R.id.view_h};
//儲存viewd的list集合
private List<ImageView> imageViewList = new ArrayList<>();
//設定一個flag來標示列表是否展開
private boolean flag = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
for (int i = 0; i < res.length; i++) {
//例項化imageView
ImageView imageView = (ImageView) findViewById(res[i]);
//新增點選事件
imageView.setOnClickListener(this);
//將每一個ImageView新增到list當中
imageViewList.add(imageView);
}
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.view_a:
//點選最上面的圖片,顯示展開動畫
//動畫方法
if (flag) {
//列表未展開
startAnimation();
flag = false;
} else {
//列表已展開
stopAnimation();
flag = true;
}
break;
default:
//其他按鈕
Toast.makeText(this, "clicked", Toast.LENGTH_SHORT).show();
break;
}
}
private void stopAnimation() {
for (int i = 1; i < res.length - 1; i++) {
ObjectAnimator animator = ObjectAnimator.ofFloat(imageViewList.get(i), "translationY", 200 * i, 0);
animator.setDuration(500);
//設定插值器
animator.setInterpolator(new AnticipateInterpolator());
animator.start();
}
}
private void startAnimation() {
for (int i = 1; i < res.length - 1; i++) {
ObjectAnimator animator = ObjectAnimator.ofFloat(imageViewList.get(i), "translationY", 0f, 200 * i);
animator.setDuration(500);
//設定延遲,讓動畫更加豐富
animator.setStartDelay(i * 100);
//設定插值器
animator.setInterpolator(new OvershootInterpolator());
animator.start();
}
}
}
上述程式碼當中有非常詳細的註釋,這裡就不多解釋了。為了讓動畫效果更棒,有時我們還可以為動畫新增差插器(interpolator)。android內建的插值器有如下
- Accelerate
- Decelerate
- Accelerate/Decelerate、
- Overshoot
- Bounce
通過插值器,我們讓某一屬性在數值上的變化時,可以擁有不同的加速曲線,進而讓我們的動畫更加豐富。