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的实现

我把实现步骤分开讲解,方便读者理解:

  1. 实现自定义的头View。
  2. 继承PtrFramLayout实现一个ParallaxPtrFrameLayout,设置自定头和PtrHandler监听下拉动作。
  3. 实现人物向左走的动画。
  4. 松开手时背景不停的向右移动,人物在原地迈步,形成一个视差上的向右走的动画。

自定义头部View

头View的底下是这样一个图:
头View背景

那么一个图是如何做到不停的向左移动还是无限重复的呢?用HTML做很简单,但是Android中并没有repeat这样的属性,于是我们想到:在屏幕上放一个ImageView向左移动100%,在这张图的右侧再放一个ImageView,以同样的速度向左移动100%,结果就是当屏幕上的图移动到左边外屏幕的时候,屏幕右边的图刚好移动到屏幕上完全显示,然后我们的动画又有重复播放的属性,结合起来就产生了一个背景无限长的动画效果。对于人物原地踏步就很简单了,直接用一个ImageView不停的切换图形成一个人物在走动的视觉效果。

所以我们用两个ImageView作为背景图来相间向左移动,用一个ImageView不停的切换图模拟人物走动,来达到一个人物走动的视差效果,我打算用FrameLayout来作为头ViewLayout,所以布局用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,在里面设置头ViewPtrHandler等来回调操作的头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就完成了,接下来就专心研究动画吧。

动画的实现

上文也提过了,这里的动画拆分开几个,一是下拉的时候人物向右中间移动,二是刷新的时候人物不停的原地踏步,三是刷新的时候背景一个向左平移,为了方便理解,这里把下拉时候人物向右中间移动放到最后来讲。

一、人物原地踏步动画

首先想到的就是帧动画,没错就是这家伙,用帧动画可以做到每多少时间换一张图片,所以我们的人物有三张不同的动画,不停的切换就形成了一个人物走动并车轮转动的效果:
icon1
icon2
icon3

我们用帧动画控制每张图显示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,当刷新的时候ImageView1ImageView2同时向左移动,看起来就是连贯的一张图(实际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

15 0
原创粉丝点击