先看一下效果:
本文将讲解如何实现类似于Google+应用中,当列表滚动的时候,ToolBar(以及悬浮操作按钮)的显示与隐藏(向下滚动隐藏,向上滚动显示),这种效果在Material Design 清单中有提到:
“在合适的地方,当列表向下滚动,app bar可以退出屏幕,以便为内容区域留下更多的空间;而当列表向上滚动回来的时候,app bar又重新显示出来”。
注:这里的向下滚动是指滚动到下面查看更多内容,相对应的手势操作其实是往上。同理向上滚动是指查看前面的内容,而手势其实是向下。
虽然此文我们将使用RecyclerView
作为列表,但是这种实现方式适用于任何可以滚动的容器(某些情况下也许要稍微多做点工作,比如listview)。我想到了两种实现的方式:
- 在列表的上面加个padding。
- 为列表加个header。
我打算只写出第二种实现方式,因为有很多人询问关于如何给RecyclerView
加上header的问题,因此借着这个机会就一起讲了。但是我也会非常简单的描述一下第一种实现方法。
开始
首先添加必要的库
1 dependencies { 2 compile fileTree(include: [‘*.jar‘], dir: ‘libs‘) 3 androidTestCompile(‘com.android.support.test.espresso:espresso-core:2.2.2‘, { 4 exclude group: ‘com.android.support‘, module: ‘support-annotations‘ 5 }) 6 compile ‘com.android.support:appcompat-v7:25.3.1‘ 7 compile ‘com.android.support.constraint:constraint-layout:1.0.2‘ 8 testCompile ‘junit:junit:4.12‘ 9 compile ‘com.android.support:design:26.0.0-alpha1‘ 10 compile ‘com.android.support:cardview-v7:26.0.0-alpha1‘ 11 compile ‘com.jakewharton:butterknife:8.7.0‘ 12 compile ‘com.jakewharton:butterknife-compiler:8.7.0‘ 13 compile ‘de.greenrobot:eventbus:3.0.0-beta1‘ 14 }
创建activity的布局:
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 xmlns:tools="http://schemas.android.com/tools" 4 android:layout_width="match_parent" 5 android:layout_height="match_parent" 6 android:orientation="vertical" 7 tools:context="recycler.huolongluo.hideonscrollexample.MainActivity"> 8 9 <android.support.v7.widget.Toolbar 10 android:id="@+id/toolbar" 11 android:layout_width="match_parent" 12 android:layout_height="?attr/actionBarSize" 13 android:background="@color/colorPrimaryDark" 14 tools:layout_editor_absoluteX="8dp" 15 tools:layout_editor_absoluteY="0dp" /> 16 17 <android.support.v7.widget.RecyclerView 18 android:id="@+id/rv_content" 19 android:layout_width="match_parent" 20 android:layout_height="match_parent" /> 21 22 </LinearLayout>
下面转向MainActivity
的代码:
1 package recycler.huolongluo.hideonscrollexample; 2 3 import android.os.Bundle; 4 import android.support.v7.app.AppCompatActivity; 5 import android.support.v7.widget.LinearLayoutManager; 6 import android.support.v7.widget.RecyclerView; 7 import android.support.v7.widget.Toolbar; 8 import android.util.Log; 9 import android.view.View; 10 import android.view.animation.AccelerateInterpolator; 11 import android.view.animation.DecelerateInterpolator; 12 13 import java.util.ArrayList; 14 import java.util.List; 15 16 import butterknife.BindView; 17 import butterknife.ButterKnife; 18 19 public class MainActivity extends AppCompatActivity { 20 private static final String TAG = "MainActivity"; 21 22 @BindView(R.id.toolbar) 23 Toolbar toolbar; 24 @BindView(R.id.rv_content) 25 RecyclerView rv_content; 26 27 private List<String> datas; 28 private MyAdapter myAdapter; 29 30 @Override 31 protected void onCreate(Bundle savedInstanceState) { 32 super.onCreate(savedInstanceState); 33 setContentView(R.layout.activity_main); 34 ButterKnife.bind(this); 35 initData(); 36 initView(); 37 } 38 39 private void initView() { 40 myAdapter = new MyAdapter(this, datas); 41 rv_content.setLayoutManager(new LinearLayoutManager(this)); 42 rv_content.setAdapter(myAdapter); 43 44 /** 45 * 实现RecyclerView上下滑动的显示和隐藏 46 * */ 47 rv_content.addOnScrollListener(new RecyclerView.OnScrollListener() { 48 private static final int HIDE_THRESHOLD = 20; 49 private int scrolledDistance = 0; 50 private boolean controlsVisible = true; 51 52 @Override 53 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 54 super.onScrolled(recyclerView, dx, dy); 55 Log.e(TAG, "onScrolled dy: " + dy); 56 Log.e(TAG, "onScrolled dx: " + dx); 57 Log.e(TAG, "-------------------- onScrolled: --------------------"); 58 if (scrolledDistance > HIDE_THRESHOLD && controlsVisible) { 59 // TODO: 2017/7/16 0016 隐藏toolbar 60 hideViews(); 61 // toolbar.setVisibility(View.GONE); 62 controlsVisible = false; 63 scrolledDistance = 0; 64 } else if (scrolledDistance < -HIDE_THRESHOLD && !controlsVisible) { 65 // TODO: 2017/7/16 0016 显示toolbar 66 showViews(); 67 // toolbar.setVisibility(View.VISIBLE); 68 controlsVisible = true; 69 scrolledDistance = 0; 70 } 71 if ((controlsVisible && dy > 0) || (!controlsVisible && dy < 0)) { 72 scrolledDistance += dy; 73 } 74 } 75 }); 76 } 77 78 private void hideViews() { 79 toolbar.animate().translationY(-toolbar.getHeight()).setInterpolator(new AccelerateInterpolator(2)); 80 } 81 82 private void showViews() { 83 toolbar.animate().translationY(0).setInterpolator(new DecelerateInterpolator(2)); 84 } 85 86 private void initData() { 87 if (datas == null) { 88 datas = new ArrayList<>(); 89 } 90 for (int i = 0; i < 20; i++) { 91 datas.add("Item " + i); 92 } 93 } 94 }
如你所见,这是一个很小的类,只实现了onCreate,做了如下几件事情:
1.初始化Toolbar
2.初始化数据源
3.初始化
RecyclerView
现在来看下适配器Adapter的写法,适配器前,我先把需要用到的两个不同布局的item画出来了。一个item作为RecyclerView的头部,一个item作为RecyclerView的内容。
item_head.xml:
1 <?xml version="1.0" encoding="utf-8"?> 2 <View xmlns:android="http://schemas.android.com/apk/res/android" 3 android:id="@+id/view_head" 4 android:layout_width="match_parent" 5 android:layout_height="?attr/actionBarSize" 6 android:orientation="vertical" />
item_content.xml:
1 <?xml version="1.0" encoding="utf-8"?> 2 <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" 3 xmlns:app="http://schemas.android.com/apk/res-auto" 4 android:layout_width="match_parent" 5 android:layout_height="wrap_content" 6 android:layout_gravity="center" 7 android:layout_margin="20dp" 8 android:orientation="vertical" 9 app:cardCornerRadius="10dp"> 10 11 <TextView 12 android:id="@+id/tv_title" 13 android:layout_width="match_parent" 14 android:layout_height="wrap_content" 15 android:padding="20dp" 16 android:text="Item " 17 android:textSize="20sp" /> 18 19 </android.support.v7.widget.CardView>
布局很简单,只需注意其高度要和Toolbar一致。
接下来我们看适配器MyAdapter.java:
1 package recycler.huolongluo.hideonscrollexample; 2 3 import android.content.Context; 4 import android.support.v7.widget.RecyclerView; 5 import android.view.LayoutInflater; 6 import android.view.View; 7 import android.view.ViewGroup; 8 import android.widget.TextView; 9 10 import java.util.List; 11 12 /** 13 * Created by Administrator on 2017/7/16 0016. 14 */ 15 16 class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { 17 18 private Context context; 19 private List<String> datas; 20 private LayoutInflater inflater; 21 22 public MyAdapter(Context context, List<String> datas) { 23 this.context = context; 24 this.datas = datas; 25 inflater = LayoutInflater.from(context); 26 } 27 28 @Override 29 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 30 if (viewType == 0) { 31 return new ViewHolderHead(inflater.inflate(R.layout.item_head, parent, false)); 32 } else { 33 return new ViewHolderContent(inflater.inflate(R.layout.item_content, parent, false)); 34 } 35 } 36 37 @Override 38 public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { 39 if (holder instanceof ViewHolderContent) { 40 ((ViewHolderContent) holder).tv_title.setText(datas.get(position - 1)); 41 } 42 } 43 44 @Override 45 public int getItemCount() { 46 return datas.size() + 1; 47 } 48 49 @Override 50 public int getItemViewType(int position) { 51 if (position == 0) { 52 return 0; 53 } else { 54 return 1; 55 } 56 } 57 58 public class ViewHolderContent extends RecyclerView.ViewHolder { 59 private TextView tv_title; 60 61 public ViewHolderContent(View itemView) { 62 super(itemView); 63 tv_title = (TextView) itemView.findViewById(R.id.tv_title); 64 } 65 } 66 67 public class ViewHolderHead extends RecyclerView.ViewHolder { 68 private View view_head; 69 70 public ViewHolderHead(View itemView) { 71 super(itemView); 72 view_head = (View) itemView.findViewById(R.id.view_head); 73 } 74 } 75 }
到这里就已经全部写完了,
下面是关于上面代码的解释:
1.需要定义Recycler
显示的item的类型。RecyclerView
是一个非常灵活的控件,当某些item的布局和其他item有区别的时候,我们一般要用到item类型。这也正是我们这里需要的-第一个item是header,不同于其他item。
2.我们需要告诉Recycler
,item想要显示的类型。getItemViewType方法将根据position返回一个item的类型(int类型,具体值由你根据项目需求自己定义)。
3.需要修改onCreateViewHolder()
和onBindViewHolder()
方法,在item类型为非0的时候绑定或者返回一个普通item,在item类型为0
的时候返回或绑定一个header item。
4.需要修改getItemCount()
-在原有的基数上+1因为多了个header
正如你所看到的,所有关键代码都在一个onScrolled
()方法中。其dx, dy参数分别是横向和纵向的滚动距离,准确是的是两个滚动事件之间的偏移量,而不是总的滚动距离。
基本的思路如下:
1.计算出滚动的总距离(deltas相加),但是只在Toolbar隐藏且上滚或者Toolbar未隐藏且下滚的时候,因为我们只关心这两种情况。
1 if((controlsVisible && dy>0) || (!controlsVisible && dy<0)) { 2 scrolledDistance += dy; 3 }
2.如果总的滚动距离超多了一定值(这个值取决于你自己的设定,越大,需要滑动的距离越长才能显示或者隐藏),我们就根据其方向显示或者隐藏Toolbar(dy>0意味着下滚,dy<0意味着上滚)。
1 if (scrolledDistance > HIDE_THRESHOLD && controlsVisible) { 2 onHide(); 3 controlsVisible = false; 4 scrolledDistance = 0; 5 } else if (scrolledDistance < -HIDE_THRESHOLD && !controlsVisible) { 6 onShow(); 7 controlsVisible = true; 8 scrolledDistance = 0; 9 }
最后,toolbar的显示和隐藏是通过动画来控制的,一句搞定。
基本上是正确的,但是还有点bug-如果你的滑动距离的触发值太小,在隐藏Toolbar
的时候会在列表的顶部留下一段空白区域(最开始,随着滚动空白区域会消失),幸好解决起来也很简单。只需检测第一个item是否可见,只有当不可见的时候才执行上面的逻辑。
于是,在重写的addOnScrollListener方法里面,最新的代码是这样的:
1 /** 2 * 实现RecyclerView上下滑动的显示和隐藏 3 * */ 4 rv_content.addOnScrollListener(new RecyclerView.OnScrollListener() { 5 private static final int HIDE_THRESHOLD = 20; 6 private int scrolledDistance = 0; 7 private boolean controlsVisible = true; 8 9 int firstVisibleItem = ((LinearLayoutManager) rv_content.getLayoutManager()).findFirstVisibleItemPosition(); 10 11 @Override 12 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 13 super.onScrolled(recyclerView, dx, dy); 14 Log.e(TAG, "onScrolled dy: " + dy); 15 Log.e(TAG, "onScrolled dx: " + dx); 16 Log.e(TAG, "-------------------- onScrolled: --------------------"); 17 if (firstVisibleItem == 0) { 18 if (!controlsVisible) { 19 showViews(); 20 controlsVisible = true; 21 } 22 } else { 23 if (scrolledDistance > HIDE_THRESHOLD && controlsVisible) { 24 // TODO: 2017/7/16 0016 隐藏toolbar 25 hideViews(); 26 // toolbar.setVisibility(View.GONE); 27 controlsVisible = false; 28 scrolledDistance = 0; 29 } else if (scrolledDistance < -HIDE_THRESHOLD && !controlsVisible) { 30 // TODO: 2017/7/16 0016 显示toolbar 31 showViews(); 32 // toolbar.setVisibility(View.VISIBLE); 33 controlsVisible = true; 34 scrolledDistance = 0; 35 } 36 } 37 if ((controlsVisible && dy > 0) || (!controlsVisible && dy < 0)) { 38 scrolledDistance += dy; 39 } 40 } 41 });