在列表滚动的时候显示或者隐藏Toolbar(第二部分)

泡在网上的日子 / 文 发表于2015-03-19 01:13 第次阅读 Toolbar

这是系列文章的第二部分(也是最后一部分),建议你先阅读 第一部分 ,在上一部分中,我们学会了如何实现Google+应用中隐藏Toolbar的效果,今天我们来实现Play Store中的效果。

在开始之前,我先讲讲这一部分对 项目? 结构的一点改动。原有的activity被分割成了两个:PartOneActivity和PartTwoActivity,他们都是被MainActivity所调用。

下面是本篇文章要实现的Toolbar效果与Play Store的对比:

goal.gif ?

译者注:在阅读本文的同时,最好先实际操作一下play store应用,即便你大致知道效果是怎样也建议操作一下,不然下面的计算有点不好理解。其实这些都是很细微的东西,要一眼带过,估计什么也看不出来。

开始

build.gradle文件和第一部分是一样的,不再赘述,我们从创建Activity的布局开始:


?
????
????
?

只有一个RecyclerView和一个Toolbar(后面我们还会添加Tabs)。注意这次我们使用的是 上篇文章? 中提到的第二种方法(添加padding到RecyclerView)。list item的布局文件和上次一样,直接跳过,RecyclerAdapter同样如此(这里 ? 是其代码-一个不带header的adapter),跳过 ,我们直接进入PartTwoActivity的代码:

public?class?PartTwoActivity?extends?ActionBarActivity?{
?
????private?Toolbar?mToolbar;
?????
????@Override
????protected?void?onCreate(Bundle?savedInstanceState)?{
????????setTheme(R.style.AppThemeGreen);
????????super.onCreate(savedInstanceState);
????????setContentView(R.layout.activity_part_two);
?????????
????????initToolbar();
????????initRecyclerView();
????}
?????
????private?void?initToolbar()?{
????????mToolbar?=?(Toolbar)?findViewById(R.id.toolbar);
????????setSupportActionBar(mToolbar);
????????setTitle(getString(R.string.app_name));
????????mToolbar.setTitleTextColor(getResources().getColor(android.R.color.white));
????}
?????
????private?void?initRecyclerView()?{
????????final?RecyclerView?recyclerView?=?(RecyclerView)?findViewById(R.id.recyclerView);
????????recyclerView.setLayoutManager(new?LinearLayoutManager(this));
????????RecyclerAdapter?recyclerAdapter?=?new?RecyclerAdapter(createItemList());
????????recyclerView.setAdapter(recyclerAdapter);
????????recyclerView.setOnScrollListener(new?HidingScrollListener(this));
????}
?????
????private?List?createItemList()?{
????????List?itemList?=?new?ArrayList<>();
????????for(int?i=0;i<20;i++)?{
????????????itemList.add("Item?"+i);
????????}
????????return?itemList;
????}
}

只是RecyclerViewToolbar基本的初始化操作,注意第27行OnScrollListener的设置。

最有趣的部分是HidingScrollListener,让我们创建一个。

