GitHub:https://github.com/baiyuliang/JdRefresh
CSDN下载地址:https://download.csdn.net/download/baiyuliang2013/12739758
优化升级版:高仿京东2020版首页效果2
效果图:
看本篇文章之前,建议最好先打开京东app,体验一下原版效果,并试着自己去思考其布局和效果的实现方法,那么再看此文章时,可达到事半功倍的效果!
我们先来根据实际效果,分析布局方式:
- 底层有一背景色,跟搜索栏背景色一致;
- Tab为首页时,有整体(带Tab栏)下拉刷新效果,且向上滑动时,也是整体向上滑动的;
- 搜索栏除了一个伸缩效果外是固定不动的;
- 切换到其它Tab后,不再有整体下拉效果,而是Tab栏以下刷新;
根据上述直观的效果,我大致将布局分为4层:
根据实际效果,广告图的顶部实际是超过布局的,如marginTop=-300dp,方可实现所需效果!
接下来,我们再分析具体的布局方式:
目前大部分app,基本都是 TabLayout+ViewPager+Fragment的方式,而这个也不列外,所以第三层内容页同样是这种实现方式;
内容层布局:
<LinearLayout
android:id="@+id/ll_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv_refresh_state"
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center"
android:text="下拉刷新"
android:textColor="#dddddd" />
<net.lucode.hackware.magicindicator.MagicIndicator
android:id="@+id/magicIndicator"
android:layout_width="match_parent"
android:layout_height="35dp" />
<com.byl.jdrefresh.CustomViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
再看Tab为首页时,为整体滑动效果,如果内容页仅为TabLayout+ViewPager布局,是不可能有整体滑动效果的,因此,外层我目前想出的只有套一层ScrollView(这里使用的是NestedScrollVIew),并且要实现需求效果,那必然要重写NestedScrollVIew,并监听手势状态!
而第一层,第二层和第三层效果联系非常紧密,所以这三层可以合并为一层,一起放进重写的NestedScrollVIew中,当然,这三层布局方式必然是帧布局或者相对布局!
主界面布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#f7f7f7"
android:focusable="true"
android:focusableInTouchMode="true">
<com.byl.jdrefresh.JdScrollView
android:id="@+id/jdScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true" />
<RelativeLayout
android:id="@+id/rl_top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#acddee">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="15dp"
android:paddingRight="15dp">
<LinearLayout
android:id="@+id/ll_search"
android:layout_width="0dp"
android:layout_height="30dp"
android:layout_weight="1"
android:background="@drawable/bg_solid_ff_50"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="搜索"
android:layout_marginLeft="15dp"
android:textColor="#666666"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
自定义NestedScrollView布局:
布局方式基本分析完毕,接下来就是核心部分,NestedScrollView的重写!
无论什么效果,都可以从最简单的实现方式开始,我早前写过一篇如何自己实现listview下拉刷新和上拉加载效果的文章:手把手教你轻松实现listview下拉刷新
如果你看过这篇文章,或者自己写过类似效果,了解原理,并对事件分发机制了如指掌,那么这篇文章说实话就没必要再看下去了,仅仅看一个思路即可!而对初学者来说,这篇文章以及上面提到的文章,是一个不错的学习机会!
这篇文章,主要讲解实现思路,和关键核心的地方,具体代码可以直接下载,或访问GitHub克隆!
手指在屏幕上向下滑动,布局整体跟随手指向下滑动,如何实现?
这其实就类似于一个跟随效果,通过监听onTouch事件,实时获取手指坐标以及滑动距离,并实时设置view的坐标,便可实现手指跟随效果!当然对于本项目的效果,并不是通过不断设置坐标实现的,而是通过不断设置View的paddingTop或marginTop实现的,上面提到的listview刷新文章中也提到过!
- 手指按下时(MotionEvent.ACTION_DOWN)记录触点位置;
- 滑动时(MotionEvent.ACTION_MOVE),计算滑动距离并实时设置内容页的paddingTop值;
- 手指离开屏幕(MotionEvent.ACTION_UP)时,复原View的初始位置;
其实最核心的地方就是上面这样非常简单的道理,而我们要做的就是去处理此过程中遇到的各种手势冲突等复杂问题,以及一些UI细节等体验问题!
我们往细节了看,下拉时搜索栏颜色渐变至消失(设置Alpha实现),广告图渐变至完全显示(设置Alpha实现),另外注意一个 细节,广告图是滑动到一定距离,才开始和内容页一起向下滚动的,同样下拉刷新也是滑动到一定距离才到释放刷新状态的!这些细节都是要考虑的地方!
监听onTouchMove时,需要注意的是,我们仅需要在ScrollView在最顶部时向下拉,才接管手势处理,其它情况都不应该阻止move事件,这样可以保证下拉和正常的上划操作互不影响!
@Override
public void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
scrollY = t;
}
仅当scrollY==0时才能执行下拉操作!
另一需要注意的地方是,从顶部下拉后松开手指,View回弹复原,一切正常,但从顶部下拉后不松开手指,而是再向上反方向滑动,此时就会有问题了,Move事件会和ScrollView本身的滑动事件onScroll冲突,此时我们就要做好处理,判断在这种情况下,要禁止onScroll事件(通过重写onOverScrolled方法)!
@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
if (!disable && !isInterceptScroll)
super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
}
在即将实现最终效果时遇到了一个大问题:正在刷新状态时,此时手指按下屏幕,并向下滑动一段距离后停留,待刷新完成,顶部复位后,又瞬间自动移动到了手指停留的地方,我已经做了刷新中截断move事件的处理,为什么会这样?
其实在了解清楚事件分发后,就不难理解了:
原本是全部在ScrollView中监听ACTION_DOWN和ACTION_MOVE ,ACTION_DOWN记录初始位置,ACTION_MOVE记录实时位置,然后通过ev.getRawY() - startY计算滑动距离,而实际当你按下手指时,先走的是ViewPager中dispatchTouchEvent的ACTION_DOWN,ScrollView中的OnTouch是最后才走的,这个没有争议,对吧,当正在刷新时,我在ScrollView的ACTION_MOVE中判断了状态,即刷新中时如果有滑动操作则直接返回true消费掉事件,不会走MOVE后面的代码,看似没问题,但实际上我仅仅是截断了ScrollView的onMove,而它的子View,ViewPager依然在执行onMove事件,它在实时传递给ScrollView的onMove,所以当刷新完成的一刹那,ScrollView的ACTION_MOVE截断解除,会瞬间执行后面的代码,而此时并没有走ScrollView的ACTION_DOWN,也就是startY=0,而ACTION_MOVE中计算移动距离:ev.getRawY() - startY得出的直接就是手指当前位置,造成这种bug效果也就不言而喻了!
解决办法:
1.VIewPager中:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//消费掉(即不走自身的onTouch,也不走父容器的onTouch)
if (isRefreshing) {
startX = 0;
startY = 0;
return true;
}
float x = ev.getRawX();
float y = ev.getRawY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = x;
startY = y;
break;
2.ScrollView中:
@Override
public boolean onTouchEvent(MotionEvent ev) {
//先走的是ViewPager的OnTouch,所以从ViewPager取startY最准确
startY = viewPager.getStartY() == 0 ? ev.getRawY() : viewPager.getStartY();
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
if (startY != 0 && viewPager.getStartY() == 0) return true;
接下来要说的是回弹动画,如果在ACTION_UP时,直接通过设置setMarginTop复位,那么效果可想而知,非常突兀,这里就会用到属性动画中的ValueAnimator,它可以通过给定的值,在你设置的一段事件内平滑的将给定的值返回给你,这样我就可以在设置的时间内平滑的去设置MarginTop直至复位,效果立竿见影,这里一定要注意,广告图和内容页移动距离是不同的!
ValueAnimator animator = ValueAnimator.ofInt(AD_START_SCROLL_DISTANCE);
animator.setDuration(100);
animator.start();
animator.addUpdateListener(animation -> {
int value = (int) animation.getAnimatedValue();
ll_content.setPadding(0, paddingTop + AD_START_SCROLL_DISTANCE - value, 0, 0);
});
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
tv_refresh_state.setText("下拉刷新");
isInterceptTouch = false;
isInterceptScroll = false;
REFRESH_STATUS = REFRESH_DONE;
viewPager.setRefreshing(false);
layoutView(0);
layoutAd(marginTop);
iv_ad.setImageAlpha(0);
if (onPullListener != null) onPullListener.onPull(255);
ll_content.setPadding(0, paddingTop, 0, 0);
enableTab();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
最后关于ScrollView嵌套ViewPager的问题还是有必要说明一下,这样干过的同学应该是都知道这么布局的问题所在的,如果不做任何处理,ViewPager的内容是无法显示的,有两种解决办法:
1.ScrollView添加属性:fillIViewPort=true,但这样做你会发现显示没问题了,但滑动却出现了问题;
2.ViewPager设置固定高度,这样确实完美了,但内容一般是不固定的,所以直接设置固定高度自然不合适,那就只有根据内容动态设置高度了,当Tab为首页时,高度设为实际内容高度,同时要禁止RecyclerView的滚动事件,这样上下滑动时就是整体滑动,其它Tab时,再将高度设为match_parent(同时禁止ScrollView滑动),这样就仅有ViewPager内部的滑动事件了!