特別說明
當(dāng)前博客平臺賬號已廢棄,如果有使用細(xì)節(jié)問題請前往我新博客平臺進(jìn)行討論交流。
個(gè)人博客平臺 HuRuWo的技術(shù)小站
文章首發(fā)于個(gè)人博客HuRuWo的技術(shù)小站,如果本文非vip用戶無法完全瀏覽或者圖片無法打開,可前往個(gè)人博客文章地址查看文章并留言討論。
個(gè)人博客文章地址(收集轉(zhuǎn)載總結(jié))MD設(shè)計(jì)常用代碼/尺寸/顏色/控件個(gè)人收集總結(jié)--動畫
更多技術(shù)文章訪問本人博客HuRuWo的技術(shù)小站,包括 Electron從零開發(fā) Android 逆向 app 微信數(shù)據(jù)抓取 抖音數(shù)據(jù)抓取 閑魚數(shù)據(jù)抓取 小紅書數(shù)據(jù)抓取 其他軟件爬蟲 等技術(shù)文章
前言
作為Android 開發(fā)者,不僅要學(xué)習(xí)功能的實(shí)現(xiàn),還需要定制用戶的界面。如何制作一款符合大家審美的app是個(gè)根令人頭疼的問題。
不過好在google在15發(fā)布了MD設(shè)計(jì)規(guī)范,幫助程序員設(shè)計(jì)更好的app。
動畫
MD設(shè)計(jì)規(guī)范對于動畫的解釋
有意義的動畫效果
動畫效果(簡稱動效)可以有效地暗示、指引用戶。動效的設(shè)計(jì)要根據(jù)用戶行為而定,能夠改變整體設(shè)計(jì)的觸感。
動效應(yīng)當(dāng)在獨(dú)立的場景呈現(xiàn)。通過動效,讓物體的變化以更連續(xù)、更平滑的方式呈現(xiàn)給用戶,讓用戶能夠充分知曉所發(fā)生的變化。
動效應(yīng)該是有意義的、合理的,動效的目的是為了吸引用戶的注意力,以及維持整個(gè)系統(tǒng)的連續(xù)性體驗(yàn)。動效反饋需細(xì)膩、清爽。轉(zhuǎn)場動效需高效、明晰。
如何實(shí)現(xiàn)符合MD設(shè)計(jì)的動畫
真實(shí)的動作
物理世界中物體擁有質(zhì)量,所以只有當(dāng)施加給它們力量的時(shí)候才會移動,因此物體沒法在瞬間開始或者結(jié)束動作。動畫突然開始或者停止,或者在運(yùn)動時(shí)突兀的變化方向,都會使用戶感到意外和不和諧的干擾。
主要有兩點(diǎn):
- 迅速的加速和平滑的減速會感到自然和愉快
物理世界中物體擁有質(zhì)量,所以只有當(dāng)施加給它們力量的時(shí)候才會移動,因此物體沒法在瞬間開始或者結(jié)束動作。動畫突然開始或者停止,或者在運(yùn)動時(shí)突兀的變化方向,都會使用戶感到意外和不和諧的干擾。- 特殊情況:進(jìn)入和退出的場景
當(dāng)一個(gè)物體進(jìn)入這個(gè)場景時(shí),請確保它在最高速度下移動,這個(gè)行為模擬了自然移動:一個(gè)人進(jìn)入場景的時(shí)候,并不是從場景的邊緣開始走入的,而是從更遠(yuǎn)的地方。當(dāng)然,一個(gè)物體退出這個(gè)場景時(shí),需要維持它的速度,緩慢的離開場景,逐漸的進(jìn)入和緩慢的離開會把用戶的注意力吸引到這個(gè)動作上,在大多數(shù)情況下,這是你希望的效果。
不是所有物體的移動方式是相同的,輕的/小的物體可能會更快的加速和減速,因?yàn)樗鼈冑|(zhì)量比較小,所以只需要施加給他們較少的力就可以。大的/重的物體可能花需要更多的時(shí)間來到達(dá)他的最高速度或者回到停止?fàn)顟B(tài)。仔細(xì)琢磨如何將他們的動作應(yīng)用到你的應(yīng)用的UI元素中。
下面分析一下開源的兩個(gè)app的動畫實(shí)現(xiàn):
FAB隨著list滑動,下拉退出,上拉出現(xiàn)

