Android自定义下拉刷新动画--仿百度外卖下拉刷新

好久没写博客了,小编之前一段时间一直在找工作,从天津来到了我们的大帝都,感觉还不错。好了废话不多说了,开始我们今天的主题吧。现如今的APP各式各样,同样也带来了各种需求,一个下拉刷新都能玩出花样了,前两天订饭的时候不经意间看到了“百度外卖”的下拉刷新,今天的主题就是它–自定义下拉刷新动画

看一下实现效果吧:

动画

我们先来看看Android中的动画吧:

Android中的动画分为三种:

  • Tween动画,这一类的动画提供了旋转、平移、缩放等效果。

    • Alpha – 淡入淡出
    • Scale – 缩放效果
    • Roate – 旋转效果
    • Translate – 平移效果
  • Frame动画(帧动画),这一类动画可以创建一个Drawable序列,按照指定时间间歇一个一个显示出来。
  • Property动画(属性动画),Android3.0之后引入出来的属性动画,它更改的是对象的实际属性。

分析

我们可以看到百度外卖的下拉刷新的头是一个骑车的快递员在路上疾行,分析一下我们得到下面的动画:

  1. 背景图片的平移动画
  2. 太阳的自旋转动画
  3. 两个小轮子的自旋转动画

这就很简单了,接下来我们去百度外面的图片资源文件里找到这几张图片:(下载百度外卖的apk直接解压即可)

定义下拉刷新头文件:headview.xml

这里注意一下:我们定义了两张背景图片的ImageView是为了可以实现背景的平移动画效果。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/iv_back1"
        android:src="@drawable/pull_back"
        android:layout_width="match_parent"
        android:layout_height="100dp" />
    <ImageView
        android:id="@+id/iv_back2"
        android:src="@drawable/pull_back"
        android:layout_width="match_parent"
        android:layout_height="100dp" />

    <RelativeLayout
        android:id="@+id/main"
        android:layout_centerHorizontal="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        <ImageView
            android:layout_marginTop="45dp"
            android:id="@+id/iv_rider"
            android:background="@drawable/pull_rider"
            android:layout_width="50dp"
            android:layout_height="50dp" />
        <ImageView
            android:id="@+id/wheel1"
            android:layout_marginLeft="10dp"
            android:layout_marginTop="90dp"
            android:background="@drawable/pull_wheel"
            android:layout_width="15dp"
            android:layout_height="15dp" />
        <ImageView
            android:id="@+id/wheel2"
            android:layout_marginLeft="40dp"
            android:layout_marginTop="90dp"
            android:background="@drawable/pull_wheel"
            android:layout_width="15dp"
            android:layout_height="15dp" />
    </RelativeLayout>
    <ImageView
        android:id="@+id/ivsun"
        android:layout_marginTop="20dp"
        android:layout_toRightOf="@+id/main"
        android:background="@drawable/pull_sun"
        android:layout_width="30dp"
        android:layout_height="30dp" />

</RelativeLayout>

接下来我们定义动画效果:

背景图片的平移效果:

实现两个animation xml文件,一个起始位置在100%,结束位置在0%,设置repeat属性为循环往复。

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/accelerate_interpolator">
    <translate android:fromXDelta="100%p" android:toXDelta="0%p"
        android:repeatMode="restart"
        android:interpolator="@android:anim/linear_interpolator"
        android:repeatCount="infinite"
        android:duration="5000" />
</set>

另一个起始位置在0%,结束位置在-100%

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/accelerate_interpolator">
    <translate android:fromXDelta="0%p" android:toXDelta="-100%p"
        android:repeatMode="restart"
        android:interpolator="@android:anim/linear_interpolator"
        android:repeatCount="infinite"
        android:duration="5000" />
</set>

太阳围绕中心旋转动画:

从0-360度开始循环旋转,旋转所用时间为1s,旋转中心距离view的左定点上边缘为50%的距离,也就是正中心。

下面是具体属性:

android:fromDegrees 起始的角度度数

android:toDegrees 结束的角度度数,负数表示逆时针,正数表示顺时针。如10圈则比android:fromDegrees大3600即可

android:pivotX 旋转中心的X坐标

浮点数或是百分比。浮点数表示相对于Object的左边缘,如5; 百分比表示相对于Object的左边缘,如5%; 另一种百分比表示相对于父容器的左边缘,如5%p; 一般设置为50%表示在Object中心

android:pivotY 旋转中心的Y坐标

