Android Material Design 組件
定義陰影
Material Design為UI元素引入了高度的概念。由 Z 屬性所表示的視圖高度將決定其陰影的視覺(jué)外觀:擁有較高 Z 值的視圖將投射更大且更柔和的陰影。 擁有較高 Z 值的視圖將擋住擁有較低 Z 值的視圖;不過(guò)視圖的 Z 值并不影響視圖的大小。
指定視圖的高度
視圖的Z值包含兩個(gè)部分:
- 高度(elevation),靜態(tài)組件。
- 轉(zhuǎn)換(translationZ),用于動(dòng)畫的動(dòng)態(tài)組件。
Z = elevation + translationZ
Z值以dp為單位度量
設(shè)置靜態(tài)組件(elevation)
設(shè)置elevation有兩種方式:
- 布局屬性,android:layout_elevation
- 代碼,View.setElevation()
注意這里的設(shè)置的elevation指的是surfaces之間的高度間距,它是相對(duì)的,并不都是以屏幕的底部為起點(diǎn)來(lái)設(shè)定elevation
效果圖:

設(shè)置動(dòng)態(tài)組件(translationZ)
通過(guò)View.setTranslationZ()方法來(lái)設(shè)置。
當(dāng)View有了Z和translationZ的屬性,可以通過(guò)PropertyAnimator改變這兩個(gè)屬性輕松地為視圖高度添加動(dòng)畫。
自定義視圖陰影與輪廓
視圖的背景可繪制對(duì)象的邊界將決定其陰影的默認(rèn)形狀。輪廓(Outline)代表圖形對(duì)象的外形并定義觸摸反饋的波紋區(qū)域
定制一個(gè)陰影需要做到兩點(diǎn):
- 設(shè)置View的elevation值
- 給View 設(shè)置一個(gè)背景或者Outline
背景陰影
View
<TextView
android:id="@+id/myview"
...
android:elevation="2dp"
android:background="@drawable/myrect" />
Background Drawable myrect.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#42000000" />
<corners android:radius="5dp" />
</shape>
視圖將投射一個(gè)帶有圓角的陰影,因?yàn)楸尘翱衫L制對(duì)象將定義視圖的輪廓。 如果提供一個(gè)自定義輪廓,則此輪廓將替換視圖陰影的默認(rèn)形狀。
Outline自定義輪廓
如果要為您的代碼中的視圖定義自定義輪廓:
- 擴(kuò)展 ViewOutlineProvider 類別。
- 替代 getOutline() 方法。
- 利用 View.setOutlineProvider() 方法向您的視圖指定新的輪廓提供程序。
可使用 Outline 類中的方法創(chuàng)建帶有圓角的橢圓形和矩形輪廓。視圖的默認(rèn)輪廓提供程序?qū)囊晥D背景取得輪廓。 如果要防止視圖投射陰影,請(qǐng)將其輪廓提供程序設(shè)置為 null。
ViewOutlineProvider viewOutlineProvider = new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
int size = getResources().getDimensionPixelSize(R.dimen.fab_size);
outline.setOval(0, 0, size, size);
}
};
fab.setOutlineProvider(viewOutlineProvider);
常用控件elevation
| 控件名稱 | 值 |
|---|---|
| Toolbar | 4dp |
| SnackBar | 6dp |
| FloatingButton | resting 6dp,pressed 12dp |
更多控件Elevation值可以參考Component reference shadows
參考
ANDROID L——Material Design詳解(視圖和陰影)
FloatingActionButton
介紹
浮動(dòng)操作按鈕 (簡(jiǎn)稱 FAB) 是: “一個(gè)特殊的promoted操作案例。因?yàn)橐粋€(gè)浮動(dòng)在UI之上的圓形圖標(biāo)而顯得格外突出,同時(shí)它還具有特殊的手勢(shì)行為”
浮動(dòng)操作按鈕代表一個(gè)屏幕之內(nèi)最基本的額操作。關(guān)于FAB按鈕的更多信息和使用案例請(qǐng)參考MaterialDesign文檔
常用屬性