這個(gè)FAB會根據(jù)RecycleView的滑動來進(jìn)入和退出界面,符合第二種動畫。既要以最大速度退出和進(jìn)入。下面看代碼實(shí)現(xiàn):
布局類似在CoordinatorLayout下添加RecycleView加上FloatingActionButton
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:title="MdView" />
<android.support.design.widget.TabLayout
android:id="@+id/tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<android.support.design.widget.FloatingActionButton
app:layout_behavior="com.othershe.mdview.ScrollAwareFABBehavior"
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="16dp"
android:src="@android:drawable/ic_menu_share" />
</android.support.design.widget.CoordinatorLayout>
關(guān)鍵代碼:app:layout_behavior="com.othershe.mdview.ScrollAwareFABBehavior"這行是用來控制fab行為的,而這個(gè)行為是用戶自定義的。
具體代碼可以參考浮動操作按鈕詳解
在此之前要先了解Behavior這個(gè)類:
參考文章:深入理解CoordinatorLayout.Behavior
在android5.0之后新的嵌套滑動機(jī)制中,引入了:NestScrollChild和NestedScrollingParent兩個(gè)接口,用于協(xié)調(diào)子父控件滑動狀態(tài),而CoordinatorLayout實(shí)現(xiàn)了NestedScrollingParent接口,在實(shí)現(xiàn)了NestScrollChild這個(gè)接口的子控件在滑動時(shí)會調(diào)用NestedScrollingParent接口的相關(guān)方法,將事件發(fā)給父控件,由父控件決定是否消費(fèi)當(dāng)前事件,在CoordinatorLayout實(shí)現(xiàn)的NestedScrollingParent相關(guān)方法中會調(diào)用Behavior內(nèi)部的方法。
我們實(shí)現(xiàn)Behavior的方法,就可以嵌入整個(gè)CoordinatorLayout所構(gòu)造的嵌套滑動機(jī)制中,可以獲取到兩個(gè)方面的內(nèi)容:
1、某個(gè)view監(jiān)聽另一個(gè)view的狀態(tài)變化,例如大小、位置、顯示狀態(tài)等
需要重寫layoutDependsOn和onDependentViewChanged方法
2、某個(gè)view監(jiān)聽
CoordinatorLayout內(nèi)NestedScrollingChild的接口實(shí)現(xiàn)類的滑動狀態(tài)
重寫onStartNestedScroll和onNestedPreScroll方法。注意:是監(jiān)聽實(shí)現(xiàn)了NestedScrollingChild的接口實(shí)現(xiàn)類的滑動狀態(tài),這就可以解釋為什么不能用ScrollView而用NestScrollView來滑動了。
下面看怎么定義這個(gè)fab行為類的:
- 繼承FAB的行為類
FloatingActionButton.Behavior
private boolean mIsAnimatingOut = false;
private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL ||
super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target,
nestedScrollAxes);
}}
重寫了onStartNestedScroll這個(gè)方法,這個(gè)方法是用來決定是否要響應(yīng)CoordinatorLayout的滾動,返回ture 表示接收。
- 重寫onNestedScroll處理滑動事件,包括定義fab進(jìn)入和退出的動畫。
@Override
public void onNestedScroll(final CoordinatorLayout coordinatorLayout, final FloatingActionButton child,
final View target, final int dxConsumed, final int dyConsumed,
final int dxUnconsumed, final int dyUnconsumed) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
if (dyConsumed > 0 && !mIsAnimatingOut && child.getVisibility() == View.VISIBLE) {
animateOut(child);
} else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
animateIn(child);
}
}
- 因?yàn)镕loatingActionButton.Behavior的基類已經(jīng)有了animateIn() 和 animateOut()方法,同時(shí)它也設(shè)置了一個(gè)私有變量mIsAnimatingOut,這些方法和變量都是私有的,所以現(xiàn)在我們需要重新實(shí)現(xiàn)這些動畫方法。
private void animateOut(final FloatingActionButton button) {
ViewCompat.animate(button).translationY(button.getHeight() + getMarginBottom(button))
.setInterpolator(INTERPOLATOR).withLayer()
.setListener(new ViewPropertyAnimatorListener() {
public void onAnimationStart(View view) {
mIsAnimatingOut = true;
}
public void onAnimationCancel(View view) {
mIsAnimatingOut = false;
}
public void onAnimationEnd(View view) {
mIsAnimatingOut = false;
view.setVisibility(View.INVISIBLE);
}
}).start();
}
private void animateIn(FloatingActionButton button) {
button.setVisibility(View.VISIBLE);
ViewCompat.animate(button).translationY(0)
.setInterpolator(INTERPOLATOR).withLayer().setListener(null)
.start();
}
private int getMarginBottom(View v) {
int marginBottom = 0;
final ViewGroup.LayoutParams layoutParams = v.getLayoutParams();
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
marginBottom = ((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin;
}
return marginBottom;
}
這里核心控制進(jìn)出是否為最大速度的就是插值器
private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
- 最后一步就是把這個(gè)CoordinatorLayout Behavior與浮動操作按鈕聯(lián)系起來。我們可以在xml的自定義屬性pp:layout_behavior中定義它:
<android.support.design.widget.FloatingActionButton
app:layout_behavior="com.codepath.floatingactionbuttontest.ScrollAwareFABBehavior" />
- 因?yàn)槲覀兪窃趚ml中靜態(tài)的定義這個(gè)behavior,為了讓 layout inflation順利進(jìn)行,我們必須實(shí)現(xiàn)一個(gè)構(gòu)造函數(shù)。
public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {
// ...
public ScrollAwareFABBehavior(Context context, AttributeSet attrs) {
super();
}
響應(yīng)式交互
響應(yīng)式交互能讓用戶信任,并且吸引他們。 當(dāng)用戶操作一個(gè)美觀且符合常理的應(yīng)用時(shí),他們會感到滿意甚至很高興。那是一種經(jīng)過深思熟慮、有目的、非隨機(jī)的而且可以帶有輕微異想天開但不會讓人分心的交互。
在 material design 中,應(yīng)用是響應(yīng)式的并且渴望用戶操作的:
- 觸摸,語音,鍵盤及鼠標(biāo)作為首要考慮的輸入方式。
- 雖然 UI 元素是有形的,但是他們被限制在屏幕里面(電腦或者移動設(shè)備的屏幕),視覺元素和動效能減少這種割裂,讓用戶能夠立即感知自己的操作。
總結(jié)來說就是:讓用戶知道他點(diǎn)擊了某個(gè)按鈕
同樣是剪影訊源碼分析:
RecyclerView的item里面使用了cardView,使用了卡片效果。
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:layout_margin="3dp"
android:id="@+id/cardview"
android:foreground="?android:attr/selectableItemBackground"
>
<LinearLayout
android:id="@+id/item_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
>
<TextView
android:id="@+id/item_tv"
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center" />
</LinearLayout>
</android.support.v7.widget.CardView>
cardView的
android:clickable="true"
android:foreground="?android:attr/selectableItemBackground"
這兩行設(shè)置了水波紋的點(diǎn)擊效果:
除了selectableItemBackground還有一種水波紋
selectableitembackgroundborderless只能在v21以上使用

要注意一點(diǎn),在adapter里面設(shè)置點(diǎn)擊事件的時(shí)候一定要設(shè)置最外層的布局的點(diǎn)擊事件才有效的實(shí)現(xiàn),否則沖突就無法實(shí)現(xiàn)點(diǎn)擊的水波紋。
holder.cardView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
}
});
水波紋可以加在所有可以點(diǎn)擊的控件上,比如Button
<Button
android:foreground="?android:attr/selectableItemBackground"
android:layout_width="match_parent"
android:layout_height="50dp" />
有意義的轉(zhuǎn)場動畫
為了讓界面切換不顯的突兀,android里設(shè)置了專場動畫。Activity的轉(zhuǎn)場動畫很早就有,但是太過于單調(diào),樣式也不好看,于是Google在Android5.0之后,又推出的新的轉(zhuǎn)場動畫,效果還是非常炫的。
參考文章: Android5.0之Activity的轉(zhuǎn)場動畫
- 舊轉(zhuǎn)場動畫回顧
首先我們還是先來看看在5.0之前如果我們想要在啟動Activity時(shí)使用動畫該怎么做呢?
startActivity(new Intent(this, Main3Activity.class));
overridePendingTransition(R.anim.in,R.anim.out);
對應(yīng)的入場和出場動畫就是兩個(gè)補(bǔ)間動畫,如下:
入場動畫:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:toYDelta="0"
android:fromYDelta="100%"
android:duration="1500"/>
</set>
出場動畫:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="1500"
android:fromYDelta="0"
android:toYDelta="-100%"/>
</set>
這種動畫是針對整個(gè)Activity而言的,無法設(shè)置Activity中元素的入場/出場動畫。
- 5.0之后的轉(zhuǎn)場動畫
分為兩種: - 分解、滑動進(jìn)入、淡入淡出
分解
先來看一張效果圖:
就是這樣一種效果,那我們接下來看看這種效果要怎么實(shí)現(xiàn)。
首先,把之前啟動Activity的代碼改成下面的寫法:
startActivity(new Intent(this, Main2Activity.class), ActivityOptions.makeSceneTransitionAnimation(this).toBundle());
添加完成之后,在Main2Activity中設(shè)置該Activity的進(jìn)出場動畫即可:
getWindow().setEnterTransition(new Explode().setDuration(2000));
getWindow().setExitTransition(new Explode().setDuration(2000));
大家一定要記得在styles.xml文件中添加下面一行代碼,表示激活A(yù)ctivity中元素的過渡效果:
<item name="android:windowContentTransitions">true</item>
滑動進(jìn)入
有了上面的步驟,再設(shè)置滑動進(jìn)入就很簡單了,只需要修改Main2Activity中的兩行代碼即可:
getWindow().setEnterTransition(new Slide().setDuration(2000));
getWindow().setExitTransition(new Slide().setDuration(2000));
顯示效果如下:
淡入淡出
Main2Activity修改代碼如下:
getWindow().setEnterTransition(new Fade().setDuration(2000));
getWindow().setExitTransition(new Fade().setDuration(2000));