public?abstract?class?HidingScrollListener?extends?RecyclerView.OnScrollListener?{
?
????private?int?mToolbarOffset?=?0;
????private?int?mToolbarHeight;
?????
????public?HidingScrollListener(Context?context)?{
????????mToolbarHeight?=?Utils.getToolbarHeight(context);
????}
?????
????@Override
????public?void?onScrolled(RecyclerView?recyclerView,?int?dx,?int?dy)?{
????????super.onScrolled(recyclerView,?dx,?dy);
?????????
????????clipToolbarOffset();
????????onMoved(mToolbarOffset);
?????????
????????if((mToolbarOffset?0)?||?(mToolbarOffset?>0?&&?dy<0))?{
????????????mToolbarOffset?+=?dy;
????????}
????}
?????
????private?void?clipToolbarOffset()?{
????????if(mToolbarOffset?>?mToolbarHeight)?{
????????????mToolbarOffset?=?mToolbarHeight;
????????}?else?if(mToolbarOffset?

如果你读了前面一篇文章,这段代码应该很眼熟(实际上这次还更简单了)。这里只有一个比较重要的变量- mToolbarOffset,它表示相对于Toolbar高度的滚动偏移量。为了简便起见,我们只追踪0到Toolbar高度之间的值:

if((mToolbarOffset?0)?||?(mToolbarOffset?>0?&&?dy<0))?{
????mToolbarOffset?+=?dy;
}

当向上滚动的时候(注意在第一篇文章中我们对于向上滚动的解释)这个值将增加(但是我们并不希望这个值大于Toolbar的高度),而向下滚动的时候这个值将减小(同样,我们也不希望减小到小于0),你很快会知道为什么我们要作此限制的原因。虽然上面的代码已经有了限制,但是在很短的时间内(比如fling的时候),还是有可能超过这个区间,因此需要调用clipToolbarOffset()方法来做二次限制,确保mToolbarOffset在0到Toolbar高度的范围内,否则会出现抖动闪烁的情况。我们还定义了一个在scroll期间被调用的抽象方法onMoved()

让我们回到PartTwoActivity,同时实现scroll listener中的onMoved()方法:

private?void?initRecyclerView()?{
????final?RecyclerView?recyclerView?=?(RecyclerView)?findViewById(R.id.recyclerView);
????recyclerView.setLayoutManager(new?LinearLayoutManager(this));
????RecyclerAdapter?recyclerAdapter?=?new?RecyclerAdapter(createItemList());
????recyclerView.setAdapter(recyclerAdapter);
?????
????recyclerView.setOnScrollListener(new?HidingScrollListener(this)?{
????????@Override
????????public?void?onMoved(int?distance)?{
????????????mToolbarContainer.setTranslationY(-distance);
????????}
????});
}

好了,我们看看现在是什么效果:

nosnap.gif


非常不错,Toolbar随着列表的滚动而滚动,并且能在消失之后再次随着反向的滚动而滚回来,这和我们的预期是一致的。这要归功于我们对mToolbarOffset的限制。如果我们省略检查mToolbarOffset是否大于0且小于mToolbarHeight,那么当我们向上滚动(这里指手指向上,也许是作者疏忽吧,前后的意思不一致)时,Toolbar将会远远超出屏幕的范围,想再次看到Toolbar需要等列表滚回到0的位置才行。而现在最多才滚动mToolbarHeight的距离,因此Toolbar始终紧挨着列表的最上面,因此向下滚动(这里也是指手指向下)的时候,能立即看到Toolbar

虽然目前看起来还不错,但并非我想要的。如果在滚动一半的时候突然停止,Toolbar将是部分可见的,这看起来很奇怪。实际上Google Play Games就是这种效果,但我觉得这是个bug。


让Toolbar自动滚动到正确位置

正确是效果是Toolbar应该像Google Play store中那样自动平滑的过度到该有的位置。


下面来看看新的HidingScrollListener:

public?abstract?class?HidingScrollListener?extends?RecyclerView.OnScrollListener?{
?????
????private?static?final?float?HIDE_THRESHOLD?=?10;
????private?static?final?float?SHOW_THRESHOLD?=?70;
?????
????private?int?mToolbarOffset?=?0;
????private?boolean?mControlsVisible?=?true;
????private?int?mToolbarHeight;
?????
????public?HidingScrollListener(Context?context)?{
????????mToolbarHeight?=?Utils.getToolbarHeight(context);
????}
?????
????@Override
????public?void?onScrollStateChanged(RecyclerView?recyclerView,?int?newState)?{
????????super.onScrollStateChanged(recyclerView,?newState);
?????????
????????if(newState?==?RecyclerView.SCROLL_STATE_IDLE)?{
????????????if?(mControlsVisible)?{
????????????????if?(mToolbarOffset?>?HIDE_THRESHOLD)?{
????????????????????setInvisible();
????????????????}?else?{
????????????????????setVisible();
????????????????}
????????????}?else?{
????????????????if?((mToolbarHeight?-?mToolbarOffset)?>?SHOW_THRESHOLD)?{
????????????????????setVisible();
????????????????}?else?{
????????????????????setInvisible();
????????????????}
????????????}
????????}
????}
?????
????@Override
????public?void?onScrolled(RecyclerView?recyclerView,?int?dx,?int?dy)?{
????????super.onScrolled(recyclerView,?dx,?dy);
?????????
????????clipToolbarOffset();
????????onMoved(mToolbarOffset);
?????????
????????if((mToolbarOffset?0)?||?(mToolbarOffset?>0?&&?dy<0))?{
????????????mToolbarOffset?+=?dy;
????????}
?????????
????}
?????
????private?void?clipToolbarOffset()?{
????????if(mToolbarOffset?>?mToolbarHeight)?{
????????????mToolbarOffset?=?mToolbarHeight;
????????}?else?if(mToolbarOffset??0)?{
????????????onShow();
????????????mToolbarOffset?=?0;
????????}
????????mControlsVisible?=?true;
????}
?????
????private?void?setInvisible()?{
????????if(mToolbarOffset?

比以前复杂了点,但是也不用怕,我们只是重写了RecyclerView.OnScrollListener的第二个方法onScrollStateChanged(),下面是onScrollStateChanged中所做的事情:

1.检查列表是否处于RecyclerView.SCROLL_STATE_IDLE状态,这个状态下列表没有滚动(因为如果在滚动,我们是像以前一样主动移动Toolbar的Y值)。

2.如果我们放开了手指并且列表停止滚动(这是就是RecyclerView.SCROLL_STATE_IDLE状态),我们需要检查当前Toolbar是否可见,如果是可见的,意味着在mToolbarOffset大于HIDE_THRESHOLD的时候隐藏它,而在mToolbarOffset小于SHOW_THRESHOLD的时候显示它。

if?(mControlsVisible)?{
????if?(mToolbarOffset?>?HIDE_THRESHOLD)?{
????????setInvisible();
????}?else?{
????????setVisible();
????}
}

如果Toolbar是不可见的,我们要做相反的事情-当mToolbarOffset(现在是从顶部计算所以是mToolbarHeight - mToolbarOffset)大于SHOW_THRESHOLD显示,当小于IDE_THRESHOLD再次隐藏:

else?{?//?it's?not?visible
????if?((mToolbarHeight?-?mToolbarOffset)?>?SHOW_THRESHOLD)?{
????????setVisible();
????}?else?{
????????setInvisible();
????}
}

ps:实话是说我没看懂。。。

onScrolled()方法和之前保持一致,最后剩下的事情是在PartTwoActivity中实现两个新的抽象方法:

private?void?initRecyclerView()?{
????final?RecyclerView?recyclerView?=?(RecyclerView)?findViewById(R.id.recyclerView);
????recyclerView.setLayoutManager(new?LinearLayoutManager(this));
????RecyclerAdapter?recyclerAdapter?=?new?RecyclerAdapter(createItemList());
????recyclerView.setAdapter(recyclerAdapter);
?????
????recyclerView.setOnScrollListener(new?HidingScrollListener(this)?{
?????
????????@Override
????????public?void?onMoved(int?distance)?{
????????????mToolbarContainer.setTranslationY(-distance);
????????}
?????????
????????@Override
????????public?void?onShow()?{
????????????mToolbarContainer.animate().translationY(0).setInterpolator(new?DecelerateInterpolator(2)).start();
????????}
?????????
????????@Override
????????public?void?onHide()?{
????????????mToolbarContainer.animate().translationY(-mToolbarHeight).setInterpolator(new?AccelerateInterpolator(2)).start();
????????}
?????????
????});
}


现在来看看编译运行的结果:

Snap no tabs gif

看起来还比较顺利!

等等,不是说要做成play store的效果吗,还差了个tab吧,你可能会觉得添加tab会让事情变得复杂很多,让我来告诉你。其实不是那么回事。

添加tab

需要修改Activity的布局:


?
????
?
????
?????
????????
?????
????????
????
?

其中tabs.xml代码如下:


????
????????
????????
????
????

可以看到,我并没有添加一个真正意义上的Tab,而是一个长得像tab的布局。但这并不会改变什么,这里可以是任意的view,原理都是一样的,至于材料设计风格的tab,github上有一些实现,你可以用来替换。

添加Tab意味着列表再次被挡住一部分空间,因此需要增加padding的值。考虑到灵活性,这次我们不再使用xml来设置padding,而是在代码中设置:

private?void?initRecyclerView()?{
????final?RecyclerView?recyclerView?=?(RecyclerView)?findViewById(R.id.recyclerView);
?????
????int?paddingTop?=?Utils.getToolbarHeight(PartTwoActivity.this)?+?Utils.getTabsHeight(PartTwoActivity.this);
????recyclerView.setPadding(recyclerView.getPaddingLeft(),?paddingTop,?recyclerView.getPaddingRight(),?recyclerView.getPaddingBottom());
?????
????recyclerView.setLayoutManager(new?LinearLayoutManager(this));
????//?...
}

很简单,我们将padding设置成ToolbarTab高度之和,运行看看正确与否:

Screen with tabs

看来是正确的列表的第一个item可以完全显示,那么我们继续,实际上,HidingScrollListener类中的代码完全不变,只需呀变更下PartTwoActivity

public?class?PartTwoActivity?extends?ActionBarActivity?{
?????
????private?LinearLayout?mToolbarContainer;
????private?int?mToolbarHeight;
?????
????@Override
????protected?void?onCreate(Bundle?savedInstanceState)?{
????????setTheme(R.style.AppThemeGreen);
????????super.onCreate(savedInstanceState);
????????setContentView(R.layout.activity_part_two);
?????????
????????mToolbarContainer?=?(LinearLayout)?findViewById(R.id.toolbarContainer);
????????initToolbar();
????????initRecyclerView();
????}
?????
????private?void?initToolbar()?{
????????Toolbar?mToolbar?=?(Toolbar)?findViewById(R.id.toolbar);
????????setSupportActionBar(mToolbar);
????????setTitle(getString(R.string.app_name));
????????mToolbar.setTitleTextColor(getResources().getColor(android.R.color.white));
????????mToolbarHeight?=?Utils.getToolbarHeight(this);
????}
?????
????private?void?initRecyclerView()?{
????????final?RecyclerView?recyclerView?=?(RecyclerView)?findViewById(R.id.recyclerView);
?????????
????????int?paddingTop?=?Utils.getToolbarHeight(PartTwoActivity.this)?+?Utils.getTabsHeight(PartTwoActivity.this);
????????recyclerView.setPadding(recyclerView.getPaddingLeft(),?paddingTop,?recyclerView.getPaddingRight(),?recyclerView.getPaddingBottom());
?????????
????????recyclerView.setLayoutManager(new?LinearLayoutManager(this));
????????RecyclerAdapter?recyclerAdapter?=?new?RecyclerAdapter(createItemList());
????????recyclerView.setAdapter(recyclerAdapter);
?????????
????????recyclerView.setOnScrollListener(new?HidingScrollListener(this)?{
?????????
????????????@Override
????????????public?void?onMoved(int?distance)?{
????????????????mToolbarContainer.setTranslationY(-distance);
????????????}
?????????????
????????????@Override
????????????public?void?onShow()?{
????????????????mToolbarContainer.animate().translationY(0).setInterpolator(new?DecelerateInterpolator(2)).start();
????????????}
?????????????
????????????@Override
????????????public?void?onHide()?{
????????????????mToolbarContainer.animate().translationY(-mToolbarHeight).setInterpolator(new?AccelerateInterpolator(2)).start();
????????????}
?????????
????????});
????}
?????
????private?List?createItemList()?{
????????List?itemList?=?new?ArrayList<>();
????????for(int?i=0;i<20;i++)?{
????????????itemList.add("Item?"+i);
????????}
????????return?itemList;
????}
}

不同之处在于,我们将Toolbar和Tab视为一个整体,被包含在LinearLayout中,mToolbarContainer变量即这个mToolbarContainer的引用,在onMove(), onHide()和onShow()方法中,我们动画变换的不是Toolbar而是mToolbarContainer。这将让整个LinearLayout中的内容一起动。

如果运行你会发现似乎很完美,但仔细观察可以发现一个bug,有时候Toolbar和Tab之间会有一条白线,这很可能是因为两者在动画的时候不同步造成的。幸好修改起来也很简单,在Toolbar和Tab的父布局上添加一个同种颜色的背景:


?
????
?
?????
?????
?????????
?????????
????????
????
?

现在即使动画不同步的问题仍然没有解决,但是你也看不到这条白线。还有一个bug,这个bug在第一部分也存在,如果我们列表在顶部,我们向上滑动一点点,当HIDE_THRESHOLD足够小的时候,Toolbar隐藏的同时列表的顶部还会有一个空白的区域,这个bug也好修复:

public?abstract?class?HidingScrollListener?extends?RecyclerView.OnScrollListener?{
?????
????private?static?final?float?HIDE_THRESHOLD?=?10;
????private?static?final?float?SHOW_THRESHOLD?=?70;
?????
????private?int?mToolbarOffset?=?0;
????private?boolean?mControlsVisible?=?true;
????private?int?mToolbarHeight;
????private?int?mTotalScrolledDistance;
?????
????public?HidingScrollListener(Context?context)?{
????????mToolbarHeight?=?Utils.getToolbarHeight(context);
????}
?????
????@Override
????public?void?onScrollStateChanged(RecyclerView?recyclerView,?int?newState)?{
????????super.onScrollStateChanged(recyclerView,?newState);
?????????
????????if(newState?==?RecyclerView.SCROLL_STATE_IDLE)?{
????????????if(mTotalScrolledDistance??HIDE_THRESHOLD)?{
????????????????????????setInvisible();
????????????????????}?else?{
????????????????????????setVisible();
????????????????????}
????????????????}?else?{
????????????????????if?((mToolbarHeight?-?mToolbarOffset)?>?SHOW_THRESHOLD)?{
????????????????????????setVisible();
????????????????????}?else?{
????????????????????????setInvisible();
????????????????????}
????????????????}
????????????}
????????}
????}
????@Override
????public?void?onScrolled(RecyclerView?recyclerView,?int?dx,?int?dy)?{
????????super.onScrolled(recyclerView,?dx,?dy);
?????????
????????//...
?????????
????????mTotalScrolledDistance?+=?dy;
????}
????//...
}


只需添加一个代表中滚动距离的变量mTotalScrolledDistance,当它小于Toolbar高度的时候,Toolbar总是显示。

再次运行:

Working example gif

运行结果非常好,即使用其他的LayoutManager,不改变任何代码也能达到同样的效果:

private?void?initRecyclerView()?{
????final?RecyclerView?recyclerView?=?(RecyclerView)?findViewById(R.id.recyclerView);
?????
????int?paddingTop?=?Utils.getToolbarHeight(PartTwoActivity.this)?+?Utils.getTabsHeight(PartTwoActivity.this);
????recyclerView.setPadding(recyclerView.getPaddingLeft(),?paddingTop,?recyclerView.getPaddingRight(),?recyclerView.getPaddingBottom());
?????
????recyclerView.setLayoutManager(new?GridLayoutManager(this,?3));
????RecyclerAdapter?recyclerAdapter?=?new?RecyclerAdapter(createItemList());
????//...
}

Grid layout

这个系列就算结束了,希望你能学到一些东西。我仍然会继续写文章,不过还没想好下一篇文章写什么。

另外需要指出,本文和上一篇文章中所使用的方法表现虽然还让人满意,但是为经过充分的测试,所以我不确定是否可以直接应用到实际项目之中。我写这两篇文章的目的是为了证明使用标准的api也能实现这些效果。

ps:这算是比较标准的实现了,如果我来写也许也是 80% 的近似。

代码

?GitHub repo.

?英文原文 How to hide/show Toolbar when list is scrolling (part 2)?

上一篇:android:clipToPadding和android:clipChildren
假设我们要做一个效果,界面最顶部是一个ActionBar并且是半透明的,ActionBar下面是一个ListView,在初始状态下,ListView是top是在ActionBar的bottom位置的,但当ListView滚动的时候可以透过ActionBar看到下面的ListView的内容。如下面两张图所示: 正常态:
下一篇:Binder机制1---Binder原理介绍
1. Binder通信机制介绍 这篇文章会先对比Binder机制与Linux的通信机制的差别,了解为什么Android会另起炉灶,采用Binder。接着,会根据 Binder的机制,去理解什么是Service Manager,在C/S模型中扮演什么角色。最后,会从一次完整的通信活动中,去理解Binder