Android自定義Transition動畫
本文已授權微信公眾號:鴻洋(hongyangAndroid)在微信公眾號平臺原創首發。
曾經(或者現在)很多人說起Android和iOS都會拿Android的UI設計來開黑, “你看看人家iOS的設計, 再來看看Android的, 差距怎麼就這麼大呢?”, 對於這種說辭, 可以一句話來總結一下”他們還停留在4.X之前的時代”. 自從Android5.0推出Material Design
設計規範後, Android在設計上早已甩那個萬年不變的iOS好幾十條街!
以上純屬個人看法, 請勿開黑~~, 下面進入今天的主題.
還記得我曾經有篇文章(你所不知道的Activity轉場動畫——ActivityOptions
ActivityOptions
還不太熟悉的朋友現在可以開啟上面的文章先來了解下), 那篇文章中介紹的Android預定義的幾個轉場雖然在效果上已經很讚了, 但是還是很難滿足我們在開發中遇到的各種需求, 那怎麼辦? View
不能滿足需求, 我們可以自定義, Transition
也是一樣~~, 所以這篇文章我們就來介紹一下如何自定義Transition
動畫.
熟悉原理
在開始自定義之前, 我們首先來簡單的瞭解一下Transition
轉場動畫的原理, 大家在看到你所不知道的Activity轉場動畫——ActivityOptions這篇文章時, 對Android提供的這種新的轉場動畫都震撼到了, 但是肯定有很多人對它的原理不是很請求, 尤其是Scene
玩玩Transition
在稍微瞭解了一下原理之後, 我們就來玩玩Transition
了, 如何自定義一個Transition
呢? 跟自定義view我們需要繼承View或者ViewGroup一樣, 這裡我們需要繼承Transition
類.
public class MyTransition extends Transition {}
有兩個抽象方法必須要要重寫,
public class MyTransition extends Transition {
@Override
public void captureStartValues(TransitionValues transitionValues) {
}
@Override
public void captureEndValues(TransitionValues transitionValues) {
}
}
除了這兩個必須要重寫的方法, 我們還要重寫一個createAnimator
方法來自定義動畫, 於是, 我們要自定義一個Transition
, 一個類的結構肯定是肯定是這樣的.
public class MyTransition extends Transition {
@Override
public void captureStartValues(TransitionValues transitionValues) {
}
@Override
public void captureEndValues(TransitionValues transitionValues) {
}
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues) {
}
}
ok, 下面我們來詳細說一下這三個方法都是用來幹嘛的.
首先captureStartValues
, 從字面上來看是用來收集開始資訊的, 什麼開始資訊? 當然是動畫的開始資訊了. 那同樣的captureEndValues
是用來收集動畫結束的資訊的. 收集完了資訊,就要通過createAnimator
來建立個Animator
供系統呼叫了.
再來看看TransitionValues
這個陌生的類, 這個類其實很簡單, 只有兩個成員變數view
和values
, view
指的是我們要從哪個view上收集資訊, values
是用來存放我們收集到的資訊的. 比如: 在captureStartValues
裡, transitionValues.view
指的就是我們在開始動畫的介面上的那個view, 在captureEndValues
指的就是在目標介面上的那個view.
好了, 上面幾個方法的作用介紹完畢後, 我們馬上就來完成一個進入訊息內容的動畫效果, 還是老規矩, 在開始程式碼之前, 我們先來看看效果.
唉, 在ubuntu上錄屏有點費勁, 效果不咋地, 湊活著看, 或者可以在文章最後的連結自己下載本文的demo原始碼自己執行看~
仔細觀察效果, 我們可以找到兩處動畫.
- 單行內容從它在列表中的位置移動到介面的最上面.
- 訊息的內容由單行逐漸展開.
- 這兩個動畫是順序執行的
通過上面的分析, 我們大致可以得出, 下面, 我們需要收集的資訊有view在介面的位置和view的高度資訊, 所以我們先來定義一下需要收集的資訊
public class MyTransition extends Transition {
private static final String TOP = "top";
private static final String HEIGHT = "height";
// ...
}
然後我們開始收集動畫開始需要的資訊
public class MyTransition extends Transition {
private static final String TOP = "top";
private static final String HEIGHT = "height";
@Override
public void captureStartValues(TransitionValues transitionValues) {
View view = transitionValues.view;
Rect rect = new Rect();
view.getHitRect(rect);
transitionValues.values.put(TOP, rect.top);
transitionValues.values.put(HEIGHT, view.getHeight());
Log.d("qibin", "start:" + rect.top + ";" + view.getHeight());
}
}
首先, 我們通過transitionValues.view
拿到我們要收集資訊的目標view, 然後我們可以通過getHitRect
可以拿到它在ListView中的上下左右資訊, 最後我們通過transitionValues.values.put(TOP, rect.top)
來儲存一下他距離父佈局上面的距離, 當然我們還需要通過transitionValues.values.put(HEIGHT, view.getHeight())
來儲存動畫初始的高度.
收集完動畫開始的資訊, 我們再來收集動畫結束的資訊, 依葫蘆畫瓢, 很快就能寫出下面的程式碼.
public class MyTransition extends Transition {
private static final String TOP = "top";
private static final String HEIGHT = "height";
@Override
public void captureEndValues(TransitionValues transitionValues) {
transitionValues.values.put(TOP, 0);
transitionValues.values.put(HEIGHT, transitionValues.view.getHeight());
Log.d("qibin", "end:" + 0 + ";" + transitionValues.view.getHeight());
}
}
這裡的程式碼和上面並無差別, 動畫結束後, view距離上面的距離應該是0, 不過這裡需要注意的是captureStartValues
方法裡的transitionValues.view
是我們頁面跳轉開始那個介面上的view, 而captureEndValues
方法裡的transitionValues.view
是我們跳轉目標上的view, 所以這兩個方法裡獲取到的view的高度肯定是不一樣的.
好了, 在完成資訊收集之後, 我們就來寫動畫效果了,
public class MyTransition extends Transition {
@Override
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues) {
if (startValues == null || endValues == null) { return null;}
final View endView = endValues.view;
final int startTop = (int) startValues.values.get(TOP);
final int startHeight = (int) startValues.values.get(HEIGHT);
final int endTop = (int) endValues.values.get(TOP);
final int endHeight = (int) endValues.values.get(HEIGHT);
ViewCompat.setTranslationY(endView, startTop);
endView.getLayoutParams().height = startHeight;
endView.requestLayout();
ValueAnimator positionAnimator = ValueAnimator.ofInt(startTop, endTop);
if (mPositionDuration > 0) { positionAnimator.setDuration(mPositionDuration);}
positionAnimator.setInterpolator(mPositionInterpolator);
positionAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int current = (int) valueAnimator.getAnimatedValue();
ViewCompat.setTranslationY(endView, current);
}
});
ValueAnimator sizeAnimator = ValueAnimator.ofInt(startHeight, endHeight);
if (mSizeDuration > 0) { sizeAnimator.setDuration(mSizeDuration);}
sizeAnimator.setInterpolator(mSizeInterpolator);
sizeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int current = (int) valueAnimator.getAnimatedValue();
endView.getLayoutParams().height = current;
endView.requestLayout();
}
});
AnimatorSet set = new AnimatorSet();
set.play(sizeAnimator).after(positionAnimator);
return set;
}
}
在說原理的時候, 我們提到過, 這一系列的動畫其實在我們跳轉後的介面上完成的, 所以這裡的動畫我們也是在目標view上完成. 上面兩個方法中收集到的資訊, 我們需要在這裡用到, 所以我們通過以下程式碼來獲取收集到的資訊.
final int startTop = (int) startValues.values.get(TOP);
final int startHeight = (int) startValues.values.get(HEIGHT);
final int endTop = (int) endValues.values.get(TOP);
final int endHeight = (int) endValues.values.get(HEIGHT);
startValues
和endValues
都是createAnimator
的引數.
接著幾行莫名奇妙的程式碼
ViewCompat.setTranslationY(endView, startTop);
endView.getLayoutParams().height = startHeight;
endView.requestLayout();
是因為我們的動畫順序是先移動, 後展開, 首先把view的高度設定為前一個介面上view的高度是為了防止在移動的過程中view的高度是他自身的高度的.
接著我們建立了兩個動畫, 這兩個動畫很好理解, 一個位移的,一個是展開的, 不過這裡我們給了動畫一個時長和插值器, 這兩個資訊是公開給呼叫者去設定的.
最後我們建立一個AnimatorSet
, 在這個動畫集合中, 我們先來完成sizeAnimator
然後開始positionAnimator
, 最後返回該動畫集合. 自定義Transition
完畢.
使用自定義Transition
上面我們完成了Transition
的自定義, 這裡我們就來用一下它, 首先我們要在應用的主題中指定可以使用場景過度動畫.
<item name="android:windowContentTransitions">true</item>
看過你所不知道的Activity轉場動畫——ActivityOptions這篇文章的朋友都應該清楚, 我們還需要給我們兩個activity中的view一個transitionName
, 這裡就不貼程式碼了, 然後我們就來看看如何做跳轉.
public class MainActivity extends AppCompatActivity {
private ListView mListView;
private Adapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = (ListView) findViewById(R.id.list);
mAdapter = new Adapter();
mListView.setAdapter(new Adapter());
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
startActivity(view, mAdapter.getItem(position));
}
});
}
public void startActivity(View view, String content) {
Intent intent = new Intent(this, MessageActivity.class);
intent.putExtra("msg", content);
ActivityOptionsCompat compat = ActivityOptionsCompat
.makeSceneTransitionAnimation(this, view, view.getTransitionName());
ActivityCompat.startActivity(this, intent, compat.toBundle());
}
}
跳轉的程式碼大家都可以在你所不知道的Activity轉場動畫——ActivityOptions這篇文章中找到, 這裡就不解釋了, 我們主要還是來看看在目標activity中怎麼應用動畫.
public class MessageActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.message_layout);
setTitle("Content");
TextView msgTextView = (TextView) findViewById(R.id.msg);
msgTextView.setText(getIntent().getStringExtra("msg"));
executeTransition();
}
public void executeTransition() {
MyTransition transition = new MyTransition();
transition.setPositionDuration(300);
transition.setSizeDuration(300);
transition.setPositionInterpolator(new FastOutLinearInInterpolator());
transition.setSizeInterpolator(new FastOutSlowInInterpolator());
transition.addTarget("message");
getWindow().setSharedElementEnterTransition(transition);
}
@Override
public void onBackPressed() {
finish();
}
}
來看executeTransition
方法, 在這個方法中, 首頁我們構建了一個我們自定義的transition
, 然後各種配置, 解析來的一行程式碼,
transition.addTarget("message");
這個message
就是我們前面提到的transitionName
, 最後我們通過
getWindow().setSharedElementEnterTransition(transition);
來設定進入的動畫.
ok, 現在我們來看看效果
閃爍問題
看到效果後, 細心的朋友可能發現, 在動畫執行的過程中我們的NavigationBar
會產生一個閃爍的效果, 這個效果不是我們想要的,出現這個問題的原因是共享元素動畫是在整個視窗的view上執行的, 在這裡找到了解決方案. 他的解決辦法是: 首先將NavigationBar也作為動畫的一部分, 然後在目標activity中延遲動畫的執行. google給我們提供了兩個方法來用, postponeEnterTransition()
和startPostponedEnterTransition()
方法來延遲動畫的執行.
所以, 現在我們的跳轉程式碼應該是這樣的.
public void startActivity(View view, String content) {
View statusBar = findViewById(android.R.id.statusBarBackground);
View navigationBar = findViewById(android.R.id.navigationBarBackground);
List<Pair<View, String>> pairs = new ArrayList<>();
pairs.add(Pair.create(statusBar, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME));
pairs.add(Pair.create(navigationBar, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME));
pairs.add(Pair.create(view, view.getTransitionName()));
Intent intent = new Intent(this, MessageActivity.class);
intent.putExtra("msg", content);
ActivityOptionsCompat compat = ActivityOptionsCompat
.makeSceneTransitionAnimation(this, pairs.toArray(new Pair[pairs.size()]));
ActivityCompat.startActivity(this, intent, compat.toBundle());
}
在目標activity中執行的動畫的程式碼也應該是這樣的.
public void executeTransition() {
postponeEnterTransition();
final View decorView = getWindow().getDecorView();
getWindow().getDecorView().getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
decorView.getViewTreeObserver().removeOnPreDrawListener(this);
supportStartPostponedEnterTransition();
return true;
}
});
MyTransition transition = new MyTransition();
transition.setPositionDuration(300);
transition.setSizeDuration(300);
transition.setPositionInterpolator(new FastOutLinearInInterpolator());
transition.setSizeInterpolator(new FastOutSlowInInterpolator());
transition.addTarget("message");
getWindow().setSharedElementEnterTransition(transition);
}
到現在, 我們就完美解決了閃爍的問題~. ok, 到這裡, 大家應該可以隨意的自定義Transition
動畫啦~最後需要demo的朋友可以到https://github.com/qibin0506/TransitionAnimator來下載.