顯示效果如下:
Activity的進(jìn)場和退出可以分別設(shè)置不同的動畫效果,如果沒有分別設(shè)置,則進(jìn)場和退出的動畫反過來。比如退出是變淡,則進(jìn)場就是變深。<只在設(shè)置了專場動畫的兩個(gè)Activity里>
-
元素共享
共享元素動畫
共享元素動畫是一個(gè)非常神奇的東東,我們先來看看效果:
可能這個(gè)Gif動畫還不太清晰,我再來解釋一下,在MainActivity和Main2Activity里邊都有一個(gè)Button,只不過一個(gè)大一個(gè)小,從MainActivity跳轉(zhuǎn)到Main2Activity時(shí),我并沒有感覺到Activity的跳轉(zhuǎn),只是覺得好像第一個(gè)頁面的Button放大了,同理,當(dāng)我從第二個(gè)頁面回到第一個(gè)頁面時(shí),也好像Button變小了。OK,這就是我們的Activity共享元素。
當(dāng)兩個(gè)Activity中有同一個(gè)控件的時(shí)候,我們便可以采用共享元素動畫。
使用共享元素動畫的時(shí)候,我們需要首先給MainActivity和Main2Activity中的兩個(gè)button分別添加Android:transitionName="mybtn"屬性,并且該屬性的值要相同,這樣系統(tǒng)才知道這兩個(gè)控件是共享元素。設(shè)置完成之后,接下來就是啟動Activity的代碼了,如下:
startActivity(new Intent(this,Main2Activity.class), ActivityOptions.makeSceneTransitionAnimation(this,view,"mybtn").toBundle());
還是上面那種啟動方式的重載方法,只不過這里多了兩個(gè)參數(shù),view表示MainActivity中的共享元素(就是那個(gè)Button),第二個(gè)參數(shù)表示布局文件中transitionAnimation屬性的值。OK,就這么簡單。
多元素共享:
那我如果兩個(gè)頁面中有多個(gè)共享元素該怎么辦呢?簡單,android:transitionName屬性還像上面一樣設(shè)置,然后在啟動Activity時(shí)我們可以通過Pair.create方法來設(shè)置多個(gè)共享元素,如下:
startActivity(new Intent(this, Main2Activity.class),
ActivityOptions.makeSceneTransitionAnimation(this, Pair.create(((View) iv1),"myiv"), create(((View) textView),"mytv")).toBundle());\
分析簡影訊里使用的過場動畫:
1.主界面的RecycleView進(jìn)入的過渡動畫:
如下:

