Material Design(一):material Design 的 主题以及取色板的使用
https://blog.csdn.net/weixin_41729259/article/details/89554791
Material Design(二)(控件)
https://blog.csdn.net/weixin_41729259/article/details/94493602
这篇将接下来的控件整理完。
- BottomNavigationView
- AppBarLayout(应用程序栏布局)、
- CoordinatorLayout(协作布局)、
- CollapsingToolbarLayout(折叠工具栏布局
- NestedScrollView
引用:
compile ‘com.android.support:design:26.0.0-alpha1’
BottomNavigationView
参考:
去掉变大动画,增加角标
https://www.jianshu.com/p/8f915ba6c5a7
xml 文件:
<android.support.design.widget.BottomNavigationView
android:id="@+id/bottomnavigationview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:itemIconTint="@drawable/selector_tab_color"
app:itemTextColor="@drawable/selector_tab_color"
app:menu="@menu/bottom_navigation_tab">
</android.support.design.widget.BottomNavigationView>
属性:
itemIconTint:icon图片的颜色
itemTextColor:文本的颜色
menu:tab的布局
selector_tab_color:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:color="@color/colorAccent"/>
<item android:state_checked="false" android:color="@color/colorPrimary"/>
</selector>
一般来说图片的颜色是和文字的颜色是一致的。
菜单的布局:
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/tab_one"
android:icon="@drawable/icon_one_selected"
android:title="首页"/>
<item
android:id="@+id/tab_two"
android:icon="@drawable/icon_two_selected"
android:title="消息"/>
<item
android:id="@+id/tab_three"
android:icon="@drawable/icon_three_selected"
android:title="订单"/>
<item
android:id="@+id/tab_four"
android:icon="@drawable/icon_four_selected"
android:title="我的"/>
</menu>
这种情况只适用于图片是纯色且选中和未选中时的图片是一样的。如果是不同的图片需要新建一个selector文件,
设置选中时的图片和未选中时的图片,并且不设置itemIconTint属性。
完整的activity代码:
public class BottomNavigationViewActivity extends AppCompatActivity implements BottomNavigationView.OnNavigationItemSelectedListener, ViewPager.OnPageChangeListener {
ViewPager viewPager;
BottomNavigationView bottomNavigationView;
private MenuItem menuItem;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_bottom_navigation_view);
viewPager = (ViewPager) findViewById(R.id.viewpager);
bottomNavigationView = (BottomNavigationView) findViewById(R.id.bottomnavigationview);
disableShiftMode(bottomNavigationView);
bottomNavigationView.setOnNavigationItemSelectedListener(this);
viewPager.addOnPageChangeListener(this);
bottomNavigationView.setSelectedItemId(R.id.tab_two);
viewPager.setAdapter(new ViewPagerAdapter(getSupportFragmentManager()));
}
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
int itemId = item.getItemId();
switch (itemId){
case R.id.tab_one:
viewPager.setCurrentItem(0);
break;
case R.id.tab_two:
viewPager.setCurrentItem(1);
break;
case R.id.tab_three:
viewPager.setCurrentItem(2);
break;
case R.id.tab_four:
viewPager.setCurrentItem(3);
break;
}
return false;
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
menuItem = bottomNavigationView.getMenu().getItem(position);
menuItem.setChecked(true);
}
@Override
public void onPageScrollStateChanged(int state) {
}
public void disableShiftMode(BottomNavigationView navigationView) {
BottomNavigationMenuView menuView = (BottomNavigationMenuView) navigationView.getChildAt(0);
try {
Field shiftingMode = menuView.getClass().getDeclaredField("mShiftingMode");
shiftingMode.setAccessible(true);
shiftingMode.setBoolean(menuView, false);
shiftingMode.setAccessible(false);
for (int i = 0; i < menuView.getChildCount(); i++) {
BottomNavigationItemView itemView = (BottomNavigationItemView) menuView.getChildAt(i);
itemView.setShiftingMode(false);
itemView.setChecked(itemView.getItemData().isChecked());
}
} catch (Exception e) {
e.printStackTrace();
}
}
class ViewPagerAdapter extends FragmentPagerAdapter {
private Fragment[] mFragments = new Fragment[]{new OneFragment(), new TwoFragment(), new ThreeFragment(),new FourFragment()};
public ViewPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
return mFragments[position];
}
@Override
public int getCount() {
return 4;
}
}
}
注意
app:itemTextColor="@drawable/tab_menu_selector_text"
是设置文本点击变化的app:labelVisibilityMode=“labeled”
如果底部超过三个tab,设置此属性可以正常显示navigation.setItemIconTintList(null);
去掉默认点击背景色3.动态显示/隐藏某个Tab根据id来获取当前的itemMenuItem homeItem =
navigation.getMenu().findItem(R.id.navigation_order);homeItem.setVisible(true);
//true默认显示,false不显示
1、BottomNavigationView只适用于3到5个的导航栏;
2、当tab个数大余3个时,BottomNavigationView不会均分宽度,一般来说我们都是需要均分宽度。
解决方案:disableShiftMode(bottomNavigationView);
public void disableShiftMode(BottomNavigationView navigationView) {
BottomNavigationMenuView menuView = (BottomNavigationMenuView) navigationView.getChildAt(0);
try {
Field shiftingMode = menuView.getClass().getDeclaredField("mShiftingMode");
shiftingMode.setAccessible(true);
shiftingMode.setBoolean(menuView, false);
shiftingMode.setAccessible(false);
for (int i = 0; i < menuView.getChildCount(); i++) {
BottomNavigationItemView itemView = (BottomNavigationItemView) menuView.getChildAt(i);
itemView.setShiftingMode(false);
itemView.setChecked(itemView.getItemData().isChecked());
}
} catch (Exception e) {
e.printStackTrace();
}
}
来看下关于mShiftingMode这个变量的源码,在BottomNavigationMenuView中:
mShiftingMode = mMenu.size() > 3;//当大于3时为true
for (int i = 0; i < mMenu.size(); i++) {
mPresenter.setUpdateSuspended(true);
mMenu.getItem(i).setCheckable(true);
mPresenter.setUpdateSuspended(false);
BottomNavigationItemView child = getNewItem();
mButtons[i] = child;
child.setIconTintList(mItemIconTint);
child.setTextColor(mItemTextColor);
child.setItemBackground(mItemBackgroundRes);
child.setShiftingMode(mShiftingMode);
child.initialize((MenuItemImpl) mMenu.getItem(i), 0);
child.setItemPosition(i);
child.setOnClickListener(mOnClickListener);
addView(child);
}
在执行onMeasure方法时:
if (mShiftingMode) {
final int inactiveCount = count - 1;
final int activeMaxAvailable = width - inactiveCount * mInactiveItemMinWidth;
final int activeWidth = Math.min(activeMaxAvailable, mActiveItemMaxWidth);
final int inactiveMaxAvailable = (width - activeWidth) / inactiveCount;
final int inactiveWidth = Math.min(inactiveMaxAvailable, mInactiveItemMaxWidth);
int extra = width - activeWidth - inactiveWidth * inactiveCount;
for (int i = 0; i < count; i++) {
mTempChildWidths[i] = (i == mSelectedItemPosition) ? activeWidth : inactiveWidth;
if (extra > 0) {
mTempChildWidths[i]++;
extra--;
}
}
} else {
final int maxAvailable = width / (count == 0 ? 1 : count);
final int childWidth = Math.min(maxAvailable, mActiveItemMaxWidth);
int extra = width - childWidth * count;
for (int i = 0; i < count; i++) {
mTempChildWidths[i] = childWidth;
if (extra > 0) {
mTempChildWidths[i]++;
extra--;
}
}
}
图是mShiftingMode为true的情况下debug拿到的数据,再结合效果图,即可分析出:
inactiveCount为闲置的个数,即没有被选中的menuItem的个数,选中的宽度activeWidth和未选中的宽度inactiveWidth不一致。
当mShiftingMode为false执行的代码很容易看出宽度是均分计算的。
其他
源码里面的各个属性的设置:
<dimen name="design_bottom_navigation_active_item_max_width">168dp</dimen>//选中时的最大宽度
<dimen name="design_bottom_navigation_active_text_size">14sp</dimen>//选中时的字体大小
<dimen name="design_bottom_navigation_elevation">8dp</dimen>//阴影的大小
<dimen name="design_bottom_navigation_height">56dp</dimen>//高度
<dimen name="design_bottom_navigation_item_max_width">96dp</dimen>//未选中的最大宽度
<dimen name="design_bottom_navigation_item_min_width">56dp</dimen>//未选中的最小的宽度
<dimen name="design_bottom_navigation_margin">8dp</dimen>//icon与文本之间的间距
<dimen name="design_bottom_navigation_shadow_height">1dp</dimen>//阴影高度
<dimen name="design_bottom_navigation_text_size">12sp</dimen>//未选中时的字体大小
AppBarLayout
参考:
https://www.jianshu.com/p/bbc703a0015e
在许多App中看到, toolbar有收缩和扩展的效果, 例如:
要实现这样的效果, 需要用到:
CoordinatorLayout和AppbarLayout的配合, 以及实现了NestedScrollView的布局或控件.
AppbarLayout是一种支持响应滚动手势的app bar布局, CollapsingToolbarLayout则是专门用来实现子布局内不同元素响应滚动细节的布局.
与AppbarLayout组合的滚动布局(RecyclerView, NestedScrollView等),需要设置
app:layout_behavior = “@string/appbar_scrolling_view_behavior”
.没有设置的话, AppbarLayout将不会响应滚动布局的滚动事件. 我们回到再前面一章"Toolbar的使用", 将布局改动如下:
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"
tools:context="com.truly.mytoolbar.MainActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:layout_scrollFlags="scroll"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:title="Title" />
</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:id="@+id/tv_content"
android:layout_margin="16dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:lineSpacingMultiplier="2"
android:text="@string/textContent" />
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
先看下效果再来解释为什么.
可以看到,
- 随着文本往上滚动, 顶部的toolbar也往上滚动, 直到消失.
- 随着文本往下滚动, 一直滚到文本的第一行露出来, toolbar也逐渐露出来
解释:
从上面的布局中可以看到, 其实在整个父布局CoordinatorLayout下面, 是有2个子布局
- AppbarLayout
- NestedScrollView
NestedScrollView先放一放, 我们来看AppbarLayout.
AppBarLayout
继承自LinearLayout,布局方向为垂直方向。所以你可以把它当成垂直布局的LinearLayout来使用。AppBarLayout是在LinearLayou上加了一些材料设计的概念,它可以让你定制当某个可滚动View的滚动手势发生变化时,其内部的子View实现何种动作。
注意:
上面提到的"某个可滚动View", 可以理解为某个ScrollView. 就是说,当某个ScrollView发生滚动时,你可以定制你的“顶部栏”应该执行哪些动作(如跟着一起滚动、保持不动等等)。
这里某个ScrollView就是NestedScrollView或者实现了NestedScrollView机制的其它控件, 如RecyclerView. 它有一个布局行为Layout_Behavior:
app:layout_behavior="@string/appbar_scrolling_view_behavior"
这是一个系统behavior, 从字面意思就可以看到, 是为appbar设置滚动动作的一个behavior. 没有这个属性的话, Appbar就是死的, 有了它就有了灵魂.
我们可以通过给Appbar下的子View添加app:layout_scrollFlags来设置各子View执行的动作. scrollFlags可以设置的动作如下:
(1) scroll: 值设为scroll的View会跟随滚动事件一起发生移动。就是当指定的ScrollView发生滚动时,该View也跟随一起滚动,就好像这个View也是属于这个ScrollView一样。
上面这个效果就是设置了scroll之后的.
(2) enterAlways: 值设为enterAlways的View,当任何时候ScrollView往下滚动时,该View会直接往下滚动。而不用考虑ScrollView是否在滚动到最顶部还是哪里.
我们把layout_scrollFlags改动如下:
app:layout_scrollFlags="scroll|enterAlways"
效果如下:
(3) exitUntilCollapsed:值设为exitUntilCollapsed的View,当这个View要往上逐渐“消逝”时,会一直往上滑动,直到剩下的的高度达到它的最小高度后,再响应ScrollView的内部滑动事件。
怎么理解呢?简单解释:在ScrollView往上滑动时,首先是View把滑动事件“夺走”,由View去执行滑动,直到滑动最小高度后,把这个滑动事件“还”回去,让ScrollView内部去上滑。
把属性改下再看效果
<android.support.v7.widget.Toolbar
...
android:layout_height="?attr/actionBarSize"
android:minHeight="20dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
/>
(4) enterAlwaysCollapsed:是enterAlways的附加选项,一般跟enterAlways一起使用,它是指,View在往下“出现”的时候,首先是enterAlways效果,当View的高度达到最小高度时,View就暂时不去往下滚动,直到ScrollView滑动到顶部不再滑动时,View再继续往下滑动,直到滑到View的顶部结束
这个得把高度加大点才好实验. 来看:
<android.support.v7.widget.Toolbar
...
android:layout_height="200dp"
android:minHeight="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"
</android.support.design.widget.AppBarLayout>
Attention:
其实toolbar的默认最小高度minHeight就是"?attr/actionBarSize" , 很多时候可以不用设置. 而且从图上可以看出, 其实这里有个缺陷, 就是title的位置和toolbar上的图标行脱离了, 即使在布局里添加了 android:gravity=“bottom|start”, 在toolbar滚动的时候, title还在, 图标滚动到隐藏了.
后面讲解的CollapsingToolbarLayout可以解决这个问题, 这里先丢出来.
(5) snap:简单理解,就是Child View滚动比例的一个吸附效果。也就是说,Child View不会存在局部显示的情况,滚动Child View的部分高度,当我们松开手指时,Child View要么向上全部滚出屏幕,要么向下全部滚进屏幕,有点类似ViewPager的左右滑动
引入CollapsingToolbarLayout
CollapsingToolbarLayout是用来对Toolbar进行再次包装的ViewGroup,主要是用于实现折叠(其实就是看起来像伸缩~)的App Bar效果。它需要放在AppBarLayout布局里面,并且作为AppBarLayout的直接子View。CollapsingToolbarLayout主要包括几个功能(参照了官方网站上内容,略加自己的理解进行解释):
(1) 折叠Title(Collapsing
title):当布局内容全部显示出来时,title是最大的,但是随着View逐步移出屏幕顶部,title变得越来越小。你可以通过调用setTitle方法来设置title。
(2)内容纱布(Content
scrim):根据滚动的位置是否到达一个阀值,来决定是否对View“盖上纱布”。可以通过setContentScrim(Drawable)来设置纱布的图片.
默认contentScrim是colorPrimary的色值
(3)状态栏纱布(Status bar
scrim):根据滚动位置是否到达一个阀值决定是否对状态栏“盖上纱布”,你可以通过setStatusBarScrim(Drawable)来设置纱布图片,但是只能在LOLLIPOP设备上面有作用。默认statusBarScrim是colorPrimaryDark的色值.
(4)视差滚动子View(Parallax scrolling children):
子View可以选择在当前的布局当时是否以“视差”的方式来跟随滚动。(PS:其实就是让这个View的滚动的速度比其他正常滚动的View速度稍微慢一点)。将布局参数app:layout_collapseMode设为parallax
(5)将子View位置固定(Pinned position
children):子View可以选择是否在全局空间上固定位置,这对于Toolbar来说非常有用,因为当布局在移动时,可以将Toolbar固定位置而不受移动的影响。
将app:layout_collapseMode设为pin。
我们来更改一下布局:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
...>
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="150dp">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_collapseMode="parallax"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:title="Title" />
</android.support.design.widget.CollapsingToolbarLayout>
</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:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
android:lineSpacingMultiplier="2"
android:text="@string/textContent" />
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
可以看到, 我们把原本属于toolbar的几个属性移到了CollapsingToolbarLayout上. 分别是:
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
同时给toolbar增加了一个折叠模式属性
app:layout_collapseMode="parallax"
效果图:
嗯嗯, 折叠模式不对, toolbar的顶部图标没了. 我们改下折叠模式:
app:layout_collapseMode="pin"
再看效果图:
我们把scrollFlags属性改下, 看下对比:
app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"
效果还是蛮不错的, 有了点Google Material Design的感觉了.
上面说CollapsingToolbarLayout是个ViewGroup, 那么肯定还可以添加控件. 那么我们在里面添加一个ImageView来看看. 更改布局如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
...>
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="200dp">
<android.support.design.widget.CollapsingToolbarLayout
...
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/darkbg"
app:layout_collapseMode="parallax" />
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_collapseMode="pin"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:title="Title" />
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
...
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<TextView
... />
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
看下图片
嗯, 有了点意思, 但不美观, 上部的toolbar和图片不协调. toolbar应该有默认的背景属性, 我们去掉它看看.
<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/ThemeOverlay.AppCompat.Light"
app:title="Title" />
再看下效果:
这次真的不错哦, 已经和很多大公司的app相像了. 但是为什么去掉toolbar的background就可以得到透明背景呢? 说句实话, 没找到原因.
不过我们没有给CollapsingToolbarLayout设置contentScrim属性哦, 给它加个属性看看.
<android.support.design.widget.CollapsingToolbarLayout
...
app:contentScrim="?attr/colorPrimary"
...>
嗯嗯, 好像还不如没设置这个属性好呢.
什么时候需要contentScrim属性呢?
因为这个布局里面给CollapsingToolbarLayout的layout_scrollFlags设置的是 “scroll|enterAlways|enterAlwaysCollapsed” , toolbar会全部消失的, 所以感觉不是很美观. 如果将layout_scrollFlags属性改为 “scroll|exitUntilCollapsed” , 效果会好点, 适合toolbar还是需要展示的场合.
不管怎么样, 先去掉contentScrim属性吧.
目前有很多APP比较喜欢采用沉浸式设计, 简单点说就是将状态栏和导航栏都设置成透明或半透明的.
我们来把状态栏statusBar设置成透明. 在style主题中的AppTheme里增加一条:
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
...
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
在布局里面, 将ImageView和所有它上面的父View都添加fitsSystemWindows属性
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
...
android:fitsSystemWindows="true">
<android.support.design.widget.AppBarLayout
...
android:fitsSystemWindows="true">
<android.support.design.widget.CollapsingToolbarLayout
...
android:fitsSystemWindows="true">
<ImageView
...
android:fitsSystemWindows="true" />
<android.support.v7.widget.Toolbar
... />
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
...>
<TextView
... />
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
看下效果图
其实还可以在CollapsingToolbarLayout里设置statusBarScrim为透明色, 不过有点问题, 最顶部的toolbar没有完全隐藏, 还留了一点尾巴.
难道就这个属性就没用吗? 我们把layout_scrollFlags改成 “scroll|exitUntilCollapsed” 看看:
这个时候toolbar不用隐藏, 所以还是美美的.
AppbarLayout整个做成沉浸式之后, 状态栏的图标可能会受到封面图片颜色过浅的影响, 可以给其加一个渐变的不透明层.
渐变遮罩设置方法:
在res/drawable文件夹下新建一个名为status_gradient的xml资源文件, 代码如下:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:angle="270"
android:endColor="@android:color/transparent"
android:startColor="#CC000000" />
<!-- shape节点中, 可以通过android:shape来设置形状, 默认是矩形.
gradient节点中angle的值270是从上到下,0是从左到右,90是从下到上。
此处的效果就是从下向上, 颜色逐渐由纯透明慢慢变成黑透色-->
</shape>
布局中, 在ImageView下面增加一个View, 背景设为上面的渐变遮罩.
<!-- 在顶部增加一个渐变遮罩, 防止出现status bar 状态栏看不清 -->
<View
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@drawable/status_gradient"
app:layout_collapseMode="pin"
android:fitsSystemWindows="true" />
给遮罩设置折叠模式: app:layout_collapseMode=“pin” , 折叠到顶部后定住.
来看下效果
上图是展开状态的对比, 后面的是没有添加遮罩的效果, 前面是添加了遮罩的效果. 下图是添加了遮罩折叠后的效果. 有点黑暗系影片的感觉哦.
FloatingActionButton再次表演
作为Google Material Design的一个重要控件, FloatingActionButton怎么可能不在AppbarLayout中起点作用呢. 我们在布局中加一个悬浮按钮, 让它的锚点挂载Appbar的右下角. 这样这个悬浮按钮就和Appbar关联起来了.
<android.support.design.widget.CoordinatorLayout
...>
<android.support.design.widget.AppBarLayout
...
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
...
</android.support.v4.widget.NestedScrollView>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/ic_share_white_24dp"
android:elevation="4dp"
app:pressedTranslationZ="16dp"
app:rippleColor="@android:color/white"
app:layout_anchor="@id/appbar"
app:layout_anchorGravity="bottom|end"/>
</android.support.design.widget.CoordinatorLayout>
我们来看下效果.
CoordinatorLayout Behavior
参考:
https://www.jianshu.com/p/00118432873a
https://www.jianshu.com/p/b987fad8fcb4
一、认识CoordinatorLayout
CoordinatorLayout作为support:design库里的核心控件,在它出现之前,要实现View之间嵌套滑动等交互操作可不是件容易的事,复杂、难度大,基本绕不开View的事件机制,CoordinatorLayout很大程度上解决了这个痛点,方便我们实现各种炫酷的交互效果。
如果你还没用过CoordinatorLayout,可先了解它的基本用法。
CoordinatorLayout为何如此强大呢?因为它的内部类Behavior,这也是CoordinatorLayout的精髓所在。
二、不可不知的Behavior
使用CoordinatorLayout时,会在xml文件中用它作为根布局,并给相应的子View添加一个类似
app:layout_behavior="@string/appbar_scrolling_view_behavior"
的属性,当然属性值也可以是其它的。进一步可以发现@string/appbar_scrolling_view_behavior的值是
android.support.design.widget.AppBarLayout$ScrollingViewBehavior
,不就是support包下一个类的路径嘛!玄机就在这里,通过CoordinatorLayout之所以可以实现炫酷的交互效果,Behavior功不可没。既然如此,我们也可以自定义Behavior,来定制我们想要的效果。
要自定义Behavior,首先认识下它:
public static abstract class Behavior<V extends View> {
public Behavior() {
}
public Behavior(Context context, AttributeSet attrs) {
}
//省略了若干方法
}
其中有一个泛型,它的作用是指定要使用这个Behavior的View的类型,可以是Button、TextView等等。如果希望所有的View都可以使用则指定泛型为View即可。
自定义Behavior可以选择重写以下的几个方法有:
- onInterceptTouchEvent():是否拦截触摸事件
- onTouchEvent():处理触摸事件
- layoutDependsOn():确定使用Behavior的View要依赖的View的类型
- onDependentViewChanged():当被依赖的View状态改变时回调
- onDependentViewRemoved():当被依赖的View移除时回调
- onMeasureChild():测量使用Behavior的View尺寸
- onLayoutChild():确定使用Behavior的View位置
- onStartNestedScroll():嵌套滑动开始(ACTION_DOWN),确定Behavior是否要监听此次事件
- onStopNestedScroll():嵌套滑动结束(ACTION_UP或ACTION_CANCEL)
- onNestedScroll():嵌套滑动进行中,要监听的子 View的滑动事件已经被消费
- onNestedPreScroll():嵌套滑动进行中,要监听的子
View将要滑动,滑动事件即将被消费(但最终被谁消费,可以通过代码控制) - onNestedFling():要监听的子 View在快速滑动中
- onNestedPreFling():要监听的子View即将快速滑动
三、实践
通常自定义Behavior分为两种情况:
- 某个View依赖另一个View,监听其位置、尺寸等状态的变化。
- 某个View监听CoordinatorLayout内实现了NestedScrollingChild接口的子View的滑动状态变化(也是一种依赖关系)。
先看第一种情况,我们要实现的效果如下:
向上滑动列表时,title(TextView)自动下滑,当title全部显示时,列表顶部和title底部恰好重合,继续上滑列表时title固定;下滑列表时,当列表顶部和title底部重合时,title开始自动上滑直到完全隐藏。
首先我们定义一个SampleTitleBehavior:
public class SampleTitleBehavior extends CoordinatorLayout.Behavior<View> {
// 列表顶部和title底部重合时,列表的滑动距离。
private float deltaY;
public SampleTitleBehavior() {
}
public SampleTitleBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof RecyclerView;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
if (deltaY == 0) {
deltaY = dependency.getY() - child.getHeight();
}
float dy = dependency.getY() - child.getHeight();
dy = dy < 0 ? 0 : dy;
float y = -(dy / deltaY) * child.getHeight();
child.setTranslationY(y);
return true;
}
}
注意不要忘了重写两个参数的构造函数,否则无法在xml文件中使用该Behavior,我们重写了两个方法:
- layoutDependsOn():使用该Behavior的View要监听哪个类型的View的状态变化。其中参数parant代表CoordinatorLayout,child代表使用该Behavior的View,dependency代表要监听的View。这里要监听RecyclerView。
- onDependentViewChanged():当被监听的View状态变化时会调用该方法,参数和上一个方法一致。所以我们重写该方法,当RecyclerView的位置变化时,进而改变title的位置。
一般情况这两个方法是一组,这样一个简单的Behavior就完成了,使用也很简单,仿照系统的用法,先在strings.xml中记录其全包名路径(当然不是必须的,下一遍会讲到):
<string name="behavior_sample_title">com.othershe.behaviortest.test1.SampleTitleBehavior</string>
然后是布局文件
<?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"
tools:context="com.othershe.behaviortest.test1.TestActivity1">
<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:elevation="0dp">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="#00ffffff"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@mipmap/bg"
android:fitsSystemWindows="true"
android:scaleType="fitXY"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.7" />
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/my_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#ff0000"
android:gravity="center"
android:text="Hello World"
android:textColor="#ffffff"
android:textSize="18sp"
app:layout_behavior="@string/behavior_sample_title" />
</android.support.design.widget.CoordinatorLayout>
。
我们给TextView设置了该Behavior。
除了实现title的位置变化,要实现透明度变化也是很简单的,对SampleTitleBehavior做如下修改即可:
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
if (deltaY == 0) {
deltaY = dependency.getY() - child.getHeight();
}
float dy = dependency.getY() - child.getHeight();
dy = dy < 0 ? 0 : dy;
float alpha = 1 - (dy / deltaY);
child.setAlpha(alpha);
return true;
}
修改后的效果如下:
第二种情况,我们的目标效果如下:
简单解释一下,该布局由RecylerView列表和一个TextView组成,其中RecylerView实现了NestedScrollingChild接口,所以TextView监听RecylerView的滑动状态。开始向上滑动列表时TextView和列表整体上移,直到TextView全部隐藏停止,再次上滑则列表内容上移。之后连续下滑列表当其第一个item全部显示时列表滑动停止,再次下滑列表时TextView跟随列表整体下移,直到TextView全部显示。(有点绕,上手体会下…)
这里涉及两个自定义Behavior,第一个实现垂直方向滑动列表时,TextView上移或下移的功能,但此时TextView会覆盖在RecyclerView上(其实CoordinatorLayout有种FrameLayout的即视感),所以第二个的作用就是解决这个问题,实现RecyclerView固定在TextView下边并跟随TextView移动,可以发现这两个View是相互依赖的。
先看第一个Behavior,代码如下:
public class SampleHeaderBehavior extends CoordinatorLayout.Behavior<TextView> {
// 界面整体向上滑动,达到列表可滑动的临界点
private boolean upReach;
// 列表向上滑动后,再向下滑动,达到界面整体可滑动的临界点
private boolean downReach;
// 列表上一个全部可见的item位置
private int lastPosition = -1;
public SampleHeaderBehavior() {
}
public SampleHeaderBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, TextView child, MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downReach = false;
upReach = false;
break;
}
return super.onInterceptTouchEvent(parent, child, ev);
}
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull TextView child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull TextView child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
if (target instanceof RecyclerView) {
RecyclerView list = (RecyclerView) target;
// 列表第一个全部可见Item的位置
int pos = ((LinearLayoutManager) list.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
if (pos == 0 && pos < lastPosition) {
downReach = true;
}
// 整体可以滑动,否则RecyclerView消费滑动事件
if (canScroll(child, dy) && pos == 0) {
float finalY = child.getTranslationY() - dy;
if (finalY < -child.getHeight()) {
finalY = -child.getHeight();
upReach = true;
} else if (finalY > 0) {
finalY = 0;
}
child.setTranslationY(finalY);
// 让CoordinatorLayout消费滑动事件
consumed[1] = dy;
}
lastPosition = pos;
}
}
private boolean canScroll(View child, float scrollY) {
if (scrollY > 0 && child.getTranslationY() == -child.getHeight() && !upReach) {
return false;
}
if (downReach) {
return false;
}
return true;
}
}
这里主要关注这两个重写的方法(这里涉及NestedScrolling机制,下一篇会讲到):
onStartNestedScroll():表示是否监听此次RecylerView的滑动事件,这里我们只监听其垂直方向的滑动事件
onNestedPreScroll():处理监听到的滑动事件,实现整体滑动和列表单独滑动(header是否完全隐藏是滑动的临界点)。
第二个Behavior就简单了,就是第一种情况,当header位置变化时,改变列表y坐标,代码如下:
public class RecyclerViewBehavior extends CoordinatorLayout.Behavior<RecyclerView> {
public RecyclerViewBehavior() {
}
public RecyclerViewBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, RecyclerView child, View dependency) {
return dependency instanceof TextView;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, RecyclerView child, View dependency) {
//计算列表y坐标,最小为0
float y = dependency.getHeight() + dependency.getTranslationY();
if (y < 0) {
y = 0;
}
child.setY(y);
return true;
}
}
将Behavior加到strings.xml中:
<string name="behavior_sample_header">com.othershe.behaviortest.test2.SampleHeaderBehavior</string>
<string name="behavior_recyclerview">com.othershe.behaviortest.test2.RecyclerViewBehavior</string>
在布局文件中的使用:
<?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”
tools:context=“com.othershe.behaviortest.test2.TestActivity2”>
<TextView
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#ff0000"
android:gravity="center"
android:text="Hello World"
android:textColor="#ffffff"
android:textSize="18sp"
app:layout_behavior="@string/behavior_sample_header" />
<android.support.v7.widget.RecyclerView
android:id="@+id/my_list"
android:layout_width="match_parent"
app:layout_behavior="@string/behavior_recyclerview"
android:layout_height="wrap_content" />
</android.support.design.widget.CoordinatorLayout>
自定义Behavior的基本用法就这些了,主要就是确定View之间的依赖关系(也可以理解为监听关系),当被依赖View的状态变化时,相应View的状态进而改变。
掌握了自定义Behavior,可以尝试实现更复杂的交互效果,如下demo(原理参考了自定义Behavior的艺术探索-仿UC浏览器主页),并添加了header滑动手势、列表下滑展开header的操作:
再进一步简化修改,就实现了类似Android版虾米音乐播放页的手势效果:
简单的分析一下最后一个效果,界面由header、title、list三部分组成,初始状态如下
title此时在屏幕顶部外,则其初始y坐标为-titleHeight;header在屏幕顶部,相当于其默认y坐标为0;list在header下边,则其初始y坐标是headerHeight。初始状态上滑header或list则list上移、title下移,同时header向上偏移,最大偏移值headerOffset。
当header达到最大偏移值时title全部显示其底部和list顶部重合,list和title的位移结束,此时title下移距离为titleHeight,其y坐标为0,即y坐标的变化范围从-titleHeight到0;而list的上移距离为headerHeight - titleHeight,此时其y值为titleHeight,y坐标的变化范围从headerHeight到headerHeight - titleHeight(下滑过程也类似,就不分析了)。上滑结束状态如下:
可以发现我们是以header向上偏移是否结束为临界点,来决定list、title是否继续位移,所以可以用header作为被依赖对象,在滑动过程中,计算header的translationY和最大偏移值headerOffset的比例进而计算title和list的y坐标来完成位移,剩下就是编写Behavior了。这里有一点需要注意,list的高度在界面初始化后已经完成测量,上滑时根据header的偏移改变list的y坐标使其移动,会出现list显示不全的问题!
还记得第一个demo吗,也是header+list的形式,但没有这个问题,可以参考一下哦,其布局文件中使用了AppBarLayout和它下边的Behavior名为appbar_scrolling_view_behavior的RecyclerView,其实AppBarLayout也使用了一个Behavior,只不过是通过注解来设置的(后边会讲到),它继承自ViewOffsetBehavior,由于ViewOffsetBehavior是包私有的,我们拷贝一份,让我们header的Behavior也继承ViewOffsetBehavior,上边appbar_scrolling_view_behavior对应的Behavior继承自HeaderScrollingViewBehavior,它同样也是私有的,拷贝一份,让list的Behavior继承自它,这样问题就解决了!这里只是简单的原理分析,代码就不贴了,有兴趣的可以看源码!
NestedScrollView
参考:
https://www.jianshu.com/p/f55abc60a879
简介
NestedScrollView 即 支持嵌套滑动的 ScrollView。
因此,我们可以简单的把 NestedScrollView 类比为 ScrollView,其作用就是作为控件父布局,从而具备(嵌套)滑动功能。
NestedScrollView 与 ScrollView 的区别就在于 NestedScrollView 支持 嵌套滑动,无论是作为父控件还是子控件,嵌套滑动都支持,且默认开启。
因此,在一些需要支持嵌套滑动的情景中,比如一个 ScrollView 内部包裹一个 RecyclerView,那么就会产生滑动冲突,这个问题就需要你自己去解决。而如果使用 NestedScrollView 包裹 RecyclerView,嵌套滑动天然支持,你无需做什么就可以实现前面想要实现的功能了。
举个例子:
我们通常为RecyclerView增加一个 Header 和 Footer 的方法是通过定义不同的 viewType来区分的,而如果使用 NestedScrollView,我们完全可以把RecyclerView当成一个单独的控件,然后在其上面增加一个控件作为 Header,在其下面增加一个控件作为 Footer。
具体布局如下所示:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants"
android:orientation="vertical">
<!-- This is the Header -->
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#888888"
android:gravity="center"
android:text="Header"
android:textColor="#0000FF"
android:textSize="30sp" />
<android.support.v7.widget.RecyclerView
android:id="@+id/rc"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<!-- This is the Footer -->
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#888888"
android:gravity="center"
android:text="Footer"
android:textColor="#FF0000"
android:textSize="30sp" />
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
注: NestedScrollView 与 ScrollView 一样,内部只能容纳一个子控件。
效果如下所示:
ps: 虽然 NestedScrollView 内嵌RecyclerView和其他控件可以实现 Header 和 Footer,但还是不推荐上面这种做法(建议还是直接使用RecyclerView自己添加 Header 和 Footer),因为虽然 NestedScrollView 支持嵌套滑动,但是在实际应用中,嵌套滑动可能会带来其他的一些奇奇怪怪的副作用,Google 也推荐我们能不使用嵌套滑动就尽量不要使用。
如果想知道 NestedScrollView 嵌套其他控件可能带来的问题,可以查看:
NestedScrollView、RecycleView、ViewPager 等布局方面的常见问题汇总,及解决
嵌套滑动机制 简析
我们知道,Android 的事件分发机制中,只要有一个控件消费了事件,其他控件就没办法再接收到这个事件了。因此,当有嵌套滑动场景时,我们都需要自己手动解决事件冲突。而在 Android 5.0 Lollipop 之后,Google 官方通过 嵌套滑动机制 解决了传统 Android 事件分发无法共享事件这个问题。
嵌套滑动机制 的基本原理可以认为是事件共享,即当子控件接收到滑动事件,准备要滑动时,会先通知父控件(startNestedScroll);然后在滑动之前,会先询问父控件是否要滑动(dispatchNestedPreScroll);如果父控件响应该事件进行了滑动,那么就会通知子控件它具体消耗了多少滑动距离;然后交由子控件处理剩余的滑动距离;最后子控件滑动结束后,如果滑动距离还有剩余,就会再问一下父控件是否需要在继续滑动剩下的距离(dispatchNestedScroll)…
上面其实就是 嵌套滑动机制 的工作原理,那么如果想让我们自定义的View或者ViewGroup实现嵌套滑动功能,应该怎样做呢?
其实,在 Android 5.0 之后,系统自带的View和ViewGroup都增加了 嵌套滑动机制 相关的方法了(但是默认不会被调用,因此默认不具备嵌套滑动功能),所以如果在 Android 5.0 及之后的平台上,自定义View只要覆写相应的 嵌套滑动机制 相关方法即可;但是为了提供低版本兼容性,Google 官方还提供了两个接口,分别作为 嵌套滑动机制 父控件接口和子控件接口:
-
NestedScrollingParent:作为父控件,支持嵌套滑动功能
-
NestedScrollingChild:作为子控件,支持嵌套滑动功能。
前面我们说过 NestedScrollView 无论是作为父控件还是子控件都支持嵌套滑动,就是因为它同时实现了 NestedScrollingParent 和 NestedScrollingChild。文档如下所示:
这里看到 NestedScrollView 实现的是NestedScrollingChild2 接口,这是因为在 Android v25.x 以前,嵌套滚动 API 存在缺陷:当用户触发 ACTION_UP 事件时,如果 view 存在的惯性较大(fling 快速划动),它将调用 dispatchNestedPreFling 让 parent 继续消费 velocity。但是如果 parent 返回 false,不进行消费,那么 view 将开始滑动并立即调用 dispatchNestedFling,然后立即调用stopNestedScroll 来将嵌套滚动标记为结束,即使 view 自己实际上还处于滑动(fling) 中。
这里的问题就在于当 ACTION_UP 事件发生后,parent 对当前剩余的滑动不感兴趣,因此滑动事件给到 view,view 则可以进行滑动。这样就存在一种场景,即此时 view 滑动到顶部/底部时,剩余速度还是很大,这里我们正常的思维就是可以把这部分剩余速度给到 parent 去响应,而由于在 ACTION_UP 后,嵌套滑动机制已经结束了,所以这些事件再也无法传递给parent,剩余的速度会被丢失。
为了修复上述这个问题,Google 在支持库的 26.0.0-beta2 版本中,发布了一些对嵌套滚动 API 的改进:
其实新 API 的修复方法就是在现有的方法上添加了一个新的参数 type。
由 type 参数就可以知道当前是什么类型的输入在驱动滑动(scroll)事件,目前有两种选项:
- ViewCompat. TYPE_TOUCH:正常的屏幕触控驱动事件
- ViewCompat. TYPE_NON_TOUCH:非手指触碰手势输入类型,通常是手指离开屏幕后的惯性滑动事件
参照我们上面的场景,在使用新嵌套滑动 API 后,此时的运行情景如下:
- 手指触摸,滑动与之前情景一致,但是这次传入了 TYPE_TOUCH 参数:startNestedScroll(TYPE_TOUCH);
- 手指离开屏幕,触发 ACTION_UP 事件,场景与之前一致,stopNestedScroll(TYPE_TOUCH) 被调用同时
touch 嵌套滚动结束。 - view 开始 fling。此时将开始新的一轮嵌套滚动,不过这次是 TYPE_NON_TOUCH
类型,从startNestedScroll(TYPE_NON_TOUCH),到dispatchNestedPreScroll(TYPE_NON_TOUCH)- dispatchNestedScroll(TYPE_NON_TOUCH), 最后是 stopNestedScroll(TYPE_NON_TOUCH)。这次所有事情都是 view 的 fling (通常是一个
Scroller)驱动的,而不是触摸事件。
- dispatchNestedScroll(TYPE_NON_TOUCH), 最后是 stopNestedScroll(TYPE_NON_TOUCH)。这次所有事情都是 view 的 fling (通常是一个
所以,其实新 API 的修复方法就是在用户手指离开屏幕后,为惯性滑动开启了新的一轮嵌套滑动事件,且该事件由参数 type=TYPE_NON_TOUCH 进行标识。
参考
1. NestedScrollView、RecycleView、ViewPager 等布局方面的常见问题汇总,及解决
2. 详解:Android嵌套滑动机制 (NestedScrolling)
3. [译] 对design库中AppBarLayout嵌套滚动问题的修复
4. Android8.0对于CoordinatorLayout、RecyclerView 精准fling的优化
5. 一点见解: Android嵌套滑动和NestedScrollView
6. 十分钟Android中的嵌套滚动机制
7. Android中NestedScrollview的使用