概述
最近利用上下班在路上的时间一直在看一些技术文章,目的当然是学习别人的开发经验来提高自己的技术水平。读别人的文章很有趣,即使是我以为很简单的知识点,别人也能写出一些新花样来,这时你会惊奇的发现,原来这玩意还能这么玩!!谷歌推出android.support.design
中的组件也好久了,写demo的人也很多,但是在实际开发过程中使用率还没有达到很普遍的程度,一是因为开发者对这些新的东西还没有比较深的学习和认识,我相信即使是现在仍然有很多人还不知道怎么使用或者不清楚他们的实现原理;二是因为开发习惯,比如很多人对ListView
用的比较熟悉,所以当他有一个需求需要用到列表显示数据时,他当然首选ListView
而不是RecyclerView
了,这也是由于上一点原因,对新组件的不熟悉导致不愿意去在实际开发中使用这些东西。但,现在是时候放弃那些老古董了。
android.support.design
中的组件我最常用的也就是AppBarLayout
和TabLayout
了,因为它很好的取代了ViewPagerIndicator这个开源组件,而其他的比如FloatingActionButton
、NavigationView
、Snackbar
、TextInputLayout
、CollapsingToolbarLayout
这些都使用的很少,虽然不常用到部分原因是产品设计出的UI原型可能不会遵循Material Design
,但是掌握他们的基本使用方法是有必要的,万一哪天就用上了呢。
NavigationView
NavigationView
一般和v4包中的DrawerLayout
结合使用,虽然目前大部分的app放弃了这种侧滑显示菜单的设计方式而改成顶部或底部Tab切换菜单,但仍有一些使用到的。它本质上是一个FrameLayout
,当在Android Studio
新建工程到选择Activity
模式时选择Navigation Drawer Activity
时会自动帮你构建一个带有侧滑抽屉效果的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
也就是相当于主屏幕显示内容的区域,它的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
:这个属性控制侧滑菜单的位置是左边还是右边。它有start
、end
、left
、right
四个值,其中start
和left
都表示位置在左边,end
和right
都表示位置在右边,但官方推荐使用start
和end
,因为使用另外两个可能会在滑动过程中导致一些问题出现。
DrawerLayout
中的openDrawer
属性是控制NavigationView
的打开和关闭的方向,所以它的值一般和layout_gravity
设置成一样。
-
app:headerLayout
:它是侧滑页面的顶部,一般放用户头像、性别、个性签名这些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
的关系:
页面布局就是这样了,在Activity
中初始化DrawerLayout
和NavigationView
并设置监听:
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);
// 监听侧滑开关的切换
drawer.addDrawerListener(toggle);
toggle.syncState();
NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
navigationView.setNavigationItemSelectedListener(this);
还需要实现NavigationView.OnNavigationItemSelectedListener
这个接口,是点击menu
中的菜单的监听,并且在onNavigationItemSelected
方法中处理回调:
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;
}
退出界面时需要关闭菜单:
@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();
}
}
这么看来,使用还是挺简单的,开发者仅仅需要关注布局和点击事件的处理,界面的交互google已经帮我们都处理好了。
FloatingActionButton
FloatingActionButton
实质上是ImageButton
,它的使用也很简单:
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View view) {
//TODO: do sth
}
});
目前一般遵循MD设计风格的app都会使用到FAB
,它一般放在屏幕右下角,比如这样:
它的作用比较简单,即提供一个点击事件,去做相应的操作,现在我们要研究的是滑动屏幕时FAB
的显示和隐藏。
常见的隐藏和显示效果有两种:一种是缩放动画,一种是平移动画,当FAB
与CoordinatorLayout
一起使用时,给它设置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
又实现了NestedScrollingParent
接口来监听列表的滚动,所以要隐藏或显示FAB
,只需要onStartNestedScroll
方法中选择垂直方向ViewCompat.SCROLL_AXIS_VERTICAL
上的滚动,在onNestedScroll
方法中监听滑动是向上还是向下。另外谷歌官方在22.2.1版本给FAB
加了hide
和show
两个方法,它的效果是做缩放动画,22.2.1之前的版本需要自己实现,可以这么写:
@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
是设置正常状态下的背景颜色,app:rippleColor
是设置点击状态下的波纹颜色,app:srcCompat
是设置它里面的图片。
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
,以确保它和其他组件的交互正常,它本质上是一个显示在屏幕最上层的FrameLayout
,看源码可知,Snackbar
继承自BaseTransientBottomBar
,而BaseTransientBottomBar
的构造方法中可以看到
mView = (SnackbarBaseLayout) inflater.inflate(R.layout.design_layout_snackbar, mTargetParent, false);
mView.addView(content);
Snackbar
只是一个容器view
,content
才是它的内容,再看看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
中的内部类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
才是它内容布局,是在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;
}
内容布局就是一个SnackbarContentLayout
类,而它是LinearLayout
的子类,也就是一个线性布局。再看看design_layout_snackbar_include.xml
它的内部布局:
<?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,那我们给它设置自己喜欢的背景色和文字颜色都可以了:
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
这种写法,我已经猜出来它是个单例了:
private static SnackbarManager sSnackbarManager;
static SnackbarManager getInstance() {
if (sSnackbarManager == null) {
sSnackbarManager = new SnackbarManager();
}
return sSnackbarManager;
}
再来看SnackbarManager
中的show()
方法实现:
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
的回调和信息,然后调用onDismissed
方法,等这条消失后再showNextSnackbarLocked
下一个。它用两个SnackbarRecord
类型的变量mCurrentSnackbar
和mNextSnackbar
来维持了显示和待显示的Snackbar
队列。
它的作用其实和Toast
类似,也是给用户一些友好的提示信息,不过它比Toast
更加丰富,不仅可以显示message
还可以setAction
设置Snackbar右侧按钮,增加进行交互事件。
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);
它的构造方法中,先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
设置了监听:
// 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) {}
});
并且在方法最后调用了下面这个方法:
// Update the label visibility with no animation
updateLabelState(false);
在EditText
的监听方法中也有调用该方法,它是根据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文字,最终都会调用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
方法是根据传递的分数来计算文字当前展开或折叠的程度。
/**
* 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();
}
}
完成计算后调用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
设置折叠时工具栏布局的颜色,通过app:statusBarScrim
设置折叠时状态栏的颜色。默认contentScrim
是colorPrimary
的色值,statusBarScrim
是colorPrimaryDark
的色值。CollapsingToolbarLayout
的子布局有3种折叠模式app:layout_collapseMode
,none这个是默认属性,布局将正常显示,没有折叠的行为,pin表示CollapsingToolbarLayout
折叠后,此布局将固定在顶部,parallax表示CollapsingToolbarLayout
折叠时,此布局也会有视差折叠效果。
再看FAB
的位置,是因为它设置了这两个属性: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
的源码发现,它里面也用到了CollapsingTextHelper
类,就是在Toolbar
上的文字title的展开和折叠,其实是和TextInputLayout
一样的。当它的外层是AppBarLayout
包裹时,可以监听其竖直方向上偏移量的变化:
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);
}
}
增加监听:
((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener);
另外需要注意的是下面滑动的部分,是
NestedScrollView
而不能时ScrollView
,因为前者才实现了NestedScrollingParent
接口,和CoordinatorLayout
结合使用滚动时才会有动画的效果。