概述
最近利用上下班在路上的時間一直在看一些技術(shù)文章溉躲,目的當然是學習別人的開發(fā)經(jīng)驗來提高自己的技術(shù)水平祭埂。讀別人的文章很有趣盯仪,即使是我以為很簡單的知識點,別人也能寫出一些新花樣來贪嫂,這時你會驚奇的發(fā)現(xiàn),原來這玩意還能這么玩0丁力崇!谷歌推出android.support.design
中的組件也好久了斗塘,寫demo的人也很多,但是在實際開發(fā)過程中使用率還沒有達到很普遍的程度亮靴,一是因為開發(fā)者對這些新的東西還沒有比較深的學習和認識馍盟,我相信即使是現(xiàn)在仍然有很多人還不知道怎么使用或者不清楚他們的實現(xiàn)原理;二是因為開發(fā)習慣台猴,比如很多人對ListView
用的比較熟悉朽合,所以當他有一個需求需要用到列表顯示數(shù)據(jù)時,他當然首選ListView
而不是RecyclerView
了饱狂,這也是由于上一點原因曹步,對新組件的不熟悉導致不愿意去在實際開發(fā)中使用這些東西。但休讳,現(xiàn)在是時候放棄那些老古董了讲婚。
android.support.design
中的組件我最常用的也就是AppBarLayout
和TabLayout
了,因為它很好的取代了ViewPagerIndicator這個開源組件俊柔,而其他的比如FloatingActionButton
筹麸、NavigationView
、Snackbar
雏婶、TextInputLayout
物赶、CollapsingToolbarLayout
這些都使用的很少,雖然不常用到部分原因是產(chǎn)品設(shè)計出的UI原型可能不會遵循Material Design
留晚,但是掌握他們的基本使用方法是有必要的酵紫,萬一哪天就用上了呢。
NavigationView
NavigationView
一般和v4包中的DrawerLayout
結(jié)合使用错维,雖然目前大部分的app放棄了這種側(cè)滑顯示菜單的設(shè)計方式而改成頂部或底部Tab切換菜單奖地,但仍有一些使用到的。它本質(zhì)上是一個FrameLayout
赋焕,當在Android Studio
新建工程到選擇Activity
模式時選擇Navigation Drawer Activity
時會自動幫你構(gòu)建一個帶有側(cè)滑抽屜效果的Activity
参歹,它默認的xml
是這樣的:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout 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/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="start">
<include
android:layout_width="match_parent"
android:layout_height="match_parent"
layout="@layout/app_bar_main"/>
<android.support.design.widget.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header_main"
app:menu="@menu/activity_main_drawer"/>
</android.support.v4.widget.DrawerLayout>
它是將app_bar_main
這個view
和NavigationView
同時包裹在DrawerLayout
中,而app_bar_main
也就是相當于主屏幕顯示內(nèi)容的區(qū)域隆判,它的xml
就和我們平常建工程的主頁面是一樣的:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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"
android:fitsSystemWindows="true"
tools:context="com.xx.design.MainActivity">
<android.support.design.widget.AppBarLayout
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:popupTheme="@style/AppTheme.PopupOverlay"/>
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_main"/>
<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="@dimen/fab_margin"
app:srcCompat="@android:drawable/ic_dialog_email"/>
</android.support.design.widget.CoordinatorLayout>
在來看看NavigationView
犬庇,我們只需要看三個屬性:
-
layout_gravity
:這個屬性控制側(cè)滑菜單的位置是左邊還是右邊。它有start
蜜氨、end
械筛、left
、right
四個值飒炎,其中start
和left
都表示位置在左邊埋哟,end
和right
都表示位置在右邊,但官方推薦使用start
和end
,因為使用另外兩個可能會在滑動過程中導致一些問題出現(xiàn)赤赊。
1701111
DrawerLayout
中的openDrawer
屬性是控制NavigationView
的打開和關(guān)閉的方向闯狱,所以它的值一般和layout_gravity
設(shè)置成一樣。
-
app:headerLayout
:它是側(cè)滑頁面的頂部抛计,一般放用戶頭像哄孤、性別、個性簽名這些view吹截。默認的nav_header_main.xml
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="@dimen/nav_header_height"
android:background="@drawable/side_nav_bar"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:theme="@style/ThemeOverlay.AppCompat.Dark"
android:orientation="vertical"
android:gravity="bottom">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@dimen/nav_header_vertical_spacing"
app:srcCompat="@android:drawable/sym_def_app_icon"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/nav_header_vertical_spacing"
android:text="Android Studio"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"/>
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="android.studio@android.com"/>
</LinearLayout>
-
app:menu
:是一些菜單按鈕的xml瘦陈,默認的activity_main_drawer.xml
:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/nav_camera"
android:icon="@drawable/ic_menu_camera"
android:title="Import"/>
<item
android:id="@+id/nav_gallery"
android:icon="@drawable/ic_menu_gallery"
android:title="Gallery"/>
<item
android:id="@+id/nav_slideshow"
android:icon="@drawable/ic_menu_slideshow"
android:title="Slideshow"/>
<item
android:id="@+id/nav_manage"
android:icon="@drawable/ic_menu_manage"
android:title="Tools"/>
</group>
<item android:title="Communicate">
<menu>
<item
android:id="@+id/nav_share"
android:icon="@drawable/ic_menu_share"
android:title="Share"/>
<item
android:id="@+id/nav_send"
android:icon="@drawable/ic_menu_send"
android:title="Send"/>
</menu>
</item>
</menu>
但是這兩個屬性不是必須有的,一張圖看清NavigationView
波俄、headerLayout
和menu
的關(guān)系:
頁面布局就是這樣了晨逝,在Activity
中初始化DrawerLayout
和NavigationView
并設(shè)置監(jiān)聽:
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
// 監(jiān)聽側(cè)滑開關(guān)的切換
drawer.addDrawerListener(toggle);
toggle.syncState();
NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
navigationView.setNavigationItemSelectedListener(this);
還需要實現(xiàn)NavigationView.OnNavigationItemSelectedListener
這個接口,是點擊menu
中的菜單的監(jiān)聽懦铺,并且在onNavigationItemSelected
方法中處理回調(diào):
public boolean onNavigationItemSelected(MenuItem item) {
// Handle navigation view item clicks here.
int id = item.getItemId();
if (id == R.id.nav_camera) {
// Handle the camera action
} else if (id == R.id.nav_gallery) {
} else if (id == R.id.nav_slideshow) {
} else if (id == R.id.nav_manage) {
} else if (id == R.id.nav_share) {
} else if (id == R.id.nav_send) {
}
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
drawer.closeDrawer(GravityCompat.START);
return true;
}
退出界面時需要關(guān)閉菜單:
@Override public void onBackPressed() {
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
if (drawer.isDrawerOpen(GravityCompat.START)) {
drawer.closeDrawer(GravityCompat.START);
} else if (drawer.isDrawerOpen(GravityCompat.END)) {
drawer.closeDrawer(GravityCompat.END);
} else {
super.onBackPressed();
}
}
這么看來捉貌,使用還是挺簡單的,開發(fā)者僅僅需要關(guān)注布局和點擊事件的處理冬念,界面的交互google已經(jīng)幫我們都處理好了趁窃。
FloatingActionButton
FloatingActionButton
實質(zhì)上是ImageButton
,它的使用也很簡單:
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View view) {
//TODO: do sth
}
});
目前一般遵循MD設(shè)計風格的app都會使用到FAB
急前,它一般放在屏幕右下角醒陆,比如這樣:
它的作用比較簡單,即提供一個點擊事件裆针,去做相應(yīng)的操作统求,現(xiàn)在我們要研究的是滑動屏幕時FAB
的顯示和隱藏。
常見的隱藏和顯示效果有兩種:一種是縮放動畫据块,一種是平移動畫,當FAB
與CoordinatorLayout
一起使用時折剃,給它設(shè)置app:layout_behavior
屬性:
<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="@dimen/fab_margin"
app:layout_behavior="@string/fab_custom_behavior"
app:srcCompat="@android:drawable/ic_dialog_email"/>
<string name="fab_custom_behavior">com.xx.design.ScrollAwareFABBehavior</string>
ScrollAwareFABBehavior
是繼承默認的FloatingActionButton.Behavior
另假,而CoordinatorLayout
又實現(xiàn)了NestedScrollingParent
接口來監(jiān)聽列表的滾動,所以要隱藏或顯示FAB
怕犁,只需要onStartNestedScroll
方法中選擇垂直方向ViewCompat.SCROLL_AXIS_VERTICAL
上的滾動边篮,在onNestedScroll
方法中監(jiān)聽滑動是向上還是向下。另外谷歌官方在22.2.1版本給FAB
加了hide
和show
兩個方法奏甫,它的效果是做縮放動畫戈轿,22.2.1之前的版本需要自己實現(xià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 && !this.mIsAnimatingOut && child.getVisibility() == View.VISIBLE) {
// User scrolled down and the FAB is currently visible -> hide the FAB
animateOut(child);
// child.hide();
} else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
// User scrolled up and the FAB is currently not visible -> show the FAB
animateIn(child);
// child.show();
}
}
// Same animation that FloatingActionButton.Behavior uses to hide the FAB when the AppBarLayout exits
private void animateOut(final FloatingActionButton button) {
if (Build.VERSION.SDK_INT >= 14) {
ViewCompat.animate(button)
.scaleX(0.0F)
.scaleY(0.0F)
.alpha(0.0F)
.setInterpolator(INTERPOLATOR)
.withLayer()
.setListener(new ViewPropertyAnimatorListener() {
public void onAnimationStart(View view) {
ScrollAwareFABBehavior.this.mIsAnimatingOut = true;
}
public void onAnimationCancel(View view) {
ScrollAwareFABBehavior.this.mIsAnimatingOut = false;
}
public void onAnimationEnd(View view) {
ScrollAwareFABBehavior.this.mIsAnimatingOut = false;
view.setVisibility(View.GONE);
}
})
.start();
} else {
Animation anim = AnimationUtils.loadAnimation(button.getContext(),
android.support.design.R.anim.design_fab_out);
anim.setInterpolator(INTERPOLATOR);
anim.setDuration(200L);
anim.setAnimationListener(new Animation.AnimationListener() {
public void onAnimationStart(Animation animation) {
ScrollAwareFABBehavior.this.mIsAnimatingOut = true;
}
public void onAnimationEnd(Animation animation) {
ScrollAwareFABBehavior.this.mIsAnimatingOut = false;
button.setVisibility(View.GONE);
}
@Override public void onAnimationRepeat(final Animation animation) {
}
});
button.startAnimation(anim);
}
}
// Same animation that FloatingActionButton.Behavior uses to show the FAB when the AppBarLayout enters
private void animateIn(FloatingActionButton button) {
button.setVisibility(View.VISIBLE);
if (Build.VERSION.SDK_INT >= 14) {
ViewCompat.animate(button)
.scaleX(1.0F)
.scaleY(1.0F)
.alpha(1.0F)
.setInterpolator(INTERPOLATOR)
.withLayer()
.setListener(null)
.start();
} else {
Animation anim = AnimationUtils.loadAnimation(button.getContext(),
android.support.design.R.anim.design_fab_in);
anim.setDuration(200L);
anim.setInterpolator(INTERPOLATOR);
button.startAnimation(anim);
}
}
豎直方向上的平移動畫:
@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 && !this.mIsAnimatingOut && child.getVisibility() == View.VISIBLE) {
// User scrolled down and the FAB is currently visible -> hide the FAB
animateOut(child);
} else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
// User scrolled up and the FAB is currently not visible -> show the FAB
animateIn(child);
}
}
// Same animation that FloatingActionButton.Behavior uses to hide the FAB when the AppBarLayout exits
private void animateOut(final FloatingActionButton button) {
if (Build.VERSION.SDK_INT >= 14) {
ViewCompat.animate(button)
.translationY(button.getHeight() + getMarginBottom(button))
.setInterpolator(INTERPOLATOR)
.withLayer()
.setListener(new ViewPropertyAnimatorListener() {
public void onAnimationStart(View view) {
ScrollAwareFABBehavior.this.mIsAnimatingOut = true;
}
public void onAnimationCancel(View view) {
ScrollAwareFABBehavior.this.mIsAnimatingOut = false;
}
public void onAnimationEnd(View view) {
ScrollAwareFABBehavior.this.mIsAnimatingOut = false;
view.setVisibility(View.GONE);
}
})
.start();
} else {
Animation anim = AnimationUtils.loadAnimation(button.getContext(),
android.support.design.R.anim.design_fab_out);
anim.setInterpolator(INTERPOLATOR);
anim.setDuration(200L);
anim.setAnimationListener(new Animation.AnimationListener() {
public void onAnimationStart(Animation animation) {
ScrollAwareFABBehavior.this.mIsAnimatingOut = true;
}
public void onAnimationEnd(Animation animation) {
ScrollAwareFABBehavior.this.mIsAnimatingOut = false;
button.setVisibility(View.GONE);
}
@Override public void onAnimationRepeat(final Animation animation) {
}
});
button.startAnimation(anim);
}
}
// Same animation that FloatingActionButton.Behavior uses to show the FAB when the AppBarLayout enters
private void animateIn(FloatingActionButton button) {
button.setVisibility(View.VISIBLE);
if (Build.VERSION.SDK_INT >= 14) {
ViewCompat.animate(button)
.translationY(0)
.setInterpolator(INTERPOLATOR)
.withLayer()
.setListener(null)
.start();
} else {
Animation anim = AnimationUtils.loadAnimation(button.getContext(),
android.support.design.R.anim.design_fab_in);
anim.setDuration(200L);
anim.setInterpolator(INTERPOLATOR);
button.startAnimation(anim);
}
}
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;
}
效果如圖所示:
另外阵子,FAB
的xml中有幾個屬性要說下思杯,app:backgroundTint
是設(shè)置正常狀態(tài)下的背景顏色,app:rippleColor
是設(shè)置點擊狀態(tài)下的波紋顏色,app:srcCompat
是設(shè)置它里面的圖片色乾。
Snackbar
Snackbar
的用法也很簡單:
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG).show();
或者
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", new View.OnClickListener() {
@Override public void onClick(View view) {
// do sth
}
})
.show();
谷歌官方推薦Snackbar
使用時外層包裹一個CoordinatorLayout
作為parent
誊册,以確保它和其他組件的交互正常,它本質(zhì)上是一個顯示在屏幕最上層的FrameLayout
暖璧,看源碼可知案怯,Snackbar
繼承自BaseTransientBottomBar
,而BaseTransientBottomBar
的構(gòu)造方法中可以看到
mView = (SnackbarBaseLayout) inflater.inflate(R.layout.design_layout_snackbar, mTargetParent, false);
mView.addView(content);
Snackbar
只是一個容器view
澎办,content
才是它的內(nèi)容嘲碱,再看看design_layout_snackbar.xml
,目錄在sdk\extras\android\m2repository\com\android\support\design\xx\design-xx.aar
局蚀,解壓即可麦锯。
<?xml version="1.0" encoding="utf-8"?>
<view xmlns:android="http://schemas.android.com/apk/res/android"
class="android.support.design.widget.Snackbar$SnackbarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:theme="@style/ThemeOverlay.AppCompat.Dark"
style="@style/Widget.Design.Snackbar" />
我們看到class這個屬性,它表示這個view
實際上是Snackbar
中的內(nèi)部類SnackbarLayout
:
/**
* @hide
*
* Note: this class is here to provide backwards-compatible way for apps written before
* the existence of the base {@link BaseTransientBottomBar} class.
*/
@RestrictTo(LIBRARY_GROUP)
public static final class SnackbarLayout extends BaseTransientBottomBar.SnackbarBaseLayout {
public SnackbarLayout(Context context) {
super(context);
}
public SnackbarLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
SnackbarBaseLayout
正是一個FrameLayout
至会,上面說content
才是它內(nèi)容布局离咐,是在make
方法中生成的:
@NonNull
public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
@Duration int duration) {
final ViewGroup parent = findSuitableParent(view);
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
final SnackbarContentLayout content = (SnackbarContentLayout) inflater.inflate(
R.layout.design_layout_snackbar_include, parent, false);
final Snackbar snackbar = new Snackbar(parent, content, content);
snackbar.setText(text);
snackbar.setDuration(duration);
return snackbar;
}
內(nèi)容布局就是一個SnackbarContentLayout
類,而它是LinearLayout
的子類奉件,也就是一個線性布局宵蛀。再看看design_layout_snackbar_include.xml
它的內(nèi)部布局:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/snackbar_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingTop="@dimen/design_snackbar_padding_vertical"
android:paddingBottom="@dimen/design_snackbar_padding_vertical"
android:paddingLeft="@dimen/design_snackbar_padding_horizontal"
android:paddingRight="@dimen/design_snackbar_padding_horizontal"
android:textAppearance="@style/TextAppearance.Design.Snackbar.Message"
android:maxLines="@integer/design_snackbar_text_max_lines"
android:layout_gravity="center_vertical|left|start"
android:ellipsize="end"
android:textAlignment="viewStart"/>
<Button
android:id="@+id/snackbar_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/design_snackbar_extra_spacing_horizontal"
android:layout_marginStart="@dimen/design_snackbar_extra_spacing_horizontal"
android:layout_gravity="center_vertical|right|end"
android:minWidth="48dp"
android:visibility="gone"
android:textColor="?attr/colorAccent"
style="?attr/borderlessButtonStyle"/>
</merge>
所以左邊是一個顯示message
的TextView
,右邊是提供點擊事件的Button
县貌,知道了它組件的id术陶,那我們給它設(shè)置自己喜歡的背景色和文字顏色都可以了:
Snackbar snackbar = Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG);
snackbar.setAction("點我點我", new View.OnClickListener() {
@Override public void onClick(View view) {
// do your sth
}
});
View snackbarView = snackbar.getView();
// 修改snackbar的背景顏色
snackbarView.setBackgroundColor(getResources().getColor(R.color.colorPrimary));
// 修改snackbar中message文字的顏色
((TextView) snackbarView.findViewById(android.support.design.R.id.snackbar_text)).setTextColor(getResources().getColor(R.color.colorAccent));
// 修改snackbar中action按鈕文字的顏色
((Button) snackbarView.findViewById(android.support.design.R.id.snackbar_action)).setTextColor(getResources().getColor(R.color.colorAccent));
snackbar.show();
效果如圖:
Snackbar
每次只能顯示一個,原因我們可以看下源碼中Snackbar
的show()
方法:
/**
* Show the {@link BaseTransientBottomBar}.
*/
public void show() {
SnackbarManager.getInstance().show(mDuration, mManagerCallback);
}
它的顯示和隱藏是有SnackbarManager
來控制的煤痕,看到SnackbarManager
這種寫法梧宫,我已經(jīng)猜出來它是個單例了:
private static SnackbarManager sSnackbarManager;
static SnackbarManager getInstance() {
if (sSnackbarManager == null) {
sSnackbarManager = new SnackbarManager();
}
return sSnackbarManager;
}
再來看SnackbarManager
中的show()
方法實現(xiàn):
public void show(int duration, Callback callback) {
synchronized (mLock) {
if (isCurrentSnackbarLocked(callback)) {
// Means that the callback is already in the queue. We'll just update the duration
mCurrentSnackbar.duration = duration;
// If this is the Snackbar currently being shown, call re-schedule it's
// timeout
mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
scheduleTimeoutLocked(mCurrentSnackbar);
return;
} else if (isNextSnackbarLocked(callback)) {
// We'll just update the duration
mNextSnackbar.duration = duration;
} else {
// Else, we need to create a new record and queue it
mNextSnackbar = new SnackbarRecord(duration, callback);
}
if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,
Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) {
// If we currently have a Snackbar, try and cancel it and wait in line
return;
} else {
// Clear out the current snackbar
mCurrentSnackbar = null;
// Otherwise, just show it now
showNextSnackbarLocked();
}
}
}
可以看到這個方法是同步加鎖的,只有當前沒有Snackbar
顯示時才會讓其顯示摆碉,否則會先cancelSnackbarLocked
當前的Snackbar
的回調(diào)和信息塘匣,然后調(diào)用onDismissed
方法,等這條消失后再showNextSnackbarLocked
下一個巷帝。它用兩個SnackbarRecord
類型的變量mCurrentSnackbar
和mNextSnackbar
來維持了顯示和待顯示的Snackbar
隊列忌卤。
它的作用其實和Toast
類似,也是給用戶一些友好的提示信息楞泼,不過它比Toast
更加豐富驰徊,不僅可以顯示message
還可以setAction
設(shè)置Snackbar右側(cè)按鈕,增加進行交互事件堕阔。
TextInputLayout
AndroidStudio
默認新建的LoginActivity
中就有使用TextInputLayout
棍厂,我們來看看它的xml布局:
<LinearLayout
android:id="@+id/email_login_form"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<AutoCompleteTextView
android:id="@+id/email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/prompt_email"
android:inputType="textEmailAddress"
android:maxLines="1"/>
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/prompt_password"
android:imeActionId="@+id/login"
android:imeActionLabel="@string/action_sign_in_short"
android:imeOptions="actionUnspecified"
android:inputType="textPassword"
android:maxLines="1"
android:singleLine="true"/>
</android.support.design.widget.TextInputLayout>
<Button
style="?android:textAppearanceSmall"
android:id="@+id/email_sign_in_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/action_sign_in"
android:textStyle="bold"/>
</LinearLayout>
從布局上就可以看出TextInputLayout
實際上就是一個ViewGroup
,它是繼承自LinearLayout
超陆,且排列方式是豎直方向:
setOrientation(VERTICAL);
它的構(gòu)造方法中牺弹,先addView
一個FrameLayout
:
mInputFrame = new FrameLayout(context);
mInputFrame.setAddStatesFromChildren(true);
addView(mInputFrame);
然后重寫了addView
方法,如果是EditText
控件,就添加到mInputFrame
中:
@Override
public void addView(View child, int index, final ViewGroup.LayoutParams params) {
if (child instanceof EditText) {
mInputFrame.addView(child, new FrameLayout.LayoutParams(params));
// Now use the EditText's LayoutParams as our own and update them to make enough space
// for the label
mInputFrame.setLayoutParams(params);
updateInputLayoutMargins();
setEditText((EditText) child);
} else {
// Carry on adding the View...
super.addView(child, index, params);
}
}
其中setEditText
方法中給EditText
設(shè)置了監(jiān)聽:
// Add a TextWatcher so that we know when the text input has changed
mEditText.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
updateLabelState(true);
if (mCounterEnabled) {
updateCounter(s.length());
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
});
并且在方法最后調(diào)用了下面這個方法:
// Update the label visibility with no animation
updateLabelState(false);
在EditText
的監(jiān)聽方法中也有調(diào)用該方法例驹,它是根據(jù)EditText
是否獲取了焦點捐韩,是否有文字等判斷提示文字的展開或折疊:
void updateLabelState(boolean animate) {
final boolean isEnabled = isEnabled();
final boolean hasText = mEditText != null && !TextUtils.isEmpty(mEditText.getText());
final boolean isFocused = arrayContains(getDrawableState(), android.R.attr.state_focused);
final boolean isErrorShowing = !TextUtils.isEmpty(getError());
if (mDefaultTextColor != null) {
mCollapsingTextHelper.setExpandedTextColor(mDefaultTextColor);
}
if (isEnabled && mCounterOverflowed && mCounterView != null) {
mCollapsingTextHelper.setCollapsedTextColor(mCounterView.getTextColors());
} else if (isEnabled && isFocused && mFocusedTextColor != null) {
mCollapsingTextHelper.setCollapsedTextColor(mFocusedTextColor);
} else if (mDefaultTextColor != null) {
mCollapsingTextHelper.setCollapsedTextColor(mDefaultTextColor);
}
if (hasText || (isEnabled() && (isFocused || isErrorShowing))) {
// We should be showing the label so do so if it isn't already
collapseHint(animate);
} else {
// We should not be showing the label so hide it
expandHint(animate);
}
}
無論是展開還是折疊hint文字,最終都會調(diào)用mCollapsingTextHelper
的setExpansionFraction
方法:
private void collapseHint(boolean animate) {
if (mAnimator != null && mAnimator.isRunning()) {
mAnimator.cancel();
}
if (animate && mHintAnimationEnabled) {
animateToExpansionFraction(1f);
} else {
mCollapsingTextHelper.setExpansionFraction(1f);
}
mHintExpanded = false;
}
private void expandHint(boolean animate) {
if (mAnimator != null && mAnimator.isRunning()) {
mAnimator.cancel();
}
if (animate && mHintAnimationEnabled) {
animateToExpansionFraction(0f);
} else {
mCollapsingTextHelper.setExpansionFraction(0f);
}
mHintExpanded = true;
}
private void animateToExpansionFraction(final float target) {
if (mCollapsingTextHelper.getExpansionFraction() == target) {
return;
}
if (mAnimator == null) {
mAnimator = ViewUtils.createAnimator();
mAnimator.setInterpolator(AnimationUtils.LINEAR_INTERPOLATOR);
mAnimator.setDuration(ANIMATION_DURATION);
mAnimator.addUpdateListener(new ValueAnimatorCompat.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimatorCompat animator) {
mCollapsingTextHelper.setExpansionFraction(animator.getAnimatedFloatValue());
}
});
}
mAnimator.setFloatValues(mCollapsingTextHelper.getExpansionFraction(), target);
mAnimator.start();
}
mCollapsingTextHelper
是提示文字的折疊輔助類鹃锈,它計算EditText
的文字展開和折疊時邊界的尺寸荤胁,setExpansionFraction
方法是根據(jù)傳遞的分數(shù)來計算文字當前展開或折疊的程度。
/**
* Set the value indicating the current scroll value. This decides how much of the
* background will be displayed, as well as the title metrics/positioning.
*
* A value of {@code 0.0} indicates that the layout is fully expanded.
* A value of {@code 1.0} indicates that the layout is fully collapsed.
*/
void setExpansionFraction(float fraction) {
fraction = MathUtils.constrain(fraction, 0f, 1f);
if (fraction != mExpandedFraction) {
mExpandedFraction = fraction;
calculateCurrentOffsets();
}
}
完成計算后調(diào)用postInvalidateOnAnimation
方法進行重繪屎债。
CollapsingToolbarLayout
用AndroidStudio
工具新建的ScrollingActivity
模板其實就是一個CollapsingToolbarLayout
的使用場景仅政,它默認的xml布局:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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"
android:fitsSystemWindows="true"
tools:context="com.xx.design.CollapsingToolbarLayoutActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/app_bar_height"
android:fitsSystemWindows="true"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:contentScrim="?attr/colorPrimary">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/AppTheme.PopupOverlay"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_collapsing_toolbar_layout"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/fab_margin"
app:layout_anchor="@id/app_bar"
app:layout_anchorGravity="bottom|end"
app:srcCompat="@android:drawable/ic_dialog_email"/>
</android.support.design.widget.CoordinatorLayout>
在Toolbar
外面還包裹了一層CollapsingToolbarLayout
,它實際是FrameLayout
盆驹。CollapsingToolbarLayout
可以通過app:contentScrim
設(shè)置折疊時工具欄布局的顏色圆丹,通過app:statusBarScrim
設(shè)置折疊時狀態(tài)欄的顏色。默認contentScrim
是colorPrimary
的色值躯喇,statusBarScrim
是colorPrimaryDark
的色值辫封。CollapsingToolbarLayout
的子布局有3種折疊模式app:layout_collapseMode
,none這個是默認屬性廉丽,布局將正常顯示倦微,沒有折疊的行為,pin表示CollapsingToolbarLayout
折疊后正压,此布局將固定在頂部欣福,parallax表示CollapsingToolbarLayout
折疊時,此布局也會有視差折疊效果焦履。
再看FAB
的位置拓劝,是因為它設(shè)置了這兩個屬性:app:layout_anchor="@id/app_bar"
和app:layout_anchorGravity="bottom|end"
。
一般的做法是當Toolbar
展開時顯示一張背景圖片嘉裤,只需要在Toolbar
后面加一個ImageView
即可:
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:contentScrim="?attr/colorPrimary">
<!--封面圖片-->
<ImageView
android:id="@+id/imageview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@mipmap/toolbar"
app:layout_collapseMode="pin"
android:fitsSystemWindows="true"/>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/AppTheme.PopupOverlay">
</android.support.v7.widget.Toolbar>
</android.support.design.widget.CollapsingToolbarLayout>
看CollapsingToolbarLayout
的源碼發(fā)現(xiàn)郑临,它里面也用到了CollapsingTextHelper
類,就是在Toolbar
上的文字title的展開和折疊屑宠,其實是和TextInputLayout
一樣的牧抵。當它的外層是AppBarLayout
包裹時,可以監(jiān)聽其豎直方向上偏移量的變化:
private class OffsetUpdateListener implements AppBarLayout.OnOffsetChangedListener {
OffsetUpdateListener() {
}
@Override
public void onOffsetChanged(AppBarLayout layout, int verticalOffset) {
mCurrentOffset = verticalOffset;
final int insetTop = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
for (int i = 0, z = getChildCount(); i < z; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child);
switch (lp.mCollapseMode) {
case LayoutParams.COLLAPSE_MODE_PIN:
offsetHelper.setTopAndBottomOffset(
constrain(-verticalOffset, 0, getMaxOffsetForPinChild(child)));
break;
case LayoutParams.COLLAPSE_MODE_PARALLAX:
offsetHelper.setTopAndBottomOffset(
Math.round(-verticalOffset * lp.mParallaxMult));
break;
}
}
// Show or hide the scrims if needed
updateScrimVisibility();
if (mStatusBarScrim != null && insetTop > 0) {
ViewCompat.postInvalidateOnAnimation(CollapsingToolbarLayout.this);
}
// Update the collapsing text's fraction
final int expandRange = getHeight() - ViewCompat.getMinimumHeight(
CollapsingToolbarLayout.this) - insetTop;
mCollapsingTextHelper.setExpansionFraction(
Math.abs(verticalOffset) / (float) expandRange);
}
}
增加監(jiān)聽:
((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener);
另外需要注意的是下面滑動的部分侨把,是
NestedScrollView
而不能時ScrollView
,因為前者才實現(xiàn)了NestedScrollingParent
接口妹孙,和CoordinatorLayout
結(jié)合使用滾動時才會有動畫的效果秋柄。