Ultra-Pull-To-Refresh 自定义下拉刷新视差动画
Posted 严振杰
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Ultra-Pull-To-Refresh 自定义下拉刷新视差动画相关的知识,希望对你有一定的参考价值。
版权声明:转载必须注明本文转自严振杰的博客:http://blog.yanzhenjie.com
下拉刷新视差动画也是这几天公司的一个动画,今晚终于不用加班了,加上好多小伙伴问我这个效果,就把这个动画用博客的形式介绍给大家吧,对了如果你想和我交流更多,可以加我博客联系方式中的QQ群。
首先要说明,今天讲的是自定义下拉刷新动画,不是下拉刷新框架怎么写,所以就算不是你想要的,你看看也无防哈哈哈哈……
效果刷新
Ultra-Pull-To-Refresh下拉刷新库的介绍
Ultra-Pull-To-Refresh
这个下拉刷新库是秋百万(廖祜秋)写的,源代码托管在Github:
https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh。
推荐这个库是一方面是因为PullToRefresh
的停止更新,另一方面是Ultra-Pull-To-Refresh
的合理设计,满足了我所有的幻想,它唯一的不足是:当顶部嵌套类似ViewPager
这种左右滑动的View
时下拉刷新会变的很灵敏,多用户体验不太好,不过这一点我已经给出了一个临时解决方案,如果要知道详情请移步此博客:
http://blog.csdn.net/yanzhenjie1003/article/details/51319181。
不过今天的博客中的库我已经把修复了的源码附上了,所以大家也可以看完本文后直接下载所有源代码。
最后关于这个库的设计和理解我就不多说了,大家直接看秋百万的原文:
https://android-ultra-ptr.liaohuqiu.net/cn/
自定义动画的分析
首先是Ultra-Pull-To-Refresh
的特点,此库提供了一个Layout
类:PtrFramLayout
作为Wrapper
来包涵ContentView
,今天用到两个方法:第一个PtrFramLayout#setHeaderView(View)
用来设置头部显示的刷新View,第二个PtrFramLayout#addPtrUIHandler(PtrHandler)
用来设置监听用户下拉状态、下拉offset
、刷新完成状态等。
其次是动画的,根据效果图,第一点是下拉的时候人物从左侧走过来到中间,到中间后手指再继续往下拉,此时人物也不走了,第二点是当手指松开时或者处于下拉状态时,人物不停的走动,并且背景产生一个相对位移,给人的视觉上造成一个视觉差,也就是我们想要的视差动画了,这就是整个视差动画的实现步骤。
那么几个动画拆分开来就是,人物向右中间移动、人物原地踏步、背景无限向左移动。
头View和刷新Layout的实现
我把实现步骤分开讲解,方便读者理解:
- 实现自定义的头View。
- 继承
PtrFramLayout
实现一个ParallaxPtrFrameLayout
,设置自定头和PtrHandler
监听下拉动作。 - 实现人物向左走的动画。
- 松开手时背景不停的向右移动,人物在原地迈步,形成一个视差上的向右走的动画。
自定义头部View
头View的底下是这样一个图:
那么一个图是如何做到不停的向左移动还是无限重复的呢?用html做很简单,但是android中并没有repeat这样的属性,于是我们想到:在屏幕上放一个ImageView
向左移动100%,在这张图的右侧再放一个ImageView
,以同样的速度向左移动100%,结果就是当屏幕上的图移动到左边外屏幕的时候,屏幕右边的图刚好移动到屏幕上完全显示,然后我们的动画又有重复播放的属性,结合起来就产生了一个背景无限长的动画效果。对于人物原地踏步就很简单了,直接用一个ImageView
不停的切换图形成一个人物在走动的视觉效果。
所以我们用两个ImageView
作为背景图来相间向左移动,用一个ImageView
不停的切换图模拟人物走动,来达到一个人物走动的视差效果,我打算用FrameLayout
来作为头View
的Layout
,所以布局用merge
包裹了一下:
refresh_parallax.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ImageView
android:id="@+id/iv_background_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/app_name"
android:src="@drawable/refresh_down_background" />
<ImageView
android:id="@+id/iv_background_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/app_name"
android:src="@drawable/refresh_down_background" />
<ImageView
android:id="@+id/iv_refresh_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:contentDescription="@string/app_name"
android:scaleType="center" />
</merge>
然后定义头View
加载刚才写好的布局,因为PtrFrameLayout
是通过PtrHandler
接口来监听下拉状态和刷新状态,然后以状态为依据来刷新头View的动画,所以头View直接实现PtrHandler
接口,然后操作自身的状态和动画也就更加方便了,所以头View初步的代码是:
ParallaxHeader
public class ParallaxHeader extends FrameLayout implements PtrUIHandler
ImageView mIvBack1;
ImageView mIvBack2;
ImageView mIvIcon;
private void initialize()
// 加载刚才的
LayoutInflater.from(getContext()).inflate(R.layout.refresh_parallax, this);
// 设置一个蓝色天空的背景。
setBackgroundColor(ContextCompat.getColor(getContext(), R.color.refresh_background));
mIvBack1 = (ImageView) findViewById(R.id.iv_background_1);
mIvBack2 = (ImageView) findViewById(R.id.iv_background_2);
mIvIcon = (ImageView) findViewById(R.id.iv_refresh_icon);
public ParallaxHeader(Context context)
this(context, null, 0);
public ParallaxHeader(Context context, AttributeSet attrs)
this(context, attrs, 0);
public ParallaxHeader(Context context, AttributeSet attrs, int defStyleAttr)
super(context, attrs, defStyleAttr);
@Override
public void onUIReset(PtrFrameLayout frame)
// 重置头View的动画状态,一般停止刷新动画。
@Override
public void onUIRefreshPrepare(PtrFrameLayout frame)
// 准备刷新的UI。
@Override
public void onUIRefreshBegin(PtrFrameLayout frame)
// 开始刷新的UI动画。
@Override
public void onUIRefreshComplete(PtrFrameLayout frame)
// 刷新完成,停止刷新动画。
@Override
public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator)
// 手指下拉的时候的状态,我们的下拉动画的控制就是通过这个方法:
// frame是刷新的root layout。
// isUnderTouch是手指是否按下,因为还有自动刷新,手指肯定是松开状态。
// status是现在的加载状态,准备、加载中、完成:PREPARE、LOADING、COMPLETE。
// ptrIndicator是一些下拉偏移量的参数封装。
里面的代码很简单,就是加载刚才定义好的头View
对应的Layout.xml
文件,然后把两个背景View
和人物View
给找出来。头View
定义好了,接下来定义刷新的Layout
。
实现ParallaxPtrFrameLayout加载头View
Ultra-Pull-To-Refresh
的刷新Layout
都是继承PtrFramLayout
,然后设置头View
和刷新状态监听等,所以我们定义一个ParallaxPtrFrameLayout
继承PtrFrameLayout
,在里面设置头View
和PtrHandler
等来回调操作的头View
的动画,很简单的几行代码:
public class ParallaxPtrFrameLayout extends PtrFrameLayout
public ParallaxPtrFrameLayout(Context context)
super(context);
initViews();
public ParallaxPtrFrameLayout(Context context, AttributeSet attrs)
super(context, attrs);
initViews();
public ParallaxPtrFrameLayout(Context context, AttributeSet attrs, int defStyle)
super(context, attrs, defStyle);
initViews();
private void initViews()
// 这里初始化上面的头View:
ParallaxHeader parallaxHeader = new ParallaxHeader(getContext());
// 这里设置头View为上面自定义的头View:
setHeaderView(parallaxHeader);
// 下拉和刷新状态监听:
// 因为ParallaxHeader已经实现过PtrUIHandler接口,所以直接设置为ParallaxHeader:
addPtrUIHandler(parallaxHeader);
由于Ultra-Pull-To-Refresh
的合理设计,到这里为止,我们的头View
和刷新的Layout
就完成了,接下来就专心研究动画吧。
动画的实现
上文也提过了,这里的动画拆分开几个,一是下拉的时候人物向右中间移动,二是刷新的时候人物不停的原地踏步,三是刷新的时候背景一个向左平移,为了方便理解,这里把下拉时候人物向右中间移动放到最后来讲。
一、人物原地踏步动画
首先想到的就是帧动画,没错就是这家伙,用帧动画可以做到每多少时间换一张图片,所以我们的人物有三张不同的动画,不停的切换就形成了一个人物走动并车轮转动的效果:
我们用帧动画控制每张图显示100毫秒,然后就切换下一张图,这样便达到我们说的人物走动的效果了,用xml来实现:
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item
android:drawable="@drawable/refresh_down_icon_1"
android:duration="100" />
<item
android:drawable="@drawable/refresh_down_icon_2"
android:duration="100" />
<item
android:drawable="@drawable/refresh_down_icon_3"
android:duration="100" />
</animation-list>
因为这是一个帧动画,需要在代码中触发,所以我们要把这个动画放在drawable
文件夹,并且把这个drawable
当图片设置头View
中的人物ImageView
:
<ImageView
android:id="@+id/iv_refresh_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:scaleType="center"
android:src="@drawable/refresh_down_icon" />
二、背景无限向左移动
这个动画就厉害了word哥,当时我先做出来,然后给ios的同学讲实现的原理,他还是花了点时间来理解的,所以我再费点口舌解释一下。
这里是两个ImageView
,一个在屏幕正中央,并且占据整屏宽,一个在屏幕外的右侧,宽度等于屏幕宽度。动画开始时,屏幕上的ImageView
开始一步步向左移动100%,屏幕之外的ImageView
以同样的速度向左移动100%,当屏幕上的ImageView
移动到左边外屏幕的时候,屏幕右边的图刚好移动到屏幕上完全显示,然后我们的动画又有重复播放的属性,结合起来就产生了一个背景无限长的动画效果。
为了方便大家理解,我画了一张图:
图是画的有点简陋了,但是很好理解,当头View刚出来的时候只显示ImageView1
,当刷新的时候ImageView1
和ImageView2
同时向左移动,看起来就是连贯的一张图(实际xml中两张图是没有空隙的),等ImageView1
移出屏幕时,ImageView2
刚好充斥满屏幕,然后我们给动画加上重复播放属性,然后又从图1开始重复到图三,就形成了一个无限向左移动的街道。
所以我们给第一张图的动画是,在2S内,匀速移动,从屏幕上移动到屏幕左外边,然后再次重复动作:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/linear_interpolator">
<translate
android:duration="2000"
android:fromXDelta="0%"
android:interpolator="@android:anim/linear_interpolator"
android:repeatCount="infinite"
android:repeatMode="restart"
android:toXDelta="-100%" />
</set>
我们给第二张图的动画是,在2S内,匀速移动,从屏幕右外边移动到屏幕上,然后再次重复动作:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/linear_interpolator">
<translate
android:duration="2000"
android:fromXDelta="100%"
android:interpolator="@android:anim/linear_interpolator"
android:repeatCount="infinite"
android:repeatMode="restart"
android:toXDelta="0%" />
</set>
三个动画到这里就定义完了,接下来就是怎么控制动画了。
三、动画和下拉动作、刷新状态的结合
要控制动画就要把三个动画加载出来,我们继续回到头View``ParallaxHeader
中。
首先要加载人物的动画,因为是帧动画,所以要用到AnimationDrawable
:
private AnimationDrawable mAnimationDrawable;
private void initialize()
...
mIvIcon = (ImageView) findViewById(R.id.iv_refresh_icon);
mAnimationDrawable = (AnimationDrawable) mIvIcon.getDrawable();
然后加在两个背景的位移动画:
private AnimationDrawable mAnimationDrawable;
private Animation mBackAnim1;
private Animation mBackAnim2;
private void initialize()
...
mIvIcon = (ImageView) findViewById(R.id.iv_refresh_icon);
mAnimationDrawable = (AnimationDrawable) mIvIcon.getDrawable();
mBackAnim1 = AnimationUtils.loadAnimation(getContext(), R.anim.refresh_down_background_1);
mBackAnim2 = AnimationUtils.loadAnimation(getContext(), R.anim.refresh_down_background_2);
接着为了方便调用,也减少代码逻辑的复杂度,我们需要定义两个方法来控制动画的结束和开始,同时为了动画不被重复开始和停止,定义一个变量来记录动画是否是运行的:
/**
* 记录动画是否在执行。
*/
private boolean isRunAnimation = false;
/**
* 开始刷新动画。
*/
private void startAnimation()
if (!isRunAnimation)
isRunAnimation = true;
mIvBack1.startAnimation(mBackAnim1);
mIvBack2.startAnimation(mBackAnim2);
mIvIcon.setImageDrawable(mAnimationDrawable);
mAnimationDrawable.start();
/**
* 停止刷新动画。
*/
private void stopAnimation()
if (isRunAnimation)
isRunAnimation = false;
mIvBack1.clearAnimation();
mIvBack2.clearAnimation();
mAnimationDrawable.stop();
到这里基本上已经完成了,我们可以把上面PrallaxHeader
中的下拉监听和刷新状态代码补全了:
@Override
public void onUIReset(PtrFrameLayout frame)
// 重置头View的动画状态,一般停止刷新动画。
stopAnimation();
@Override
public void onUIRefreshPrepare(PtrFrameLayout frame)
// 准备刷新的UI。
@Override
public void onUIRefreshBegin(PtrFrameLayout frame)
// 开始刷新的UI动画。
stopAnimation();
startAnimation();
@Override
public void onUIRefreshComplete(PtrFrameLayout frame)
// 刷新完成,停止刷新动画。
stopAnimation();
@Override
public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator)
// 手指下拉的时候的状态,我们的下拉动画的控制就是通过这个方法:
// frame是刷新的root layout。
// isUnderTouch是手指是否按下,因为还有自动刷新,手指肯定是松开状态。
// status是现在的加载状态,准备、加载中、完成:PREPARE、LOADING、COMPLETE。
// ptrIndicator是一些下拉偏移量的参数封装。
onUIRefreshPrepare()
是准备UI,这里不需要实现,除了最后一个方法在下拉的时候触发外,其它都已经实现了,如果你基础还过关,你可以照着本博客敲出来上面讲的所有代码,然后在你的Layout
中用一个ParallaxPtrFrameLayout
包涵一个布局,运行起来,然后下拉后松开看看,已经看到了文章开头刷新状态时的背景左移,人物走路的动画了。
不过我是个追求完美的人,所以我必须要实现下拉的时候人物走向中间的动画。
四、下拉时,人物走向中间
不可避免,这里我们要拿到下拉时的总offset,还要拿到手指已经下拉的offset,然后算出一个百分比,结合从屏幕最左边到屏幕中间的位置,算出当前人物需要走到哪里。
这里有一个注意的点,就是人物要走到屏幕中间的位置,这个位置可不是屏幕宽度/2,应该等于屏幕宽度/2 - 人物View宽度/2。因为人物是从屏幕最左边x=0开始移动,如果移动到x=屏幕宽/2这个位置,那么人物就看起来偏右了。好吧说这么多,不如再来个图解释一下:
这里的问号代表的是Y,这个不用关心,我们只需要关心X方向的平移,这里人物ImageView
的X是以左边开始算的,让它移动到屏幕中间的时候它就会是图一所示,此时如果我们将人物ImageView
向左移动半个人的距离,刚好是到屏幕中间,所以人物每次需要移动的距离是(屏幕宽度/2 - 人物View宽度/2)。
那么下面我们把代码撸起:
/**
* 人物到屏幕中间的x点。
*/
private int limitX;
/**
* 计算人物到屏幕中间的x点。
*/
private void calcLimitX()
limitX = DisplayUtils.screenWidth / 2;
int mIconIvWidth = mIvIcon.getMeasuredWidth();
limitX -= (mIconIvWidth / 2);
@Override
public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator)
// 获取总的头部可下拉的距离:
final int offsetToRefresh = frame.getOffsetToRefresh();
// 获取当前手指已经下拉的距离:
final int currentPos = ptrIndicator.getCurrentPosY();
// 当前距离小于总的下拉距离时才计算移动
if (currentPos <= offsetToRefresh && !isRunAnimation)
// 计算人物到屏幕中间的x点。
calcLimitX();
// 根据下拉距离占可下拉高度的比例,算出向右走的距离:
double percent = (double) currentPos / offsetToRefresh;
int targetX = (int) (limitX * percent);
// 人物向右走:
mIvIcon.setTranslationX(targetX);
// 人物向右移动算出来还不够,因为还有换图片才能模拟出人物走动的效果。
// 当百分比是10 30 50 70 90时显示第一张图。
// 当百分比是20 40 60 80 100时显示第二张图。
// 当百分比是5 15 25 35 45 55 65 75 85 95时显示第三张图。
// 这样就模拟出了下拉时人物向右走的效果了。
int newPercent = (int) (percent * 100);
if (newPercent % 10 == 0)
double i = newPercent / 10;
if (i % 2 == 0)
mIvIcon.setImageResource(R.drawable.refresh_down_icon_3);
else
mIvIcon.setImageResource(R.drawable.refresh_down_icon_1);
else if (newPercent % 5 == 0)
mIvIcon.setImageResource(R.drawable.refresh_down_icon_2);
这里废话就再不多说了,一切都在代码注释中,所以下面贴出ParallaxHeader
的完整代码,本文源码下载链接在文章末尾:
public class ParallaxHeader extends FrameLayout implements PtrUIHandler
ImageView mIvBack1;
ImageView mIvBack2;
ImageView mIvIcon;
private Animation mBackAnim1;
private Animation mBackAnim2;
private AnimationDrawable mAnimationDrawable;
private boolean isRunAnimation = false;
private int limitX;
private void initialize()
LayoutInflater.from(getContext()).inflate(R.layout.refresh_parallax, this);
setBackgroundColor(ContextCompat.getColor(getContext(), R.color.refresh_background));
mIvBack1 = (ImageView) findViewById(R.id.iv_background_1);
mIvBack2 = (ImageView) findViewById(R.id.iv_background_2);
mIvIcon = (ImageView) findViewById(R.id.iv_refresh_icon);
mAnimationDrawable = (AnimationDrawable) mIvIcon.getDrawable();
mBackAnim1 = AnimationUtils.loadAnimation(getContext(), R.anim.refresh_down_background_1);
mBackAnim2 = AnimationUtils.loadAnimation(getContext(), R.anim.refresh_down_background_2);
public ParallaxHeader(Context context)
this(context, null, 0);
public ParallaxHeader(Context context, AttributeSet attrs)
this(context, attrs, 0);
public ParallaxHeader(Context context, AttributeSet attrs, int defStyleAttr)
super(context, attrs, defStyleAttr);
initialize();
/**
* 开始刷新动画。
*/
private void startAnimation()
if (!isRunAnimation)
isRunAnimation = true;
mIvBack1.startAnimation(mBackAnim1);
mIvBack2.startAnimation(mBackAnim2);
mIvIcon.setImageDrawable(mAnimationDrawable);
mAnimationDrawable.start();
/**
* 停止刷新动画。
*/
private void stopAnimation()
if (isRunAnimation)
isRunAnimation = false;
mIvBack1.clearAnimation();
mIvBack2.clearAnimation();
mAnimationDrawable.stop();
@Override
public void onUIReset(PtrFrameLayout frame)
stopAnimation();
@Override
public void onUIRefreshPrepare(PtrFrameLayout frame)
@Override
public void onUIRefreshBegin(PtrFrameLayout frame)
stopAnimation();
startAnimation();
@Override
public void onUIRefreshComplete(PtrFrameLayout frame)
stopAnimation();
@Override
public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator)
final int offsetToRefresh = frame.getOffsetToRefresh();
final int currentPos = ptrIndicator.getCurrentPosY();
if (currentPos <= offsetToRefresh && !isRunAnimation)
if (limitX == 0) calcLimitX();
double percent = (double) currentPos / offsetToRefresh;
int targetX = (int) (limitX * percent);
mIvIcon.setTranslationX(targetX);
int newPercent = (int) (percent * 100);
if (newPercent % 10 == 0)
double i = newPercent / 10;
if (i % 2 == 0)
mIvIcon.setImageResource(R.drawable.refresh_down_icon_3);
else
mIvIcon.setImageResource(R.drawable.refresh_down_icon_1);
else if (newPercent % 5 == 0)
mIvIcon.setImageResource(R.drawable.refresh_down_icon_2);
private void calcLimitX()
limitX = DisplayUtils.screenWidth / 2;
int mIconIvWidth = mIvIcon.getMeasuredWidth();
limitX -= (mIconIvWidth / 2);
这会是凌晨两点,瞌睡的要死要死,大家晚安咯。
下载源码的同学注意,源码中ParallaxHeader
最后一个方法的这行代码少了&& !isRunAnimation
判断,自行加上即可:
if (currentPos <= offsetToRefresh && !isRunAnimation)
源码下载传送门: http://download.csdn.net/detail/yanzhenjie1003/9701130;
版权声明:转载必须注明本文转自严振杰的博客:http://blog.yanzhenjie.com
以上是关于Ultra-Pull-To-Refresh 自定义下拉刷新视差动画的主要内容,如果未能解决你的问题,请参考以下文章