在列表滚动的时候显示或者隐藏Toolbar(第一部分)
导读:这个系列包含两篇文章,都是关于列表滚动时Toolbar(以及FAB)的显示与隐藏的,但是分为两种一种是Google+中的效果,一种是play store中的效果,本文是第一种。原文翻译如下:
本文将讲解如何实现类似于Google+应用中,当列表滚动的时候,ToolBar(以及悬浮操作按钮)的显示与隐藏(向下滚动隐藏,向上滚动显示),这种效果在Material Design 清单中有提到:
“在合适的地方,当列表向下滚动,app bar可以退出屏幕,以便为内容区域留下更多的空间;而当列表向上滚动回来的时候,app bar又重新显示出来”。
注:这里的向下滚动是指滚动到下面查看更多内容,相对应的手势操作其实是往上。同理向上滚动是指查看前面的内容,而手势其实是向下。
下面是我们应该实现的效果图:
虽然此文我们将使用RecyclerView
作为列表,但是这种实现方式适用于任何可以滚动的容器(某些情况下也许要稍微多做点工作,比如listview)。我想到了两种实现的方式:
-
在列表的上面加个padding。
-
为列表加个header。
我打算只写出第二种实现方式,因为有很多人询问关于如何给RecyclerView
加上header的问题,因此借着这个机会就一起讲了。但是我也会非常简单的描述一下第一种实现方法。
开始
首先添加必要的库
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).
<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的布局
<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
的代码:
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创建布局:``
<?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
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
的代码
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 ``添加和``````` `` `Toolbar相同高度的` ``paddingTo,但是需要注意`````` `````RecyclerView默认clipToPadding是true的,我们需要关掉,关于```` ``` `` `clipToPadding` `` ``` ````,请看这篇文章 [http://jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0317/2613.html](https://img.paonet.com/upload-images-old/a/anzhuokaifa/androidkaifa/2015/0317/2613.html) 。下面是布局代码:````` `````` ```````
<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
:
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。
<View xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"/>
布局很简单,只需注意其高度要和Toolbar一致,它的ViewHolder
也很简单:
public class RecyclerHeaderViewHolder extends RecyclerView.ViewHolder {
public RecyclerHeaderViewHolder(View itemView) {
super(itemView);
}
}
ok,运行结果:
顺眼多了是吧?最后我们来实现滚动时候的显示与隐藏。
只需要再多为RecyclerView
创建一个类OnScrollListener
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未隐藏且下滚的时候,因为我们只关心这两种情况。
if((controlsVisible && dy>0) || (!controlsVisible && dy<0)) {
scrolledDistance += dy;
}
2.如果总的滚动距离超多了一定值(这个值取决于你自己的设定,越大,需要滑动的距离越长才能显示或者隐藏),我们就根据其方向显示或者隐藏Toolbar(dy>0意味着下滚,dy<0意味着上滚)。
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:
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();
}
});
}
动画显示隐藏的代码如下
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是否可见,只有当不可见的时候才执行上面的逻辑。
@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)