浮点数或是百分比。浮点数表示相对于Object的上边缘,如5; 百分比表示相对于Object的上边缘,如5%; 另一种百分比表示相对于父容器的上边缘,如5%p; 一般设置为50%表示在Object中心

android:duration 表示从android:fromDegrees转动到android:toDegrees所花费的时间,单位为毫秒。可以用来计算速度。

android:interpolator表示变化率,但不是运行速度。一个插补属性,可以将动画效果设置为加速,减速,反复,反弹等。默认为开始和结束慢中间快,

android:startOffset 在调用start函数之后等待开始运行的时间,单位为毫秒,若为10,表示10ms后开始运行

android:repeatCount 重复的次数,默认为0,必须是int,可以为-1表示不停止

android:repeatMode 重复的模式,默认为restart,即重头开始重新运行,可以为reverse即从结束开始向前重新运行。在android:repeatCount大于0或为infinite时生效

android:detachWallpaper 表示是否在壁纸上运行

android:zAdjustment 表示被animated的内容在运行时在z轴上的位置,默认为normal。

normal保持内容当前的z轴顺序

top运行时在最顶层显示

bottom运行时在最底层显示

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <rotate
        android:fromDegrees="0"
        android:toDegrees="360"
        android:duration="1000"
        android:repeatCount="-1"
        android:pivotX="50%"
        android:pivotY="50%" />
</set>

同理轮子的动画也一样,不占代码了。

动画定义完了我们开始定义下拉刷新列表,下拉刷新网上有很多,不详细的说了,简单的改造一下,根据刷新状态开启关闭动画即可。

注释写的很详细,看一下代码吧:

package com.hankkin.baidugoingrefreshlayout;

import android.widget.AbsListView;
import android.widget.ListView;

import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.RelativeLayout;

/**
 * Created by Hankkin on 16/4/10.
 */
public class BaiDuRefreshListView extends ListView implements AbsListView.OnScrollListener{
    private static final int DONE = 0;      //刷新完毕状态
    private static final int PULL_TO_REFRESH = 1;   //下拉刷新状态
    private static final int RELEASE_TO_REFRESH = 2;    //释放状态
    private static final int REFRESHING = 3;    //正在刷新状态
    private static final int RATIO = 3;
    private RelativeLayout headView;    //下拉刷新头
    private int headViewHeight; //头高度
    private float startY;   //开始Y坐标
    private float offsetY;  //Y轴偏移量
    private OnBaiduRefreshListener mOnRefreshListener;  //刷新接口
    private int state;  //状态值
    private int mFirstVisibleItem;  //第一项可见item索引
    private boolean isRecord;   //是否记录
    private boolean isEnd;  //是否结束
    private boolean isRefreable;    //是否刷新

    private ImageView ivWheel1,ivWheel2;    //轮组图片组件
    private ImageView ivRider;  //骑手图片组件
    private ImageView ivSun,ivBack1,ivBack2;    //太阳、背景图片1、背景图片2
    private Animation wheelAnimation,sunAnimation;  //轮子、太阳动画
    private Animation backAnimation1,backAnimation2;    //两张背景图动画

    public BaiDuRefreshListView(Context context) {
        super(context);
        init(context);
    }

    public BaiDuRefreshListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public BaiDuRefreshListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    public interface OnBaiduRefreshListener{
        void onRefresh();
    }

    /**
     * 回调接口,想实现下拉刷新的listview实现此接口
     * @param onRefreshListener
     */
    public void setOnBaiduRefreshListener(OnBaiduRefreshListener onRefreshListener){
        mOnRefreshListener = onRefreshListener;
        isRefreable = true;
    }

    /**
     * 刷新完毕,从主线程发送过来,并且改变headerView的状态和文字动画信息
     */
    public void setOnRefreshComplete(){
        //一定要将isEnd设置为true,以便于下次的下拉刷新
        isEnd = true;
        state = DONE;

        changeHeaderByState(state);
    }

