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

导读:这个系列包含两篇文章,都是关于列表滚动时Toolbar(以及FAB)的显示与隐藏的,但是分为两种一种是Google+中的效果,一种是play store中的效果,本文是第一种。原文翻译如下:


本文将讲解如何实现类似于Google+应用中,当列表滚动的时候,ToolBar(以及悬浮操作按钮)的显示与隐藏(向下滚动隐藏,向上滚动显示),这种效果在Material Design 清单中有提到:


“在合适的地方,当列表向下滚动,app bar可以退出屏幕,以便为内容区域留下更多的空间;而当列表向上滚动回来的时候,app bar又重新显示出来”。

注:这里的向下滚动是指滚动到下面查看更多内容,相对应的手势操作其实是往上。同理向上滚动是指查看前面的内容,而手势其实是向下。

下面是我们应该实现的效果图:

demo_gif.gif

虽然此文我们将使用RecyclerView作为列表,但是这种实现方式适用于任何可以滚动的容器(某些情况下也许要稍微多做点工作,比如listview)。我想到了两种实现的方式:

  1. 在列表的上面加个padding。

  2. 为列表加个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>

包含了RecyclerViewToolbar以及作为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的系列文章 。

代码结构准备就绪,先运行来看看!

Clipped screenshot

很明显列表的最上面有部分内容被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感兴趣可以用第二种)。


为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
     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,运行结果:

Fixed clipping screenshot

顺眼多了是吧?最后我们来实现滚动时候的显示与隐藏。

只需要再多为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不能完全隐藏。

看看效果!

Broken scrolling screenshot

基本上是正确的,但是还有点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;
     }
}

再次运行

Working example screenshot

嗦嘎,貌似很完美了。

这是我第一次发布博客,如果内容显得枯燥或者有误还请原谅。如果你不想使用添加header的方式,可以试试padding的方法。在下篇文章中我将讲解如何使其和Google Play Store的效果一样。

代码

这篇文章的代码在GitHub repo.

 

英文原文: How to hide/show Toolbar when list is scroling (part 1)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值