FloatingActionButton繼承自ImageView,所以擁有所有ImageView的屬性。同時(shí)還有一些特制的屬性:
| 屬性名稱 | 描述 |
|---|---|
| app:backgroundTint | 設(shè)置FAB的背景顏色。 |
| app:rippleColor | 設(shè)置FAB點(diǎn)擊時(shí)的背景顏色。 |
| app:borderWidth | 該屬性尤為重要,如果不設(shè)置0dp,那么在4.1的sdk上FAB會(huì)顯示為正方形,而且在5.0以后的sdk沒(méi)有陰影效果。所以設(shè)置為borderWidth="0dp"。 |
| app:elevation | 設(shè)置FAB z軸的靜態(tài)高度 |
| app:pressedTranslationZ | 設(shè)置FAB 點(diǎn)擊時(shí)的Z軸的動(dòng)態(tài)值 |
| app:fabSize | 設(shè)置FAB的大小,該屬性有兩個(gè)值,分別為normal和mini,對(duì)應(yīng)的FAB大小分別為56dp和40dp。 |
| android:src | 設(shè)置FAB的圖標(biāo),Google建議符合Design設(shè)計(jì)的該圖標(biāo)大小為24dp。 |
| app:layout_anchor | 設(shè)置FAB的錨點(diǎn),即以哪個(gè)控件為參照點(diǎn)設(shè)置位置。 |
| app:layout_anchorGravity | 設(shè)置FAB相對(duì)錨點(diǎn)的位置,值有 bottom、center、right、left、top等。 |
在上述表格中可以看到最后兩個(gè)屬性是布局屬性。一般FAB配合CoordinatorLayout使用,通過(guò)這兩個(gè)屬性構(gòu)建出特定位置與效果的FloatingActionButton。
根據(jù)MaterialDesign文檔應(yīng)該為FAB設(shè)置手機(jī)上下方的margin設(shè)置為16dp而平板上設(shè)置為24dp(layout_margin)。
注意,當(dāng)設(shè)置layout_behavior時(shí),不能引用CoordinatorLayout,會(huì)提示CoordinatorLayout不能作為View parent。
<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/id_coordinatorlayout_fab"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="256dp"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
...
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<TextView
android:text="@string/text_content"
android:textSize="20sp"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.v4.widget.NestedScrollView>
<android.support.design.widget.FloatingActionButton
android:id="@+id/id_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@mipmap/icon"
app:backgroundTint="#30469b"
app:elevation="6dp"
app:fabSize="normal"
app:rippleColor="#a6a6a6"
app:layout_anchor="@id/id_collapselayout_fab"
app:layout_anchorGravity="bottom|center"
app:borderWidth="0dp"/>
</android.support.design.widget.CoordinatorLayout>
效果:

默認(rèn)Behavior
浮動(dòng)操作按鈕默認(rèn)的behavior是為Snackbar讓出空間。效果如下:

