本文将讲解如何实现类似于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"
xmlns:card_view=" http://schemas.android.com/apk/res-auto"
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的缘故,上面提到了两种解决办法,一种是为RecyclerView添加和Toolbar相同高度的paddingTo,但是需要注意RecyclerView默认clipToPadding是true的,我们需要关掉,关于clipToPadding,请看这篇文章http://jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0317/2613.html 。下面是布局代码:
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"/>
|
这种实现方法没有问题,但是上面也说了,我们将使用第二种方法-可能还要复杂些(读者还是采用第一种吧,不过对于给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;
}
}
|
再次运行