    private void init(Context context) {
        //关闭view的OverScroll
        setOverScrollMode(OVER_SCROLL_NEVER);
        setOnScrollListener(this);
        //加载头布局
        headView = (RelativeLayout) LayoutInflater.from(context).inflate(R.layout.headview,this,false);
        //测量头布局
        measureView(headView);
        //给ListView添加头布局
        addHeaderView(headView);
        //设置头文件隐藏在ListView的第一项
        headViewHeight = headView.getMeasuredHeight();
        headView.setPadding(0, -headViewHeight, 0, 0);

        //获取头布局图片组件
        ivRider = (ImageView) headView.findViewById(R.id.iv_rider);
        ivSun = (ImageView) headView.findViewById(R.id.ivsun);
        ivWheel1 = (ImageView) headView.findViewById(R.id.wheel1);
        ivWheel2 = (ImageView) headView.findViewById(R.id.wheel2);
        ivBack1 = (ImageView) headView.findViewById(R.id.iv_back1);
        ivBack2 = (ImageView) headView.findViewById(R.id.iv_back2);
        //获取动画
        wheelAnimation = AnimationUtils.loadAnimation(context, R.anim.tip);
        sunAnimation = AnimationUtils.loadAnimation(context, R.anim.tip1);

        backAnimation1 = AnimationUtils.loadAnimation(context, R.anim.a);
        backAnimation2 = AnimationUtils.loadAnimation(context, R.anim.b);

        state = DONE;
        isEnd = true;
        isRefreable = false;

    }

    @Override
    public void onScrollStateChanged(AbsListView absListView, int i) {
    }
    @Override
    public void onScroll(AbsListView absListView, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        mFirstVisibleItem = firstVisibleItem;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (isEnd) {//如果现在时结束的状态,即刷新完毕了,可以再次刷新了,在onRefreshComplete中设置
            if (isRefreable) {//如果现在是可刷新状态   在setOnMeiTuanListener中设置为true
                switch (ev.getAction()){
                    //用户按下
                    case MotionEvent.ACTION_DOWN:
                        //如果当前是在listview顶部并且没有记录y坐标
                        if (mFirstVisibleItem == 0 && !isRecord) {
                            //将isRecord置为true,说明现在已记录y坐标
                            isRecord = true;
                            //将当前y坐标赋值给startY起始y坐标
                            startY = ev.getY();
                        }
                        break;
                    //用户滑动
                    case MotionEvent.ACTION_MOVE:
                        //再次得到y坐标,用来和startY相减来计算offsetY位移值
                        float tempY = ev.getY();
                        //再起判断一下是否为listview顶部并且没有记录y坐标
                        if (mFirstVisibleItem == 0 && !isRecord) {
                            isRecord = true;
                            startY = tempY;
                        }
                        //如果当前状态不是正在刷新的状态,并且已经记录了y坐标
                        if (state!=REFRESHING && isRecord ) {
                            //计算y的偏移量
                            offsetY = tempY - startY;
                            //计算当前滑动的高度
                            float currentHeight = (-headViewHeight+offsetY/3);
                            //用当前滑动的高度和头部headerView的总高度进行比 计算出当前滑动的百分比 0到1
                            float currentProgress = 1+currentHeight/headViewHeight;
                            //如果当前百分比大于1了,将其设置为1,目的是让第一个状态的椭圆不再继续变大
                            if (currentProgress>=1) {
                                currentProgress = 1;
                            }
                            //如果当前的状态是放开刷新,并且已经记录y坐标
                            if (state == RELEASE_TO_REFRESH && isRecord) {

                                setSelection(0);
                                //如果当前滑动的距离小于headerView的总高度
                                if (-headViewHeight+offsetY/RATIO<0) {
                                    //将状态置为下拉刷新状态
                                    state = PULL_TO_REFRESH;
                                    //根据状态改变headerView,主要是更新动画和文字等信息
                                    changeHeaderByState(state);
                                    //如果当前y的位移值小于0,即为headerView隐藏了
                                }else if (offsetY<=0) {
                                    //将状态变为done
                                    state = DONE;
                                    stopAnim();
                                    //根据状态改变headerView,主要是更新动画和文字等信息
                                    changeHeaderByState(state);
                                }
                            }
                            //如果当前状态为下拉刷新并且已经记录y坐标
                            if (state == PULL_TO_REFRESH && isRecord) {
                                setSelection(0);
                                //如果下拉距离大于等于headerView的总高度
                                if (-headViewHeight+offsetY/RATIO>=0) {
                                    //将状态变为放开刷新
                                    state = RELEASE_TO_REFRESH;
                                    //根据状态改变headerView,主要是更新动画和文字等信息
                                    changeHeaderByState(state);
                                    //如果当前y的位移值小于0,即为headerView隐藏了
                                }else if (offsetY<=0) {
                                    //将状态变为done
                                    state = DONE;
                                    //根据状态改变headerView,主要是更新动画和文字等信息
                                    changeHeaderByState(state);
                                }
                            }
                            //如果当前状态为done并且已经记录y坐标
                            if (state == DONE && isRecord) {
                                //如果位移值大于0
                                if (offsetY>=0) {
                                    //将状态改为下拉刷新状态
                                    state = PULL_TO_REFRESH;
                                    changeHeaderByState(state);
                                }
                            }
                            //如果为下拉刷新状态
                            if (state == PULL_TO_REFRESH) {
                                //则改变headerView的padding来实现下拉的效果
                                headView.setPadding(0,(int)(-headViewHeight+offsetY/RATIO) ,0,0);
                            }
                            //如果为放开刷新状态
                            if (state == RELEASE_TO_REFRESH) {
                                //改变headerView的padding值
                                headView.setPadding(0,(int)(-headViewHeight+offsetY/RATIO) ,0, 0);
                            }
                        }
                        break;
                    //当用户手指抬起时
                    case MotionEvent.ACTION_UP:
                        //如果当前状态为下拉刷新状态
                        if (state == PULL_TO_REFRESH) {
                            //平滑的隐藏headerView
                            this.smoothScrollBy((int)(-headViewHeight+offsetY/RATIO)+headViewHeight, 500);
                            //根据状态改变headerView
                            changeHeaderByState(state);
                        }
                        //如果当前状态为放开刷新
                        if (state == RELEASE_TO_REFRESH) {
                            //平滑的滑到正好显示headerView
                            this.smoothScrollBy((int)(-headViewHeight+offsetY/RATIO), 500);
                            //将当前状态设置为正在刷新
                            state = REFRESHING;
                            //回调接口的onRefresh方法
                            mOnRefreshListener.onRefresh();
                            //根据状态改变headerView
                            changeHeaderByState(state);
                        }
                        //这一套手势执行完,一定别忘了将记录y坐标的isRecord改为false,以便于下一次手势的执行
                        isRecord = false;
                        break;
                }

            }
        }
        return super.onTouchEvent(ev);
    }

