Material Design(三)(控件)

在这里插入图片描述

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有收缩和扩展的效果, 例如:
在这里插入图片描述

要实现这样的效果, 需要用到:

CoordinatorLayoutAppbarLayout的配合, 以及实现了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继承自它,这样问题就解决了!这里只是简单的原理分析,代码就不贴了,有兴趣的可以看源码!

相关Demo

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)驱动的,而不是触摸事件。

所以,其实新 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的使用

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值