背景
贝贝的各个业务线APP中,常常会出现类似下面结构的页面:

整个页面主要由头部信息区域、tab区域、列表内容区域组成。当向上滑动,头部内容滑出屏幕后,tab栏会吸顶,列表内容可以继续滑动。要实现这个页面结构,贝贝目前已有成熟的方案。
贝店首页也有这样的类似需求。用贝贝的成熟方案很容易实现,但是存在一个问题:

当向上滑动页面,tab栏到达屏幕顶部的临界状态时,之后列表内容就不能继续向上滑动,需要松开手指后才能继续进行滑动操作。
为了更好的用户体验,我们决定解决这个问题,实现无阻断的流畅滑动体验。
需求分析
从贝店的具体需求出发,与贝贝首页相比,发现有些差异:贝店的tab栏是一个横向混动的时间轴,tab栏下方的列表区域是单个列表,而不是可以左右滑动的ViewPager。时间轴内选中一个时间点tab,下面的列表内容会同步刷新为对应数据。
基于此,我们选用RecyclerView来呈现列表数据,由于用不到ViewPager,我们对BeiBeiSDK中的公共组件PagerSlidingTabStrip进行了改造,剔除了其中的ViewPager相关逻辑,变得更轻量,产生了自定义的时间轴控件PagerSlidingTimeSlotTabStrip。
最后,要实现无阻断滑动,首先想到的是从事件分发的角度来处理滑动冲突问题,但这个方案导致的页面结构比较复杂,同时也需要自定义相关控件来处理Touch事件,比较繁琐。在当时贝店版本飞速迭代的情况下,我们采用了比较讨巧的方案:
整个列表包括头部内容区域只用一个RecyclerView来实现,为了实现时间轴吸顶效果,同时存在两个时间轴控件A和B,A位于HeaderView内,B悬浮在RecyclerView上方,默认不可见。他们的状态完全同步,如下图:

详细实现
来看下时间轴控件PagerSlidingTimeSlotTabStrip:
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 54 55 56 57 58 59 60
| public class PagerSlidingTimeSlotTabStrip extends HorizontalScrollView { private OnTabSelectedListener mSelectedListener; private PagerSlidingTimeSlotTabStrip mAttachedTabStrip; public interface OnTabSelectedListener { void onNewTabSelected(int curPosition); } public void setTabSelectedListener(OnTabSelectedListener listener) { mSelectedListener = listener; if (mAttachedTabStrip != null && !mIsOnlyAttachScroll) { mAttachedTabStrip.setTabSelectedListener(listener); } } private boolean mIsOnlyAttachScroll = false; public void setAttachedTabStrip(PagerSlidingTimeSlotTabStrip tabStrip) { setAttachedTabStrip(tabStrip, false); } public void setAttachedTabStrip(PagerSlidingTimeSlotTabStrip tabStrip, boolean isOnlyAttachScroll) { mAttachedTabStrip = tabStrip; mIsOnlyAttachScroll = isOnlyAttachScroll; } public void refreshData(List<TimeSlotModel> list) { if (mAttachedTabStrip != null && !mIsOnlyAttachScroll) { mAttachedTabStrip.refreshData(list); } } public void scrollToChild(int position, int offset) { if (mAttachedTabStrip != null && !mIsOnlyAttachScroll) { mAttachedTabStrip.scrollToChild(position, offset); } } @Override protected void onScrollChanged(int x, int y, int oldX, int oldY) { super.onScrollChanged(x, y, oldX, oldY); if (mOnScrollChangeListener != null) { mOnScrollChangeListener.onScrollChanged(this, x, y, oldX, oldY); } if (mAttachedTabStrip != null) { mAttachedTabStrip.scrollTo(x, y); } } }
|
之前提到,我们有两个一毛一样的时间轴控件,当到达吸顶临界状态的时候控制他们是否可见,因此重点在于如何处理两个时间轴的状态完全同步。这里通过调用setAttachedTabStrip方法,让时间轴A内部持有时间轴B的对象引用,这样一来,A发生任何滚动、时间项选中等等UI变化时,都可以及时的应用到B上,这样就保证了两者的同步。
这里需要特别指出的是,两个时间轴是有主动和被动的角色区分的:
1 2 3
| mTimeSlotTabStrip.setAttachedTabStrip(mStickyTimeSlotTabStrip); mStickyTimeSlotTabStrip.setAttachedTabStrip(mTimeSlotTabStrip, true);
|
在列表的HeaderView内的时间轴A为主动角色,悬浮状态的时间轴B为被动角色。
因为两者互相attach,部分操作可能会导致死循环,所以只在操作A时同步给B。这样不会有问题,因为任意时刻,A和B只会有一个为可见状态,我们只能看到当前所操作的那个时间轴。
可能有人会有疑惑:既然操作时只把A的状态同步给B,那么AB的可见状态切换时,如何保证两者看起来一样呢?
看上面代码,PagerSlidingTimeSlotTabStrip的onScrollChanged滚动监听回调方法里,会去处理滚动同步,这里不会区分主被动角色。
关于时间轴控件的visible切换控制,是通过监听RecyclerView的滚动来实现的:
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
| private final RecyclerView.OnScrollListener mRecyclerViewOnScrollListener = new RecyclerView.OnScrollListener() { ... @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); int headerViewHeight = mHeaderViewContainer.getHeight() - mTimeSlotTabStrip.getHeight(); if (getScrollYDistance() > headerViewHeight) { setStickyTimeSlotTabStropVisible(View.VISIBLE); EventBus.getDefault().post(new TopbarAnimEvent(TopbarAnimEvent.TYPE_HIDE)); } else { setStickyTimeSlotTabStropVisible(View.GONE); EventBus.getDefault().post(new TopbarAnimEvent(TopbarAnimEvent.TYPE_SHOW)); } } private void setStickyTimeSlotTabStropVisible(int visible) { if (visible == View.VISIBLE) { mStickyTimeSlotTabStrip.scrollToChild(getCurrentTimeSlotIndex(), 0); } mStickyTimeSlotTabStrip.setVisibility(visible); mStickyTimeSlotDividerLine.setVisibility(visible); } };
|
比较简单,这里不再赘述。
到此为止,核心的实现逻辑基本介绍完了,重点就在于如何控制两个时间轴控件的状态同步,让他们在visible切换后仍然看起来一样,实现无缝衔接。
总结
以上就是贝店首页无阻断流畅滑动体验的实践。在贝店业务快速迭代的背景下,我们需要用最快的方案实现需求,为了尽可能复用目前已有的组件,采用最小改动的方案来实现,以支撑业务飞速发展。