    /**
     * 根据状态改变headerView的动画和文字显示
     * @param state
     */
    private void changeHeaderByState(int state){
        switch (state) {
            case DONE://如果的隐藏的状态
                //设置headerView的padding为隐藏
                headView.setPadding(0, -headViewHeight, 0, 0);
                startAnim();
                break;
            case RELEASE_TO_REFRESH://当前状态为放开刷新
                break;
            case PULL_TO_REFRESH://当前状态为下拉刷新
                startAnim();
                break;
            case REFRESHING://当前状态为正在刷新
                break;
            default:
                break;
        }
    }

    /**
     * 测量View
     * @param child
     */
    private void measureView(View child) {
        ViewGroup.LayoutParams p = child.getLayoutParams();
        if (p == null) {
            p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT);
        }
        int childWidthSpec = ViewGroup.getChildMeasureSpec(0, 0 + 0, p.width);
        int lpHeight = p.height;
        int childHeightSpec;
        if (lpHeight > 0) {
            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight,
                    MeasureSpec.EXACTLY);
        } else {
            childHeightSpec = MeasureSpec.makeMeasureSpec(0,
                    MeasureSpec.UNSPECIFIED);
        }
        child.measure(childWidthSpec, childHeightSpec);
    }

    /**
     * 开启动画
     */
    public void startAnim(){
        ivBack1.startAnimation(backAnimation1);
        ivBack2.startAnimation(backAnimation2);
        ivSun.startAnimation(sunAnimation);
        ivWheel1.startAnimation(wheelAnimation);
        ivWheel2.startAnimation(wheelAnimation);
    }

    /**
     * 关闭动画
     */
    public void stopAnim(){
        ivBack1.clearAnimation();
        ivBack2.clearAnimation();
        ivSun.clearAnimation();
        ivWheel1.clearAnimation();
        ivWheel2.clearAnimation();
    }
}

好了,自定义下拉刷新动画我们就实现了,其实很简单,所有的下拉刷新动画都类似这样实现的。源码我已经上传到Github上了:

https://github.com/Hankkin/BaiduGoingRefreshLayout

求star啊。有不合理的地方还希望大家多多指正,共同进步哈。

时间: 2024-08-01 10:43:47

Android自定义下拉刷新动画--仿百度外卖下拉刷新的相关文章

【Android自定义View实战】之仿百度加载动画,一种优雅的Loading方式

转载请注明出处:http://blog.csdn.net/linglongxin24/article/details/53470872 本文出自[DylanAndroid的博客] Android自定义View实战之仿百度加载动画一种优雅的Loading方式 第一个仿百度加载动画用ObjectAnimator属性动画操作ImageView的属性方法实现 第二个仿百度加载动画第二种实现方式用ValueAnimator原生的ondraw方法实现 第三个扔球动画-水平旋转动画 第四个扔球动画-垂直旋转动

