Android自定義下拉刷新動畫--仿百度外賣下拉刷新

好久沒寫博客了,小編之前一段時間一直在找工作,從天津來到了我們的大帝都,感覺還不錯。好了廢話不多說了,開始我們今天的主題吧。現如今的APP各式各樣,同樣也帶來了各種需求,一個下拉刷新都能玩出花樣了,前兩天訂飯的時候不經意間看到了“百度外賣”的下拉刷新,今天的主題就是它--自定義下拉刷新動畫

看一下實現效果吧:

20160411115612150.gif

動畫

我們先來看看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啊。有不合理的地方還希望大家多多指正,共同進步哈。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,264評論 25 708
  • 內容抽屜菜單ListViewWebViewSwitchButton按鈕點贊按鈕進度條TabLayout圖標下拉刷新...
    皇小弟閱讀 46,884評論 22 665
  • 看到阿桑分享的又受到觸動,沒有想到會有這么相似木馬程序的伙伴。好像連鎖反應,因為某個問題,引起了各種木馬程序。依賴...
    aseeya閱讀 239評論 0 0
  • 我們都知道混合型的肌膚他就是T區比較油兩頰比較干。但是也有一些,常常會遇到一些混合型的肌膚他會很奇怪。他明明t區會...
    Rbaobao閱讀 878評論 0 0
  • 01 前陣子一篇報道的標題亮了:“殺害金正男的特工身份曝光,疑似是1988年中年女子”——據說,無數80后女子看到...
    觀觀之洲閱讀 460評論 -3 4