概述
最近利用上下班在路上的时间一直在看一些技术文章,目的当然是学习别人的开发经验来提高自己的技术水平。读别人的文章很有趣,即使是我以为很简单的知识点,别人也能写出一些新花样来,这时你会惊奇的发现,原来这玩意还能这么玩!!谷歌推出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,因为使用另外两个可能会在滑动过程中导致一些问题出现。
1701111
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的关系:
1701112
页面布局就是这样了,在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,它一般放在屏幕右下角,比如这样:
smooth.png
它的作用比较简单,即提供一个点击事件,去做相应的操作,现在我们要研究的是滑动屏幕时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.gif
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
看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结合使用滚动时才会有动画的效果。












网友评论