Ultra-Pull-To-Refresh 自定义下拉刷新视差动画
来源:互联网 发布:电脑怎么激活windows 编辑:程序博客网 时间:2024/05/16 08:44
版权声明:转载必须注明本文转自严振杰的博客: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);}@Overridepublic 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 自定义下拉刷新视差动画
- Ultra-Pull-To-Refresh 自定义下拉刷新视差动画
- Ultra Pull To Refresh使用(自定义下拉刷新的头部)
- android-Ultra-Pull-To-Refresh-下拉自定义显示动画1
- android-Ultra-Pull-to-Refresh下拉刷新
- android下拉刷新android-Ultra-Pull-To-Refresh使用
- 下拉刷新框架android-Ultra-Pull-To-Refresh示例
- 下拉刷新库android-Ultra-Pull-To-Refresh的使用
- Ultra Pull To Refresh 下拉刷新 替代PullToRefresh
- Ultra-Pull-To-Refresh实现下拉刷新上拉加载
- android-Ultra-Pull-To-Refresh实现下拉刷新WebView
- 使用 android-Ultra-Pull-To-Refresh 实现 WebView 下拉刷新
- 下拉刷新框架Android-Ultra-Pull-To-Refresh的使用
- Pull to Refresh下拉刷新
- android-Ultra-Pull-To-Refresh刷新
- Ultra-Pull-To-Refresh-自定义头部
- Android下拉刷新开源控件 liaohuqiu/android-Ultra-Pull-To-Refresh
- Ultra Pull To Refresh实现知乎下拉刷新风格注意事项
- 哈佛大学凌晨四点半
- Ubuntu 16.04 LTS 安装 Nginx/PHP 7/MySQL 5.7 (LEMP)
- javascript复制网页内容 execCommand(´ Copy´ )
- <<Linux内核完全剖析 --基于0.12内核>>学习笔记 第4章 80x86保护模式及其编程 4.2 保护模式内存管理
- 优先队列c++ STL用法
- Ultra-Pull-To-Refresh 自定义下拉刷新视差动画
- sdut 数据结构实验之二叉树六:哈夫曼编码
- Linux最基本的一些命令
- Android 解决ListView、GridView在首次显示时adapter可能多次调用getView的问题
- 赫夫曼编码长度计算问题?
- [Leetcode] Container With Most Water Python
- 模拟题:流量问题
- 446. Arithmetic Slices II - Subsequence
- 树结构练习——判断给定森林中有多少棵树-并查集