导读:这个系列包含两篇文章,都是关于列表滚动时Toolbar(以及FAB)的显示与隐藏的,但是分为两种一种是Google+中的效果,一种是play store中的效果,本文是第一种。原文翻译如下:
本文将讲解如何实现类似于Google+应用中,当列表滚动的时候,ToolBar(以及悬浮操作按钮)的显示与隐藏(向下滚动隐藏,向上滚动显示),这种效果在Material Design 清单中有提到:
“在合适的地方,当列表向下滚动,app bar可以退出屏幕,以便为内容区域留下更多的空间;而当列表向上滚动回来的时候,app bar又重新显示出来”。
注:这里的向下滚动是指滚动到下面查看更多内容,相对应的手势操作其实是往上。同理向上滚动是指查看前面的内容,而手势其实是向下。
下面是我们应该实现的效果图:
虽然此文我们将使用RecyclerView
作为列表,但是这种实现方式适用于任何可以滚动的容器(某些情况下也许要稍微多做点工作,比如listview)。我想到了两种实现的方式:
-
在列表的上面加个padding。
-
为列表加个header。
我打算只写出第二种实现方式,因为有很多人询问关于如何给RecyclerView
加上header的问题,因此借着这个机会就一起讲了。但是我也会非常简单的描述一下第一种实现方法。
开始
首先添加必要的库
1
2
3
4
5
6
|
dependencies {
compile fileTree(dir:
'libs'
, include: [
'*.jar'
])
compile
'com.android.support:appcompat-v7:21.0.3'
compile
"com.android.support:recyclerview-v7:21.0.0"
compile
'com.android.support:cardview-v7:21.0.3'
}
|
定义style,使用不带actionbar的Material主题(因为要用Toolbar).
1
2
3
4
|
<style name=
"AppTheme"
parent=
"Theme.AppCompat.Light.NoActionBar"
>
<item name=
"colorPrimary"
>@color/color_primary</item>
<item name=
"colorPrimaryDark"
>@color/color_primary_dark</item>
</style>
|
创建activity的布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<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.support.v7.widget.Toolbar
android:id=
"@+id/toolbar"
android:layout_width=
"match_parent"
android:layout_height=
"?attr/actionBarSize"
android:background=
"?attr/colorPrimary"
/>
<ImageButton
android:id=
"@+id/fabButton"
android:layout_width=
"56dp"
android:layout_height=
"56dp"
android:layout_gravity=
"bottom|right"
android:layout_marginBottom=
"16dp"
android:layout_marginRight=
"16dp"
android:background=
"@drawable/fab_background"
android:src=
"@drawable/ic_favorite_outline_white_24dp"
android:contentDescription=
"@null"
/>
</FrameLayout>
|
包含了RecyclerView
,Toolbar
以及作为FAB(悬浮操作按钮)的ImageButton
。我们将这三个控件放在FrameLayout
中是因为Toolbar
需要上浮在RecyclerView
之上。如果我们不这样做,当Toolbar
隐藏的时候列表的上方会有一个空白的区域。
下面转向MainActivity
的代码:
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
|
public class MainActivity extends ActionBarActivity {
private Toolbar mToolbar;
private ImageButton mFabButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initToolbar();
mFabButton = (ImageButton) findViewById(R.id.fabButton);
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() {
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(
new
LinearLayoutManager(
this
));
RecyclerAdapter recyclerAdapter =
new
RecyclerAdapter(createItemList());
recyclerView.setAdapter(recyclerAdapter);
}
}
|
如你所见,这是一个很小的类,只实现了onCreate,做了如下几件事情:
1.初始化Toolbar
2.获得FAB的引用
3.初始化
RecyclerView
现在来创建
RecyclerView
的adapter,首先需要为item创建布局:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<?xml version=
"1.0"
encoding=
"utf-8"
?>
<android.support.v7.widget.CardView xmlns:android=
"http://schemas.android.com/apk/res/android"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:layout_gravity=
"center"
android:layout_margin=
"8dp"
card_view:cardCornerRadius=
"4dp"
>
<TextView
android:id=
"@+id/itemTextView"
android:layout_width=
"match_parent"
android:layout_height=
"?attr/listPreferredItemHeight"
android:gravity=
"center_vertical"
android:padding=
"8dp"
style=
"@style/Base.TextAppearance.AppCompat.Body2"
/>
</android.support.v7.widget.CardView>
|
以及和布局对应的ViewHolder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class RecyclerItemViewHolder extends RecyclerView.ViewHolder {
private final TextView mItemTextView;
public RecyclerItemViewHolder(final View parent, TextView itemTextView) {
super
(parent);
mItemTextView = itemTextView;
}
public static RecyclerItemViewHolder newInstance(View parent) {
TextView itemTextView = (TextView) parent.findViewById(R.id.itemTextView);
return
new
RecyclerItemViewHolder(parent, itemTextView);
}
public void setItemText(CharSequence text) {
mItemTextView.setText(text);
}
}
|
RecyclerAdapter
的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<String> mItemList;
public RecyclerAdapter(List<String> itemList) {
mItemList = itemList;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent,
false
);
return
RecyclerItemViewHolder.newInstance(view);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
RecyclerItemViewHolder holder = (RecyclerItemViewHolder) viewHolder;
String itemText = mItemList.get(position);
holder.setItemText(itemText);
}
@Override
public int getItemCount() {
return
mItemList ==
null
? 0 : mItemList.size();
}
}
|
这是一个基本的RecyclerView.Adapter
的实现,如果你想了解关于RecyclerView
的更多东西,推荐阅读Mark Allison的系列文章 。
代码结构准备就绪,先运行来看看!
很明显列表的最上面有部分内容被Toolbar挡住了,你应该知道是因为FrameLayout的缘故,上面提到了两种解决办法,一种是为
添加和RecyclerVie
w
paddingTo,但是需要注意Toolbar相同高度的
RecyclerView默认clipToPadding是true的,我们需要关掉,关于
,请看这篇文章 http://jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0317/2613.html 。下面是布局代码:clipToPadding
1
2
3
4
5
6
|
<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"
/>
|
这种实现方法没有问题,但是上面也说了,我们将使用第二种方法-可能还要复杂些(读者还是采用第一种吧,不过对于给
添加header感兴趣可以用第二种)。RecyclerView
为RecyclerView添加header
首先我们需要修改一下Adapter
:
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
|
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
//added view types
private static final int TYPE_HEADER = 2;
private static final int TYPE_ITEM = 1;
private List<String> mItemList;
public RecyclerAdapter(List<String> itemList) {
mItemList = itemList;
}
//modified creating viewholder, so it creates appropriate holder for a given viewType
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Context context = parent.getContext();
if
(viewType == TYPE_ITEM) {
final View view = LayoutInflater.from(context).inflate(R.layout.recycler_item, parent,
false
);
return
RecyclerItemViewHolder.newInstance(view);
}
else
if
(viewType == TYPE_HEADER) {
final View view = LayoutInflater.from(context).inflate(R.layout.recycler_header, parent,
false
);
return
new
RecyclerHeaderViewHolder(view);
}
throw
new
RuntimeException(
"There is no type that matches the type "
+ viewType +
" + make sure your using types correctly"
);
}
//modifed ViewHolder binding so it binds a correct View for the Adapter
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
if
(!isPositionHeader(position)) {
RecyclerItemViewHolder holder = (RecyclerItemViewHolder) viewHolder;
String itemText = mItemList.get(position - 1);
// we are taking header in to account so all of our items are correctly positioned
holder.setItemText(itemText);
}
}
//our old getItemCount()
public int getBasicItemCount() {
return
mItemList ==
null
? 0 : mItemList.size();
}
//our new getItemCount() that includes header View
@Override
public int getItemCount() {
return
getBasicItemCount() + 1;
// header
}
//added a method that returns viewType for a given position
@Override
public int getItemViewType(int position) {
if
(isPositionHeader(position)) {
return
TYPE_HEADER;
}
return
TYPE_ITEM;
}
//added a method to check if given position is a header
private boolean isPositionHeader(int position) {
return
position == 0;
}
}
|
下面是关于上面代码的解释:
1.需要定义Recycler
显示的item的类型。RecyclerView
是一个非常灵活的控件,当某些item的布局和其他item有区别的时候,我们一般要用到item类型。这也正是我们这里需要的-第一个item是header,不同于其他item(代码9-4行)。
2.我们需要告诉Recycler
,item想要显示的类型(49-54行)。getItemViewType方法将根据position返回一个item的类型(int类型,具体值由你自己定义)。
3.需要修改onCreateViewHolder()
和onBindViewHolder()
方法,在item类型为TYPE_ITEM
的时候绑定或者返回一个普通item,在item类型为TYPE_HEADER
的时候返回或绑定一个header item(14-34行)
4.需要修改getItemCount()
-在原有的基数上+1因为多了个header(43-45行)。
现在,我们为header view 创建一个布局和ViewHolder。
1
2
3
|
<View xmlns:android=
"http://schemas.android.com/apk/res/android"
android:layout_width=
"match_parent"
android:layout_height=
"?attr/actionBarSize"
/>
|
布局很简单,只需注意其高度要和Toolbar一致,它的ViewHolder
也很简单:
1
2
3
4
5
|
public class RecyclerHeaderViewHolder extends RecyclerView.ViewHolder {
public RecyclerHeaderViewHolder(View itemView) {
super
(itemView);
}
}
|
ok,运行结果:
顺眼多了是吧?最后我们来实现滚动时候的显示与隐藏。
只需要再多为RecyclerView
创建一个类OnScrollListener
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public abstract class HidingScrollListener extends RecyclerView.OnScrollListener {
private static final int HIDE_THRESHOLD = 20;
private int scrolledDistance = 0;
private boolean controlsVisible =
true
;
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super
.onScrolled(recyclerView, dx, dy);
if
(scrolledDistance > HIDE_THRESHOLD && controlsVisible) {
onHide();
controlsVisible =
false
;
scrolledDistance = 0;
}
else
if
(scrolledDistance < -HIDE_THRESHOLD && !controlsVisible) {
onShow();
controlsVisible =
true
;
scrolledDistance = 0;
}
if
((controlsVisible && dy>0) || (!controlsVisible && dy<0)) {
scrolledDistance += dy;
}
}
public abstract void onHide();
public abstract void onShow();
}
|
正如你所看到的,所有关键代码都在一个onScrolled
()方法中。其dx, dy参数分别是横向和纵向的滚动距离,准确是的是两个滚动事件之间的偏移量,而不是总的滚动距离。
基本的思路如下:
1.计算出滚动的总距离(deltas相加),但是只在Toolbar隐藏且上滚或者Toolbar未隐藏且下滚的时候,因为我们只关心这两种情况。
1
2
3
|
if
((controlsVisible && dy>0) || (!controlsVisible && dy<0)) {
scrolledDistance += dy;
}
|
2.如果总的滚动距离超多了一定值(这个值取决于你自己的设定,越大,需要滑动的距离越长才能显示或者隐藏),我们就根据其方向显示或者隐藏Toolbar(dy>0意味着下滚,dy<0意味着上滚)。
1
2
3
4
5
6
7
8
9
|
if
(scrolledDistance > HIDE_THRESHOLD && controlsVisible) {
onHide();
controlsVisible =
false
;
scrolledDistance = 0;
}
else
if
(scrolledDistance < -HIDE_THRESHOLD && !controlsVisible) {
onShow();
controlsVisible =
true
;
scrolledDistance = 0;
}
|
3.实际显示和隐藏的操作我们并没有定义在scroll listener类中,而是定义了两个抽象方法。
现在我们为RecyclerView
添加listener:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
private void initRecyclerView() {
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(
new
LinearLayoutManager(
this
));
RecyclerAdapter recyclerAdapter =
new
RecyclerAdapter(createItemList());
recyclerView.setAdapter(recyclerAdapter);
//setting up our OnScrollListener
recyclerView.setOnScrollListener(
new
HidingScrollListener() {
@Override
public void onHide() {
hideViews();
}
@Override
public void onShow() {
showViews();
}
});
}
|
动画显示隐藏的代码如下
1
2
3
4
5
6
7
8
9
10
11
|
private void hideViews() {
mToolbar.animate().translationY(-mToolbar.getHeight()).setInterpolator(
new
AccelerateInterpolator(2));
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mFabButton.getLayoutParams();
int fabBottomMargin = lp.bottomMargin;
mFabButton.animate().translationY(mFabButton.getHeight()+fabBottomMargin).setInterpolator(
new
AccelerateInterpolator(2)).start();
}
private void showViews() {
mToolbar.animate().translationY(0).setInterpolator(
new
DecelerateInterpolator(2));
mFabButton.animate().translationY(0).setInterpolator(
new
DecelerateInterpolator(2)).start();
}
|
我们需要将margin也计算进去,不然fab不能完全隐藏。
看看效果!
基本上是正确的,但是还有点bug-如果你的滑动距离的触发值太小,在隐藏Toolbar
的时候会在列表的顶部留下一段空白区域(最开始,随着滚动空白区域会消失),幸好解决起来也很简单。只需检测第一个item是否可见,只有当不可见的时候才执行上面的逻辑。
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
|
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super
.onScrolled(recyclerView, dx, dy);
int firstVisibleItem = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
//show views if first item is first visible position and views are hidden
if
(firstVisibleItem == 0) {
if
(!controlsVisible) {
onShow();
controlsVisible =
true
;
}
}
else
{
if
(scrolledDistance > HIDE_THRESHOLD && controlsVisible) {
onHide();
controlsVisible =
false
;
scrolledDistance = 0;
}
else
if
(scrolledDistance < -HIDE_THRESHOLD && !controlsVisible) {
onShow();
controlsVisible =
true
;
scrolledDistance = 0;
}
}
if
((controlsVisible && dy>0) || (!controlsVisible && dy<0)) {
scrolledDistance += dy;
}
}
|
再次运行
嗦嘎,貌似很完美了。
这是我第一次发布博客,如果内容显得枯燥或者有误还请原谅。如果你不想使用添加header的方式,可以试试padding的方法。在下篇文章中我将讲解如何使其和Google Play Store的效果一样。
代码
这篇文章的代码在GitHub repo.
英文原文: How to hide/show Toolbar when list is scroling (part 1)