分析源碼:
這個(gè)是RecycleView加載Item的動畫,所以寫在RecycleView的Adapter。
其實(shí)RecycleView內(nèi)部是可以設(shè)置Item的動畫的,使用這個(gè)方法:
mRecyclerView.setItemAnimator(new DefaultItemAnimator(mRecyclerView));
除了默認(rèn)的動畫還有其他內(nèi)置的動畫:
SlideInOutLeftItemAnimator : which applies a slide in/out from/to the left animation
SlideInOutRightItemAnimator : which applies a slide in/out from/to the right animation
SlideInOutTopItemAnimator : which applies a slide in/out from/to the top animation
**SlideInOutBottomItemAnimator **: which applies a slide in/out from/to the bottom animation
ScaleInOutItemAnimator : which applies a scale animation
**SlideScaleInOutRightItemAnimator **: which applies a scale animation with a slide in/out from/to the right animation
但是我們都不用,所以自己寫在Adapter里面:
定義屬性動畫:
protected Animator[] getAnimators(View view) {
return new Animator[]{
ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1f).setDuration(4000),
ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, 200, 0).setDuration(4000)
};
}
寫一個(gè)函數(shù)設(shè)置是否加載動畫:
public void setShowAnim(boolean showAnim) {
isShowAnim = showAnim;
}
在onBindViewHolder里為每一個(gè)item設(shè)置動畫:
private int mLastPosition = -1;
Animator[] animators = getAnimators(holder.itemView);
if (isShowAnim && animators != null && animators.length > 0
&& holder.getAdapterPosition() > mLastPosition) {
if (animators.length > 1) {
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(animators);
animatorSet.start();
} else {
for (Animator animator : animators) {
animator.start();
}
}
mLastPosition = holder.getAdapterPosition();
}
mLastPosition 用來判斷是不是最后一個(gè),否則不設(shè)置加載動畫。
2.進(jìn)入詳情頁面的動畫

過度動畫:在第二個(gè)Activity里設(shè)置:
public static void navigation(Activity activity, View view, MovieModel movieModel) {
Intent intent = new Intent(activity, MovieDetailActivity.class);
intent.putExtra("movie_model", movieModel);
if (Build.VERSION.SDK_INT >= 21) {
activity.getWindow().setExitTransition(new Explode());
ActivityCompat.startActivity(activity, intent,
ActivityOptions.makeSceneTransitionAnimation(activity).toBundle());
} else {
ActivityOptionsCompat option = ActivityOptionsCompat.makeScaleUpAnimation(view, 0, 0,
view.getMeasuredWidth(), view.getMeasuredHeight());
ActivityCompat.startActivity(activity, intent, option.toBundle());
}
}
第一個(gè)Activity里跳轉(zhuǎn)調(diào)用:
startActivity(new Intent(MainActivity.this,DetailActivity.class), ActivityOptions.makeSceneTransitionAnimation(MainActivity.this,view,"mybtn").toBundle());
打動用戶的細(xì)節(jié)
這里主要是一些圖標(biāo)的小動畫