布局代碼和上面類似,Activity中代碼:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_floating_btn);
initView();
initEvent();
}
private void initEvent() {
mFab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Snackbar.make(mCoordinatorLayout, "SnackBar", Snackbar.LENGTH_SHORT).show();
}
});
}
private void initView() {
mCoordinatorLayout = (CoordinatorLayout) findViewById(R.id.id_coordinatorlayout);
mFab = (FloatingActionButton) findViewById(R.id.id_fab);
}
自定義Behavior
有一下幾個(gè)準(zhǔn)備工作:
- 首先需要一個(gè)起源控件,可以是RecyclerView,也可以是AppBarLayout。
- 需要為浮動(dòng)操作按鈕實(shí)現(xiàn)CoordinatorLayout.Behavior。這個(gè)類用于定義按鈕該如何響應(yīng)包含在同一CoordinatorLayout之內(nèi)的其它view。
布局文件
<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/id_coordinatorlayout_behavior"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/id_recycler_behavior"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/id_fab_behavior"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:layout_behavior=".behavior.ScrollAwareFABBehavior"
app:layout_anchor="@id/id_recycler_behavior"
app:layout_anchorGravity="bottom|right"
app:borderWidth="0dp"
app:fabSize="normal"
app:elevation="6dp"
app:pressedTranslationZ="12dp"
app:backgroundTint="#30469b"
app:rippleColor="#a6a6a6"
android:src="@mipmap/icon"/>
</android.support.design.widget.CoordinatorLayout>
這里采用了RecyclerView作為起源控件。
注意,起源控件可以是CoordinatorLayout包含的ViewTree中任一子View(直接活著間接)。但是與Behavior關(guān)聯(lián)的必須是CoordinatorLayout的直接子View。
Activity代碼
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fab_behavior);
initDate();
initView();
}
private void initDate() {
mDates = new ArrayList<>();
for(int i = 0; i < 20; i++) {
mDates.add("This is item " + i);
}
}
private void initView() {
mRecyclerView = (RecyclerView) findViewById(R.id.id_recycler_behavior);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mAdapter = new HomeAdapter();
mRecyclerView.setAdapter(mAdapter);
}
class HomeAdapter extends RecyclerView.Adapter<HomeAdapter.MyViewHolder> {
@Override
public int getItemCount() {
return mDates.size();
}
@Override
public HomeAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
MyViewHolder holder = new MyViewHolder(LayoutInflater.from(FABBehaviorActivity.this)
.inflate(R.layout.layout_item, parent, false));
return holder;
}
@Override
public void onBindViewHolder(HomeAdapter.MyViewHolder holder, int position) {
holder.tv.setText(mDates.get(position));
}
class MyViewHolder extends RecyclerView.ViewHolder {
TextView tv;
public MyViewHolder(View itemView) {
super(itemView);
tv = (TextView) itemView.findViewById(R.id.id_tv_num);
}
}
}
主要是對(duì)RecyclerView的初始化,以及設(shè)置Adapter。RecyclerView應(yīng)該默認(rèn)開(kāi)啟了NestedScrolling允許條件ViewCompat.setNestedScrollingEnabled(RecyclerView,true);
Custom Behavior
整個(gè)代碼如下:
public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior{
private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
private static final String TAG = "Behavior";
/**
* 用于判斷當(dāng)前FloatingActionButton是否在執(zhí)行退出動(dòng)畫
*/
private boolean mIsAnimatingOut = false;
public ScrollAwareFABBehavior(Context context, AttributeSet attrs) {
super();
}
@Override
public boolean onStartNestedScroll(
CoordinatorLayout coordinatorLayout,
FloatingActionButton child,
View directTargetChild,
View target,
int nestedScrollAxes) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
@Override
public void onNestedScroll(
CoordinatorLayout coordinatorLayout,
FloatingActionButton child,
View target,
int dxConsumed,
int dyConsumed,
int dxUnconsumed,
int dyUnconsumed) {
super.onNestedScroll(
coordinatorLayout,
child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
Log.d(TAG, target.toString());
//上拉,DOWN坐標(biāo)減去MOVE坐標(biāo),值為正
if(dyConsumed > 0 && !mIsAnimatingOut && child.getVisibility() == View.VISIBLE) {
animateOut(child);
} else if(dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
//下拉,DOWN坐標(biāo)減去MOVE坐標(biāo),值為負(fù)
animateIn(child);
}
}
private void animateOut(final FloatingActionButton child) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
ViewCompat.animate(child).scaleX(0.0f).scaleY(0.0f).alpha(0.0f)
.setInterpolator(INTERPOLATOR)
.withLayer()
.setListener(new ViewPropertyAnimatorListenerAdapter() {
@Override
public void onAnimationCancel(View view) {
mIsAnimatingOut = false;
}
@Override
public void onAnimationEnd(View view) {
mIsAnimatingOut = false;
view.setVisibility(View.GONE);
}
@Override
public void onAnimationStart(View view) {
mIsAnimatingOut = true;
}
})
.start();
}else {
Animation anim = AnimationUtils.loadAnimation(child.getContext(), R.anim.fab_out);
anim.setInterpolator(INTERPOLATOR);
anim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
mIsAnimatingOut = true;
}
@Override
public void onAnimationEnd(Animation animation) {
mIsAnimatingOut = false;
child.setVisibility(View.GONE);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
child.startAnimation(anim);
}
}
private void animateIn(FloatingActionButton child) {
child.setVisibility(View.VISIBLE);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
ViewCompat.animate(child).scaleX(1.0f).scaleY(1.0f).alpha(1.0f)
.setInterpolator(INTERPOLATOR)
.withLayer()
.setListener(null)
.start();
}else {
Animation animation = AnimationUtils.loadAnimation(child.getContext(), R.anim.fab_in);
animation.setInterpolator(INTERPOLATOR);
child.startAnimation(animation);
}
}
}
代碼分析:
自定義的Behavior繼承自FloatingActionButton.Behavior。這樣的好處就是使用該Behavior可以保留默認(rèn)的Behavior的操作(為Snackbar騰出空間),又可以實(shí)現(xiàn)自定義的Behavior。
其實(shí)CoordinatorLayout.Behavior有兩種模式,一種是實(shí)現(xiàn)
layoutDependsOn()和onDependentViewChanged()方法,Snackbar就是;而另一種就是采用NestedScrolling事件傳遞。
可以發(fā)現(xiàn)上面覆寫的方法還是和NestedScrollParent接口方法有一點(diǎn)區(qū)別的。上面覆寫的方法來(lái)自CoordinatorLayout.Behavior,而CoordinatorLayout實(shí)現(xiàn)了NestedScrollParent接口。CoordinatorLayout.java中實(shí)現(xiàn)了該接口的方法,并且在這些方法中去調(diào)用Behavior的相應(yīng)方法。
CoordinatorLayout.onStartNestedScroll()方法通過(guò)遍歷所有直接子View的布局參數(shù)(LayoutParams)來(lái)找到有設(shè)置layout_behavior屬性的View,并且獲取到相應(yīng)的Behavior類。然后調(diào)用該Behavior的相應(yīng)方法。這也就解釋了為什么關(guān)聯(lián)Behavior控件必須是CoordinatorLayout直接子View
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
nestedScrollAxes);
handled |= accepted;
lp.acceptNestedScroll(accepted);
} else {
lp.acceptNestedScroll(false);
}
}
return handled;
}
這樣一來(lái),具備了事件起源控件(RecyclerView),NestedScrollParent(CoordinatorLayout),Behavior,以及與Behavior關(guān)聯(lián)的FloatingActionButton。
當(dāng)CoordinatorLayout同時(shí)有AppBarLayout和RecyclerView時(shí),AppBarLayout作為事件起源控件,同時(shí)給RecyclerView和FloatingActionButton設(shè)置各自的layout_behavior屬性。
有一點(diǎn)要注意,自定義Behavior一定要實(shí)現(xiàn)上述代碼中的構(gòu)造函數(shù)。
效果
