實戰 | 使用揭露動畫(Reveal Effect)做一個絲滑的Activity轉場動畫
提筆之際(附總體思路)
最近跟幾個小夥伴在實踐一個專案,考慮到介面效果,我們決定使用揭露動畫作為Activity的轉場動畫。
這裡主要是我負責這部分的實現。
話說之前是沒接觸過的,關於具體的實現跟大體的思路都不太清楚。於是最先啃官方API,有點難看懂,然後下載了官方的demo,直接看程式碼,還是有問題,畢竟它規模略大,集成了好多動畫效果;
接著就找了很多博文,發現網上真的水文忒多了哎。。
最後找到了這三篇,算是解答了我的疑問:
https://www.jianshu.com/p/b75548e488df
這篇思路很好,寫得也很走心,啟發了我設計的思路跟注意到的一些問題,像揭露動畫的邏輯放在哪裡之類的;正當我要下載demo的時候發現他程式碼是kotlin來的,好吧,再繼續找博文;-
https://blog.csdn.net/shedoor/article/details/81251849#5
這篇就講得更加詳盡,這還看不懂那就有點說不過去了。一開始覺得揭露動畫還是挺高大上的樣子,結果此博文文首便說很簡單,那就很簡單吧,後來理解透了之後發覺確實也不難,畢竟只有一個靜態方法而已;
https://github.com/OCNYang/Android-Animation-Set/tree/master/reveal-animation
這個點進去是他的GitHub,demo下下來,程式碼看一下,自己寫個小demo(我是先在一個activity裡面跑通揭露動畫,再進一步將揭露動畫實現成跳轉動畫),再加入自己的邏輯,一步一步來算是沒有問題了。
到這裡就跑通了一個活動中的Activity了;
GitHub中附方法詳解圖
- https://github.com/whyalwaysmea/AndroidDemos
接下來就進入本文主題了,使用揭露動畫作為Activity的轉場動畫;
這篇文件跟程式碼算是幫上大忙了,有較大的參考價值;
不同的是作者的思路是在跳轉的目標活動中,啟動做揭露動畫的收挽,收挽結束後再finish();
我這裡根據情況修改為跳轉的目標活動中按下返回鍵即finish(),完了之後原始活動中的onReStart()中做揭露動畫的收挽;另外我在在跳轉的目標活動中完成揭露動畫展開的時候,添加了一個AlphaAnimation;
這邊的起始活動用的是button的onClick觸發的方式,以及這裡對兩個活動各自的控制元件的visible做了細節的把控;
引子
使用揭露動畫做一個絲滑的Activity轉場動畫,
關於這個需求,可能不同的同學,會有不同的問題,
我這裡把可能遇到的問題跟我在完成這個demo的過程中遇到的問題做一個總結,
然後附上總體的思路,大家可以交流一下~
什麼是揭露動畫?
Material-Animations; 官網有詳細的介紹,
揭露動畫具有相當絲滑的效果,
常常可以用與基於一個Activity的碎片切換或者View、控制元件的切換覆蓋鋪張,如本文第一個demo;
或者直接作為兩個Activity之間的轉場動畫,如本文第二個demo;揭露動畫怎麼用?
官方API封裝好了,
一個類一個靜態方法——ViewAnimationUtils.createCircularReveal(),
傳進五個引數,返回一個Animator物件。根據具體情況呼叫即可。
詳細可見參考文件;“絲滑”之解
這個轉場動畫要實現得絲滑,需要注意幾個細節:
活動A跳轉到活動B的情況下,
a.在A點選觸發跳轉時刻,揭露動畫要放在哪個活動展開;
b.在B按下返回鍵之後,揭露動畫又要放在哪個活動收挽;
c.揭露動畫的展開和收挽,createCircularReveal()分別以誰為操作物件;
d.這裡A通過FloatingActionButton出發,那揭露層View跟FloatingActionButton的visible跟invisible設定的順序;
e.關閉android預設的activity轉場動畫(不然就相當不絲滑了hhh);相關解答詳解下方第二個demo的思路總結,請移步到正文中的第二個demo;
瞭解本文的兩個demo之後,我相信以這個兩個demo為模板,結合筆者之前關於Material Design做的諸多筆記,應該是可以做出不少很有趣的東西來的~
再附上在做本demo的過程中一些debugExperience:
正文
1.先在一個activity裡面跑通揭露動畫
首先新建一個空專案,接著走三步即可:
1)更改styles.xml,改成NoActionBar,去掉標題欄,待會兒除錯可以觀察:
2)書寫activity_main.xml:
- 這裡對子空間的佈局範圍要求並不多,直接簡單用FrameLayout即可;
- 然後是Textview放在最前面,最先渲染,以為至底層;
- 接著我們這裡使用一個View原生控制元件來作為揭露動畫的操作物件,即通過對View控制元件的顯示和隱藏以及動畫操作來具體實現揭露動畫;
- 最後放置一個懸浮按鈕,用於啟動點選事件,這裡響應的事件是啟動揭露動畫:
另外說一下,關於FloatingActionButton,
android:backgroundTint可以設定其背景色,
android:src則給按鈕設定圖示,
這裡用的圖示資源來自於阿里的向量圖示庫
。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.lwp.justtest.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:layout_gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/view_puppet"
android:background="@color/colorPrimaryDark"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:backgroundTint="@color/colorPrimary"
android:src="@drawable/map"
app:pressedTranslationZ="10dp" />
</FrameLayout>
3)書寫MainActivity.java:
- 例項化各個元件之後,實現FloatingActionButton的onClick(),
- onClick()中我們呼叫一個自定義方法,在裡面啟動揭露動畫;
- 這裡通過變數flag實現點選按鈕時揭露動畫的交替開啟顯示以及關閉隱藏,效果圖在下方程式碼之後;
- 關於揭露動畫的邏輯以及具體實現的語法,
其實核心就是ViewAnimationUtils.createCircularReveal()這個方法以及其五個引數的意義,
詳細地可以參考前面提到的參考文章連結:
package com.lwp.justtest;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewAnimationUtils;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
boolean flag = false;
FloatingActionButton fab;
private View mPuppet;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mPuppet = findViewById(R.id.view_puppet);
fab = (FloatingActionButton)findViewById(R.id.fab);
fab.setOnClickListener(this);
}
@Override
public void onClick(View v) {
doRevealAnimation();
}
private void doRevealAnimation() {
int[] vLocation = new int[2];
fab.getLocationInWindow(vLocation);
int centerX = vLocation[0] + fab.getMeasuredWidth() / 2;
int centerY = vLocation[1] + fab.getMeasuredHeight() / 2;
int height = mPuppet.getHeight();
int width = mPuppet.getWidth();
int maxRradius = (int) Math.hypot(height, width);
Log.e("hei", maxRradius + "");
if (flag) {
Animator animator = ViewAnimationUtils.createCircularReveal(mPuppet, centerX, centerY, maxRradius, 0);
animator.setDuration(1000);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mPuppet.setVisibility(View.GONE);
}
});
animator.start();
flag = false;
} else {
Animator animator = ViewAnimationUtils.createCircularReveal(mPuppet, centerX, centerY, 0, maxRradius);
animator.setDuration(1000);
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mPuppet.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationEnd(Animator animation) {
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animator.start();
flag = true;
}
}
}
上面三步走完,即可執行程式,揭露動畫隨即實現,效果如下:
接著後面就進入本文主題了;
2.使用揭露動畫作為Activity的轉場動畫
思路總結:
1. MainActivity.java:
1.1. 例項化、宣告各種物件,注意:
根佈局物件(用來控制整個佈局),
揭露層物件指的是用於作揭露操作的純色的match_parent的View控制元件;
1.2. onCreate():完成findViewById()以及intent的構建,FloatingActionButton的setOnClickListener;
1.3. onClick():計算fab的中心座標,用於作為揭露動畫的圓心;同時把這對座標put進intent中,然後startActivity(intent);跳轉到下一個活動,同時把座標對傳過去;
1.4. createRevealAnimator():計算startRadius、endRadius,呼叫核心方法createCircularReveal()構建出animator並做相關配置後return之;
注意:
這裡的createCircularReveal()操作物件用的是揭露層純色View物件mPuppet0;
以及配置中用了animator.addListener(animatorListener0);新增一個動畫監聽器;--->> 1.5.
1.5. Animator.AnimatorListener animatorListener0
注意這裡的思路:
!!!
onAnimationStart():收挽揭露動畫開啟時,揭露層setVisibility(View.VISIBLE);fab.setVisibility(View.INVISIBLE);
onAnimationEnd():收挽版揭露動畫結束時,mPuppet0.setVisibility(View.INVISIBLE);fab.setVisibility(View.VISIBLE);
!!!
1.6. onRestart():回撥方法,計算fab的中心座標,用於作為揭露動畫的圓心;
呼叫createRevealAnimator()建立並配置一個animator(--->> 1.4.),
然後開啟收挽版揭露動畫,即animator.start();
2. next.java:
2.1. 例項化、宣告各種物件,注意:
根佈局物件(用來控制整個佈局),
揭露層物件指的是用於作揭露操作的純色的match_parent的View控制元件;
2.2. onCreate():完成findViewById(),
這裡注意:
動畫需要依賴於某個檢視才可啟動,這裡依賴於根佈局物件並且開闢一個子執行緒,
在子執行緒中get座標對,呼叫createRevealAnimator()建立並配置一個animator;--->> 2.3.
然後開啟展開版揭露動畫,即animator.start();
2.3. createRevealAnimator():計算startRadius、endRadius,呼叫核心方法createCircularReveal()構建出animator並做相關配置後return之;
注意這裡的思路:
!!!這裡的createCircularReveal()操作物件用的是根佈局物件content;
!!!!!
(即先載入好整個佈局,再把整個佈局作為揭露物件從0徑到螢幕對角線徑揭露展開,
展開過程中揭露層純色view在最頂層,所以感覺是View在做展開而已,
而實際上並不是;展開完畢後,再把view層去掉,去掉之後下層的活動內容自然就顯示出來了。)
!!!!!
以及配置中用了animator.addListener(animatorListener0);新增一個動畫監聽器;--->> 2.4.
2.4. Animator.AnimatorListener animatorListener1
注意這裡的思路:
!!!
onAnimationEnd():展開版揭露動畫結束時,
mPuppet.startAnimation(createAlphaAnimation());//呼叫透明度動畫,絲滑效果-->>2.5.
mPuppet.setVisibility(View.INVISIBLE);//動畫結束時,揭露動畫設定為不可見
!!!
2.5. createAlphaAnimation():定義透明度動畫,返回一個AlphaAnimation物件;
3. 兩個佈局xml沒什麼特別需要說的地方,
注意第一個xml的view要android:visibility="gone",以及按照渲染先後層次關係按序書寫控制元件即是;
4. styles.xml:android:windowAnimationStyle屬性置為null,取消掉Android預設的轉場動畫
<style name="noAnimTheme" parent="AppTheme">
<item name="android:windowAnimationStyle">@null</item>
</style>
5. AndroidManifest.xml:為參與的活動新增剛剛設定好的主題;
<activity
android:name=".MainActivity"
android:theme="@style/noAnimTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".next"
android:theme="@style/noAnimTheme">
</activity>
最後上程式碼了,不同的功能基本上都放在了不同的方法內實現,結合註釋應該不難理解了~
MainActivity.java:
package com.lwp.justtest;
import ...
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
FloatingActionButton fab;
Intent intent;
private View content;//根佈局物件(用來控制整個佈局)
private View mPuppet0;//揭露層物件
private int centerX;
private int centerY;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
content = findViewById(R.id.reveal_content0);
mPuppet0 = findViewById(R.id.view_puppet);
intent = new Intent(MainActivity.this, next.class);
fab = (FloatingActionButton)findViewById(R.id.fab);
fab.setOnClickListener(this);
}
@Override
public void onClick(View v) {
int[] vLocation = new int[2];
fab.getLocationInWindow(vLocation);
centerX = vLocation[0] + fab.getMeasuredWidth() / 2;
centerY = vLocation[1] + fab.getMeasuredHeight() / 2;
intent.putExtra("cx",centerX);
intent.putExtra("cy",centerY);
startActivity(intent);
}
private Animator createRevealAnimator(int x, int y) {
float startRadius = (float) Math.hypot(content.getHeight(), content.getWidth());
float endRadius = fab.getMeasuredWidth() / 2 ;
//注意揭露動畫開啟時是用根佈局作為操作物件,關閉時用揭露層作為操作物件
Animator animator = ViewAnimationUtils.createCircularReveal(
mPuppet0, x, y,
startRadius,
endRadius);
animator.setDuration(500);
animator.setInterpolator(new AccelerateDecelerateInterpolator());//設定插值器
animator.addListener(animatorListener0);
return animator;
}
//定義動畫狀態監聽器_按下返回鍵版
private Animator.AnimatorListener animatorListener0 = new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mPuppet0.setVisibility(View.VISIBLE);//按下返回鍵時,動畫開啟,揭露層設定為可見
fab.setVisibility(View.INVISIBLE);
}
@Override
public void onAnimationEnd(Animator animation) {
mPuppet0.setVisibility(View.INVISIBLE);
fab.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
};
//第二個活動退回來時,回撥本方法
@Override
protected void onRestart() {
super.onRestart();
//動畫需要依賴於某個檢視才可啟動,
// 這裡依賴於根佈局物件,並且開闢一個子執行緒,充分利用資源
content.post(new Runnable() {
@Override
public void run() {
int[] vLocation = new int[2];
fab.getLocationInWindow(vLocation);
centerX = vLocation[0] + fab.getMeasuredWidth() / 2;
centerY = vLocation[1] + fab.getMeasuredHeight() / 2;
Animator animator = createRevealAnimator(centerX, centerY);
animator.start();
}
});
}
}
main.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/reveal_content0"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.lwp.justtest.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Here is First Activity!"
android:textColor="@color/colorPrimaryDark"
android:textSize="30sp"
android:layout_gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/view_puppet"
android:background="@color/colorPrimaryDark"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:backgroundTint="@color/colorPrimary"
android:src="@drawable/map"
app:pressedTranslationZ="10dp" />
</FrameLayout>
next.java:
package com.lwp.justtest;
import ...
public class next extends AppCompatActivity {
private View content;//根佈局物件(用來控制整個佈局)
private View mPuppet;//揭露層物件
private int mX ;
private int mY ;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_next);//先載入好整個佈局,後面再用整個佈局作為揭露動畫的操作物件,揭露完畢後再去掉揭露層
content = findViewById(R.id.reveal_content);
mPuppet = findViewById(R.id.view_puppet);
//動畫需要依賴於某個檢視才可啟動,
// 這裡依賴於根佈局物件,並且開闢一個子執行緒,充分利用資源
content.post(new Runnable() {
@Override
public void run() {
mX = getIntent().getIntExtra("cx", 0);
mY = getIntent().getIntExtra("cy", 0);
Animator animator = createRevealAnimator(mX, mY);
animator.start();
}
});
}
private Animator createRevealAnimator(int x, int y) {
float startRadius = 0;
float endRadius = (float) Math.hypot(content.getHeight(), content.getWidth());
Animator animator = ViewAnimationUtils.createCircularReveal(
content, x, y,
startRadius,
endRadius);
animator.setDuration(660);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
//判斷標誌位reversed,true則為新增返回鍵版動畫監聽器,false則為跳轉動畫開啟版
// if (!reversed)
animator.addListener(animatorListener1);
return animator;
}
//定義動畫狀態監聽器_跳轉動畫開啟版
private Animator.AnimatorListener animatorListener1 = new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
// content.setVisibility(View.VISIBLE);//跳轉進來時,(因為finish之前會將之設定為不可見,)
// 根佈局要設定為可見,與finish部分的不可見相對應
// mPuppet.setAlpha(1);
}
@Override
public void onAnimationEnd(Animator animation) {
mPuppet.startAnimation(createAlphaAnimation());
mPuppet.setVisibility(View.INVISIBLE);//動畫結束時,揭露動畫設定為不可見
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
};
private AlphaAnimation createAlphaAnimation() {
AlphaAnimation aa = new AlphaAnimation(1,0);
aa.setDuration(400);
aa.setInterpolator(new AccelerateDecelerateInterpolator());//設定插值器
return aa;
}
}
next.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/reveal_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.lwp.justtest.next">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Here is Second Activity!"
android:textColor="@color/colorPrimaryDark"
android:textSize="30sp"
android:layout_gravity="center"/>
<View
android:id="@+id/view_puppet"
android:background="@color/colorPrimaryDark"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
res/values/styles.xml:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="noAnimTheme" parent="AppTheme">
<item name="android:windowAnimationStyle">@null</item>
</style>
</resources>
AndroidManifest.xml:
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:theme="@style/noAnimTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".next"
android:theme="@style/noAnimTheme">
</activity>
</application>
最終效果圖:
本文的兩個demo就到此為止了,我相信以這個兩個demo為模板,結合筆者之前關於Material Design做的諸多筆記,應該是可以做出不少很有趣的東西來的~