转自:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0319/2618.html
编辑推荐:稀土掘金,这是一个针对技术开发者的一个应用,你可以在掘金上获取最新最优质的技术干货,不仅仅是Android知识、前端、后端以至于产品和设计都有涉猎,想成为全栈工程师的朋友不要错过!
这是系列文章点击打开链接的第二部分(也是最后一部分),建议你先阅读 第一部分 ,在上一部分中,我们学会了如何实现Google+应用中隐藏Toolbar的效果,今天我们来实现Play Store中的效果。
在开始之前,我先讲讲这一部分对 项目 结构的一点改动。原有的activity被分割成了两个:PartOneActivity
和PartTwoActivity,他们都是被
MainActivity
所调用。
下面是本篇文章要实现的Toolbar效果与Play Store的对比:
译者注:
在阅读本文的同时,最好先实际操作一下play store应用,即便你大致知道效果是怎样也建议操作一下,不然下面的计算有点不好理解。其实这些都是很细微的东西,要一眼带过,估计什么也看不出来。
开始
build.gradle
文件和第一部分是一样的,不再赘述,我们从创建Activity
的布局开始:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <FrameLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:layout_width= "match_parent" android:layout_height= "match_parent" > <android.support.v7.widget.RecyclerView android:id= "@+id/recyclerView" android:layout_width= "match_parent" android:layout_height= "match_parent" android:paddingTop= "?attr/actionBarSize" android:clipToPadding= "false" /> <android.support.v7.widget.Toolbar android:id= "@+id/toolbar" android:layout_width= "match_parent" android:layout_height= "?attr/actionBarSize" android:background= "?attr/colorPrimary" /> </FrameLayout> |
只有一个RecyclerView
和一个Toolbar
(后面我们还会添加Tabs
)。注意这次我们使用的是 上篇文章 中提到的第二种方法(添加padding到RecyclerView)
。list item的布局文件和上次一样,直接跳过,RecyclerAdapter
同样如此(这里 是其代码-一个不带header的adapter),跳过 ,我们直接进入PartTwoActivity
的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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<String> createItemList() { List<String> itemList = new ArrayList<>(); for (int i=0;i<20;i++) { itemList.add( "Item " +i); } return itemList; } } |
只是RecyclerView
和Toolbar
基本的初始化操作,注意第27行OnScrollListener
的设置。
最有趣的部分是HidingScrollListener
,让我们创建一个。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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 <mToolbarHeight && dy>0) || (mToolbarOffset >0 && dy<0)) { mToolbarOffset += dy; } } private void clipToolbarOffset() { if (mToolbarOffset > mToolbarHeight) { mToolbarOffset = mToolbarHeight; } else if (mToolbarOffset < 0) { mToolbarOffset = 0; } } public abstract void onMoved(int distance); } |
如果你读了前面一篇文章,这段代码应该很眼熟(实际上这次还更简单了)。这里只有一个比较重要的变量-mToolbarOffset
,它表示相对于Toolbar高度的
滚动偏移量。为了简便起见,我们只追踪0到Toolbar
高度之间的值:
1 2 3 | if ((mToolbarOffset <mToolbarHeight && dy>0) || (mToolbarOffset >0 && dy<0)) { mToolbarOffset += dy; } |
当向上滚动的时候(注意在第一篇文章中我们对于向上滚动的解释)这个值将增加(但是我们并不希望这个值大于Toolbar
的高度),而向下滚动的时候这个值将减小(同样,我们也不希望减小到小于0),你很快会知道为什么我们要作此限制的原因。虽然上面的代码已经有了限制,
但是在很短的时间内(比如fling的时候),还是有可能超过这个区间,
因此需要调用clipToolbarOffset()方法来做二次限制,确保mToolbarOffset
在0到Toolbar
高度的范围内,否则会出现抖动闪烁的情况。我们还定义了一个在scroll期间被调用的抽象方法onMoved()
。
让我们回到
PartTwoActivity
,同时实现scroll listener中的onMoved()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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); } }); } |
好了,我们看看现在是什么效果:
非常不错,Toolbar
随着列表的滚动而滚动,并且能在消失之后再次随着反向的滚动而滚回来,这和我们的预期是一致的。这要归功于我们对mToolbarOffset
的限制。如果我们省略检查mToolbarOffset
是否大于0且小于mToolbarHeight
,那么当我们向上滚动(这里指手指向上,也许是作者疏忽吧,前后的意思不一致)时,Toolbar
将会远远超出屏幕的范围,想再次看到Toolbar
需要等列表滚回到0的位置才行。而现在最多才滚动mToolbarHeight
的距离,因此Toolbar
始终紧挨着列表的最上面,因此向下滚动(这里也是指手指向下)的时候,能立即看到Toolbar
。
虽然目前看起来还不错,但并非我想要的。如果在滚动一半的时候突然停止,Toolbar
将是部分可见的,这看起来很奇怪。实际上Google Play Games就是这种效果,但我觉得这是个bug。
让Toolbar自动滚动到正确位置
正确是效果是Toolbar
应该像Google Play store中那样自动平滑的过度到该有的位置。
下面来看看新的HidingScrollListener:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | 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 <mToolbarHeight && dy>0) || (mToolbarOffset >0 && dy<0)) { mToolbarOffset += dy; } } private void clipToolbarOffset() { if (mToolbarOffset > mToolbarHeight) { mToolbarOffset = mToolbarHeight; } else if (mToolbarOffset < 0) { mToolbarOffset = 0; } } private void setVisible() { if (mToolbarOffset > 0) { onShow(); mToolbarOffset = 0; } mControlsVisible = true ; } private void setInvisible() { if (mToolbarOffset < mToolbarHeight) { onHide(); mToolbarOffset = mToolbarHeight; } mControlsVisible = false ; } public abstract void onMoved(int distance); public abstract void onShow(); public abstract void onHide(); } |
比以前复杂了点,但是也不用怕,我们只是重写了RecyclerView.OnScrollListener的第二个方法onScrollStateChanged(),下面是onScrollStateChanged中所做的事情:
1.检查列表是否处于RecyclerView.SCROLL_STATE_IDLE
状态,这个状态下列表没有滚动(因为如果在滚动,我们是像以前一样主动移动Toolbar
的Y值)。
2.如果我们放开了手指并且列表停止滚动(这是就是RecyclerView.SCROLL_STATE_IDLE
状态),我们需要检查当前Toolbar
是否可见,如果是可见的,意味着在mToolbarOffset大于
的时候显示它。HIDE_THRESHOLD
的时候隐藏它,而在mToolbarOffset
小于SHOW_THRESHOLD
1 2 3 4 5 6 7 | if (mControlsVisible) { if (mToolbarOffset > HIDE_THRESHOLD) { setInvisible(); } else { setVisible(); } } |
如果Toolbar
是不可见的,我们要做相反的事情-当mToolbarOffset
(现在是从顶部计算所以是mToolbarHeight - mToolbarOffset
)大于SHOW_THRESHOLD
显示,当小于IDE_THRESHOLD
再次隐藏:
1 2 3 4 5 6 7 | else { // it's not visible if ((mToolbarHeight - mToolbarOffset) > SHOW_THRESHOLD) { setVisible(); } else { setInvisible(); } } |
ps:实话是说我没看懂。。。
onScrolled()
方法和之前保持一致,最后剩下的事情是在PartTwoActivity
中实现两个新的抽象方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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(); } }); } |
现在来看看编译运行的结果:
看起来还比较顺利!
等等,不是说要做成play store的效果吗,还差了个tab吧,你可能会觉得添加tab会让事情变得复杂很多,让我来告诉你。其实不是那么回事。
添加tab
需要修改Activity
的布局:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <FrameLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:layout_width= "match_parent" android:layout_height= "match_parent" > <android.support.v7.widget.RecyclerView android:id= "@+id/recyclerView" android:layout_width= "match_parent" android:layout_height= "match_parent" android:clipToPadding= "false" /> <LinearLayout android:id= "@+id/toolbarContainer" android:layout_width= "match_parent" android:layout_height= "wrap_content" android:orientation= "vertical" > <android.support.v7.widget.Toolbar android:id= "@+id/toolbar" android:layout_width= "match_parent" android:layout_height= "?attr/actionBarSize" android:background= "?attr/colorPrimary" /> <include layout= "@layout/tabs" /> </LinearLayout> </FrameLayout> |
其中tabs.xml
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:layout_width= "match_parent" android:layout_height= "@dimen/tabsHeight" android:background= "?attr/colorPrimary" > <FrameLayout android:layout_width= "0dp" android:layout_height= "match_parent" android:layout_weight= "1" > <TextView android:layout_width= "match_parent" android:layout_height= "match_parent" android:text= "@string/tab_1" android:gravity= "center" style= "@style/Base.TextAppearance.AppCompat.Body2" android:textColor= "@android:color/white" android:background= "@android:color/transparent" /> <View android:layout_width= "match_parent" android:layout_height= "6dp" android:layout_gravity= "bottom" android:background= "@android:color/white" /> </FrameLayout> <TextView android:layout_width= "0dp" android:layout_height= "match_parent" android:layout_weight= "1" android:text= "@string/tab_2" android:gravity= "center" style= "@style/Base.TextAppearance.AppCompat.Body2" android:textColor= "@android:color/white" android:background= "@android:color/transparent" /> </LinearLayout> |
可以看到,我并没有添加一个真正意义上的Tab,而是一个长得像tab的布局。但这并不会改变什么,这里可以是任意的view,原理都是一样的,至于材料设计风格的tab,github上有一些实现,你可以用来替换。
添加
Tab意味着列表再次被挡住一部分空间,因此需要增加padding的值。考虑到灵活性,这次我们不再使用xml来设置padding,而是在代码中设置:
1 2 3 4 5 6 7 8 9 | 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设置成Toolbar
和Tab
高度之和,运行看看正确与否:
看来是正确的列表的第一个item可以完全显示,那么我们继续,实际上,HidingScrollListener
类中的代码完全不变,只需呀变更下PartTwoActivity
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | 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<String> createItemList() { List<String> 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的父布局上添加一个同种颜色的背景:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <FrameLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:layout_width= "match_parent" android:layout_height= "match_parent" > <android.support.v7.widget.RecyclerView android:id= "@+id/recyclerView" android:layout_width= "match_parent" android:layout_height= "match_parent" android:clipToPadding= "false" /> <LinearLayout android:id= "@+id/toolbarContainer" android:layout_width= "match_parent" android:layout_height= "wrap_content" android:orientation= "vertical" android:background= "?attr/colorPrimary" > <!-- added here --> <android.support.v7.widget.Toolbar android:id= "@+id/toolbar" android:layout_width= "match_parent" android:layout_height= "?attr/actionBarSize" /> <!-- removed from here and tabs.xml --> <include layout= "@layout/tabs" /> </LinearLayout> </FrameLayout> |
现在即使动画不同步的问题仍然没有解决,但是你也看不到这条白线。还有一个bug,这个bug在第一部分也存在,如果我们列表在顶部,我们向上滑动一点点,当
HIDE_THRESHOLD
足够小的时候,Toolbar
隐藏的同时列表的顶部还会有一个空白的区域,这个bug也好修复:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | 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 < mToolbarHeight) { setVisible(); } else { 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); //... mTotalScrolledDistance += dy; } //... } |
只需添加一个代表中滚动距离的变量mTotalScrolledDistance,当它小于Toolbar高度的时候,Toolbar
总是显示。
再次运行:
运行结果非常好,即使用其他的LayoutManager,不改变任何代码也能达到同样的效果:
1 2 3 4 5 6 7 8 9 10 | 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()); //... } |
这个系列就算结束了,希望你能学到一些东西。我仍然会继续写文章,不过还没想好下一篇文章写什么。
另外需要指出,本文和上一篇文章中所使用的方法表现虽然还让人满意,但是为经过充分的测试,所以我不确定是否可以直接应用到实际项目之中。我写这两篇文章的目的是为了证明使用标准的api也能实现这些效果。
ps:这算是比较标准的实现了,如果我来写也许也是 80% 的近似。
代码
GitHub repo.
英文原文 How to hide/show Toolbar when list is scrolling (part 2)