前段时间PM提了一个需求,实现一种滑动折叠效果,这种效果与掌上英雄联盟的战绩页面很像。经过调研,发现使用CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout可以轻松实现这一效果。先给大家看一下掌上英雄联盟的效果,大家可以先确定是不是自己想要的效果(PS:这战绩真不是我打的,我发誓。。。)。
布局文件详细代码如下所示,这里说几个注意点:
(1)CoordinatorLayout必须作为最外层的布局。
(2)AppBarLayout作为CoordinatorLayout的直接子布局。
(3)CollapsingToolbarLayout作为AppBarLayout的直接子布局,CollapsingToolbarLayout包裹的内容是可以收缩和伸展的。
(4)CollapsingToolbarLayout下方放了一个TabLayout,也是被AppBarLayout包裹的。
(5)AppBarLayout的下方放一个ViewPager用来和TabLayout联动。
首先,是ViewPager和TabLayout联动的实现,关键代码:mTabLayout.setupWithViewPager(mViewPager)。
fragmentList = new ArrayList<>();
fragment1 = new Fragment1();
fragment2= new Fragment1();
fragment3 = new Fragment1();
fragmentList.add(fragment1);
fragmentList.add(fragment2);
fragmentList.add(fragment3);
mViewPager.setAdapter(new FragmentStatePagerAdapter(getSupportFragmentManager()) {
@Override
public Fragment getItem(int i) {
return fragmentList.get(i);
}
@Override
public int getCount() {
return fragmentList.size();
}
});
mTabLayout.setupWithViewPager(mViewPager);
mTabLayout.getTabAt(0).setText("战绩");
mTabLayout.getTabAt(1).setText("能力");
mTabLayout.getTabAt(2).setText("资产");
或许,有的同学发现了,我已经在xml文件中写死了三个TabItem,为何又手动设置了一次。这里要特别注意,在调用setupWithViewPager后,需要手动的再设置一下TabItem的文本,不然不显示。为什么呢,我们可以看一下setupWithViewPager这个方法。来,一起跟踪一下TabLayout的源码:
private void setupWithViewPager(@Nullable ViewPager viewPager, boolean autoRefresh, boolean implicitSetup) {
if (this.viewPager != null) {
if (this.pageChangeListener != null) {
this.viewPager.removeOnPageChangeListener(this.pageChangeListener);
}
if (this.adapterChangeListener != null) {
this.viewPager.removeOnAdapterChangeListener(this.adapterChangeListener);
}
}
if (this.currentVpSelectedListener != null) {
this.removeOnTabSelectedListener(this.currentVpSelectedListener);
this.currentVpSelectedListener = null;
}
if (viewPager != null) {
this.viewPager = viewPager;
if (this.pageChangeListener == null) {
this.pageChangeListener = new TabLayout.TabLayoutOnPageChangeListener(this);
}
this.pageChangeListener.reset();
viewPager.addOnPageChangeListener(this.pageChangeListener);
this.currentVpSelectedListener = new TabLayout.ViewPagerOnTabSelectedListener(viewPager);
this.addOnTabSelectedListener(this.currentVpSelectedListener);
PagerAdapter adapter = viewPager.getAdapter();
if (adapter != null) {
this.setPagerAdapter(adapter, autoRefresh);
}
if (this.adapterChangeListener == null) {
this.adapterChangeListener = new TabLayout.AdapterChangeListener();
}
this.adapterChangeListener.setAutoRefresh(autoRefresh);
viewPager.addOnAdapterChangeListener(this.adapterChangeListener);
this.setScrollPosition(viewPager.getCurrentItem(), 0.0F, true);
} else {
this.viewPager = null;
this.setPagerAdapter((PagerAdapter)null, false);
}
this.setupViewPagerImplicitly = implicitSetup;
}
上面的源码,大都是与监听器有关的代码,最后,有一行设置PagerAdapter的代码:
this.setPagerAdapter((PagerAdapter)null, false);
我们看到Adapter,都知道Adapter是View绑定数据的一个适配器。View没显示数据,肯定与Adapter有关系,我们继续跟踪:
void setPagerAdapter(@Nullable PagerAdapter adapter, boolean addObserver) {
if (this.pagerAdapter != null && this.pagerAdapterObserver != null) {
this.pagerAdapter.unregisterDataSetObserver(this.pagerAdapterObserver);
}
this.pagerAdapter = adapter;
if (addObserver && adapter != null) {
if (this.pagerAdapterObserver == null) {
this.pagerAdapterObserver = new TabLayout.PagerAdapterObserver();
}
adapter.registerDataSetObserver(this.pagerAdapterObserver);
}
this.populateFromPagerAdapter();
}
上面是注册和注销DataSetObserver,看名字我们能猜到是监听数据变化的一个观察者模式,与数据的展示无关,于是,我们跟踪最后那个不熟悉的populateFrompagerAdapter()方法:
void populateFromPagerAdapter() {
this.removeAllTabs();
if (this.pagerAdapter != null) {
int adapterCount = this.pagerAdapter.getCount();
int curItem;
for(curItem = 0; curItem < adapterCount; ++curItem) {
this.addTab(this.newTab().setText(this.pagerAdapter.getPageTitle(curItem)), false);
}
if (this.viewPager != null && adapterCount > 0) {
curItem = this.viewPager.getCurrentItem();
if (curItem != this.getSelectedTabPosition() && curItem < this.getTabCount()) {
this.selectTab(this.getTabAt(curItem));
}
}
}
}
好了,我们找到问题根源了。在这个方法里面,调用了removeAllTabs,我们可以不用跟踪了,这个方法很明显,移除了所有的Tab,因此,我们原来设置的tab不显示了。
这个可以随意的,可以用AppBarLayout.OnOffsetChangedListener来判断AppBar的状态,也可以仿照这个去实现一个自定义的状态监听器:
public abstract class AppBarStateChangeListener implements AppBarLayout.OnOffsetChangedListener {
public enum State {
EXPANDED,
COLLAPSED,
IDLE
}
private State mCurrentState = State.IDLE;
@Override
public final void onOffsetChanged(AppBarLayout appBarLayout, int i) {
if (i == 0) {
if (mCurrentState != State.EXPANDED) {
onStateChanged(appBarLayout, State.EXPANDED);
}
mCurrentState = State.EXPANDED;
} else if (Math.abs(i) >= appBarLayout.getTotalScrollRange()) {
if (mCurrentState != State.COLLAPSED) {
onStateChanged(appBarLayout, State.COLLAPSED);
}
mCurrentState = State.COLLAPSED;
} else {
if (mCurrentState != State.IDLE) {
onStateChanged(appBarLayout, State.IDLE);
}
mCurrentState = State.IDLE;
}
}
public abstract void onStateChanged(AppBarLayout appBarLayout, State state);
}
有了收缩和伸展状态的监听器,我们就可以根据状态来动态的显示和隐藏顶部的控件了,代码如下:
private void changeToolBarState(AppBarStateChangeListener.State state) {
if (state == AppBarStateChangeListener.State.EXPANDED) {
mView.setVisibility(View.VISIBLE);
mInnerSearch.setVisibility(View.VISIBLE);
mEditText.setVisibility(View.VISIBLE);
mAuthorSearch.setVisibility(View.GONE);
mTopHead.setVisibility(View.GONE);
} else if (state == AppBarStateChangeListener.State.COLLAPSED) {
mView.setVisibility(View.GONE);
mInnerSearch.setVisibility(View.GONE);
mEditText.setVisibility(View.GONE);
mAuthorSearch.setVisibility(View.VISIBLE);
mTopHead.setVisibility(View.VISIBLE);
}else{
mView.setVisibility(View.VISIBLE);
mInnerSearch.setVisibility(View.VISIBLE);
mEditText.setVisibility(View.VISIBLE);
mAuthorSearch.setVisibility(View.GONE);
mTopHead.setVisibility(View.GONE);
}
}
好了,我们看一下最后的效果:
最后,简单总结一下。Material Design库为我们提供了很多华丽的控件和效果,有些看似很复杂的效果,可能使用Material Design来实现会非常的简单。我本人也会继续学习和研究Material Design库其他的效果和控件,最后也会总结和分享。最后,附上Github源码,感兴趣的朋友可以自行查看:https://github.com/hearttudu/CollapsingToolbarLayoutDemo