iOS-Andriod百度地图仿百度外卖-饿了么-选择我的地址-POI检索/

http://zanderzhang.gitcafe.io/2015/09/19/iOS-Andriod百度地图仿百度外卖-饿了么-选择我的地址-POI检索/ 百度外卖选择送货地址: 饿了么选择送货地址: 百度地图api官网 第一个图,就是放一个UIImageView在MapView的中间,然后我们拖动的时候下面地图在跑. -(void)addMiddleImage{ UIImageView *imaV=[UIImageView new]; imaV.center=_mapView.center

android自定义View之(六)------高仿华为荣耀3C的圆形刻度比例图(ShowPercentView)

为什么写这篇文章: 显示当前的容量所占的比例,表现当前计划的进度,一般都会采用百分比的方式,而图形显示,以其一目了然的直观性和赏心悦目的美观形成为了我们的当然的首选. 在图形表示百分比的方法中,我们有用画圆的圆形进度条的方法<<android自定义View之(二)------简单进度条显示样例篇>>,也有用画弧形的进度条的方法<<android自定义View之(三)------视频音量调控样例>>,今天看到华为荣耀3C的一个界面: 个人觉得这个表示比例的圆形

【Android自定义View实战】之仿QQ运动步数圆弧及动画,Dylan计步中的控件StepArcView

转载请注明出处:http://blog.csdn.net/linglongxin24/article/details/52936609[DylanAndroid的csdn博客] 在之前的Android超精准计步器开发-Dylan计步中的首页用到了一个自定义控件,和QQ运动的界面有点类似,还有动画效果,下面就来讲一下这个View是如何绘制的. 1.先看效果图 2.效果图分析 功能说明:黄色的代表用户设置的总计划锻炼步数,红色的代表用户当前所走的步数. 初步分析:完全自定义View重写onDraw(

jquery 仿百度搜索下拉框的插件

转载地址:http://www.open-open.com/lib/view/open1420624048437.html 今天写了个下拉插件分享出来 效果: , 可以搜素,也可以使用上下键选择匹配出来的值 css 1 .select4_box { 2 border: 1px solid #5897fb; 3 position: absolute; 4 width: 250px; 5 background: #fff; 6 border-radius: 4px; 7 -webkit-box-sh

模糊匹配仿百度自动下拉提示

实际项目中,我们可以把数据获取改成ajax动态获取,在 getContent()中 <!DOCTYPE> <html> <head> <title>js/jQuery实现类似百度搜索功能</title> <meta name="Author" content="Michael"> <meta name="Keywords" content="js/jQuery

android自定义GifView显示gif动画

gif动画在web开发中使用的非常的多,利用gif,许多动画不必再用程序编写,现在有非常多的App已经使用到了gif动画,可是android sdk并没有为我们提供gif这种View,所以我们只能自定义一个View,去实现gif效果. android虽然没有为我们提供现成的GifView,但是为我们提供了Movie类,这个类就是用来实现GifView的关键类.它主要有两个最重要的方法,一个是根据动画播放时间设置当前要现显示的帧,二是将当前要显示的帧绘制到画布中.下面看一下效果图: 点击暂停按钮还

Android 自定义录音、播放动画View,让你的录音浪起来

最近公司项目有一个录音的录制和播放动画需求,然后时间是那么紧,那么赶紧开撸. 先看效果图 嗯,然后大致就是这样,按住录音,然后有一个倒计时,最外层一个进度条,还有一个类似模拟声波的动画效果(其实中间的波浪会根据声音的大小浪起来的~) 2 实现思路 然后,我们适当的来分析一下这个录音动画的实现方式.这个肯定是通过自定义控件,咱们来把这个效果完完全全画出来. 大致包括以下几个点: 1. 最外层的进度条,最坑的就是一开始的一个渐变的效果 2. 然后进度条最前方是有一个点的(我肯定选择用图片来实现) 3

转 android Listview,gridview局部刷新,部分刷新 (仿QQ动态评论局部刷新)

转自:http://blog.csdn.net/jdsjlzx/article/details/45582719 众所周知Listview和Gridview的刷新界面的方式是调用adapter.notifyDataSetChanged()进行界面刷新. 但是此方法有其弊端,他是将界面中的数据全部刷新一遍,不论数据有没有变化.如果listview加载了很多的数据(如:100条) 在进行刷新时就会造成很大的系统开销如何像qq空间个人动态那样点赞只刷新一条呢: 主要原理: 对listview的某一个i