支付宝首页的下拉刷新效果不走寻常路,跟我们理解的很不一样。它在下拉刷新时,分成上下两段(topLayout & bottomLayout),loading动画处于中间的位置。在拖拽的时候,就像是从中间撕开的样子。
真要细细琢磨起来,这样的下拉刷新效果,确实挺难搞的。在页面中的任何一处都能上下拖动,相当考验细心和耐心。
点击图片可查看截屏视频
或许很多人都想到了CoordinatorLayout,诚然,CoordinatorLayout是距离这种下拉刷新效果最近的官方控件。但是,有一些体验上的问题,却是CoordinatorLayout也无能为力的。比如说:在topLayout按下触摸,向下拖动时,怎么把loading动画慢慢显示出来? 或者,topLayout向上拖动,以较快的速度松手时,fling效果如何传达到bottomLayout?
或许你会说,我们自定义CoordinatorLayout...
我不否认这可能是一种可行的方案,如果你对其源码足够了解,如果你对Nesting机制和behavior有足够的掌控力。
可是我也想问,如果这些如果都能成真的话,干嘛不来一次全新的旅程?
CoordinatorLayout和Nesting机制告诉我们,一次Touch拖动事件,并不是一次性消费的,而是可以被多个View同时消费。如果你涉猎过足够多的系统源码,会知道Nesting机制的核心是MotionEvent有一个bug级的方法offsetLocation。这是一个public方法,我们在处理Touch事件时一样可以调用。
然而无论如何,CoordinatorLayout规规矩矩做事,本本分分做View,跟我们理解的Touch事件分发机制是并不冲突的。
我曾经做过一个试验:
- FrameLayout包含两个子View,第一个子View是ScrollView,放在底部;第二个子View是TextView(MATCH_PARENAT)放在顶部,用作遮罩,背景透明;
- 我用手指滑动屏幕,ScrollView可以正常滚动;
- 将TextView设置成可点击的,setClickable(true),用手指滑动屏幕时,ScrollView无法正常滚动。
- 自定义FrameLayout,onInterceptTouchEvent和onTouchEvent在自行处理的同时,也转发给scrollView,scrollView可以正常滑动;
关键代码如下:
public class AlipayContainerLayout extends FrameLayout {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
scrollView.onInterceptTouchEvent(ev);
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
scrollView.onTouchEvent(event);
return true;
}
}
你可能会说:这扰乱了Touch事件的分发流程,简直就是在胡搞!
是的,不得不承认这确实是耍流氓,是一种违法行为!呵呵,淡定,不要鸡冻!我能想起很多开源框架,用反射、用Hook的方式欺骗系统、绕过检查等等,跟它们比起来,我们只能算轻微的投机倒把而已(捂脸),不要方!
那么,支付宝的首页刷新是不是可以理解成这样:
- FrameLayout包含两个子View,第一个子View是ScrollView,第二个子View是topLayout;
- ScrollView顶部留白,占位用;
- ScrollView消费Touch事件;如果触摸滑动落在topLayout,则Touch事件由FrameLayout转发给ScrollView;如果触摸滑动落在ScrollView内部,则ScrollView调用自身的Touch消费即可;
感兴趣的同学可以先这么试试看。
可以透露的是,你一定会遇到林林总总的麻烦。不用怕,这些都是考验,走过九九八十一道坎,你会对系统底层的理解更进一步;而且,前文提及offsetLocation是个好东西,能用好这个彩蛋,绝对是一件值得开心的事情。
- layout布局文件
<com.stone.alipay.library.AlipayContainerLayout
android:id="@+id/home_container_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
alipay:progressColor="@color/statusBarColor"
alipay:progressCenterOffset="3dp"
alipay:progressHeight="@dimen/alipay_progress_height" />
- java代码使用
containerLayout = findViewById(R.id.home_container_layout)
containerLayout.setDecorator(new AlipayContainerLayout.Decorator() {
@Override
public View getContentView() {
// 内部滑动的scrollView content
View contentView = initContentView(inflater);
return contentView;
}
@Override
public View getTopLayout() {
// 顶部悬浮的topLayout
topLinearLayout = (TopLinearLayout) initTopLayout(inflater);
return topLinearLayout;
}
});
// 2. 下拉刷新
scrollView = containerLayout.getScrollView();
scrollView.setOnRefreshListener(new AlipayScrollView.OnRefreshListener() {
@Override
public void onRefresh() {
// 下拉刷新回调,请求网络数据
requestNetwork();
}
});
// 3. 顶部视差效果绑定
scrollView.setScrollChangeListener(new AlipayScrollView.ScrollChangeListener() {
@Override
public void onScrollChange(int scrollY) {
parallaxScroll(scrollY);
}
});
topLinearLayout.bindParallax(scrollView, topBlueLayout);