Android從零擼美團(二) – 仿美團下拉重新整理自定義動畫

solocoder發表於2019-03-04

這是【從零擼美團】系列文章第二篇。
專案地址:github.com/cachecats/L…

Android從零擼美團(一) – 統一管理 Gradle 依賴 提取到單獨檔案中

Android從零擼美團(三) – Android多標籤tab滑動切換 – 自定義View快速實現高度定製封裝

Android從零擼美團(四) – 美團首頁佈局解析及實現 – Banner+自定義View+SmartRefreshLayout下拉重新整理上拉載入更多


今天寫了下拉重新整理,框架用的是 SmartRefreshLayout ,不為啥,因為
Github 上它有 9.5kstar,中文支援好節省時間。

先上圖:

在這裡插入圖片描述

一、分析

美團的下拉載入動畫初看挺簡單的,就一個賣萌的小人。細看的話還稍微有點複雜,一共有三個狀態。

  1. 剛開始下拉的時候,小腦袋從小變大的過程。
  2. 下拉到一定程度但還沒鬆手,小人翻了個跟頭直到完全出現。再往下拉保持最後完全出現的狀態。
  3. 鬆開後左右搖頭賣萌直至載入結束回彈回去。

這是三個動畫啊!真佩服這些大廠,簡單的載入動畫都搞這麼複雜。。

分析完過程該想怎麼實現了。

二、反編譯app看實現原理

最簡單直白的方法就是反編譯美團app,雖然看不到程式碼但資原始檔能還原出來,圖片和 xml 檔案完美還原。

在這裡插入圖片描述

反編譯工具是 apktool,使用方法官網上都有就不囉嗦了。

大部分圖片都放在 res/drawable-xhdpi-v4res/drawable-xxhdpi-v4 兩個資料夾內,仔細找下能看到多張連續的 loading 圖片。這裡給美團程式猿點個贊,檔案命名都很規範,很好找~

在這裡插入圖片描述
在這裡插入圖片描述

看到圖片後知道原來它用的是最普通的幀動畫啊,也不是太複雜。
拿到資源圖片,知道實現原理,就開工吧!

三、實現動畫效果

首先自定義View CustomRefreshHeader 繼承自 LinearLayout,並實現 SmartRefreshLayoutRefreshHeader 介面。
然後主要就是重寫 RefreshHeader 介面中的方法,裡面提供了下拉重新整理時不同階段的回撥,找到對應的方法碼程式碼就好。

public class CustomRefreshHeader extends LinearLayout implements RefreshHeader {

    private ImageView mImage;
    private AnimationDrawable pullDownAnim;
    private AnimationDrawable refreshingAnim;

    private boolean hasSetPullDownAnim = false;

    public CustomRefreshHeader(Context context) {
        this(context, null, 0);
    }

    public CustomRefreshHeader(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomRefreshHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        View view = View.inflate(context, R.layout.widget_custom_refresh_header, this);
        mImage = (ImageView) view.findViewById(R.id.iv_refresh_header);
    }

    @NonNull
    @Override
    public View getView() {
        return this;
    }

    @Override
    public SpinnerStyle getSpinnerStyle() {
        return SpinnerStyle.Translate;
    }

    @Override
    public void onStartAnimator(RefreshLayout layout, int height, int extendHeight) {

    }

    /**
     * 狀態改變時呼叫。在這裡切換第三階段的動畫賣萌小人
     * @param refreshLayout
     * @param oldState
     * @param newState
     */
    @Override
    public void onStateChanged(RefreshLayout refreshLayout, RefreshState oldState, RefreshState newState) {
        switch (newState) {
            case PullDownToRefresh: //下拉重新整理開始。正在下拉還沒鬆手時呼叫
                //每次重新下拉時,將圖片資源重置為小人的大腦袋
                mImage.setImageResource(R.drawable.commonui_pull_image);
                break;
            case Refreshing: //正在重新整理。只呼叫一次
                //狀態切換為正在重新整理狀態時,設定圖片資源為小人賣萌的動畫並開始執行
                mImage.setImageResource(R.drawable.anim_pull_refreshing);
                refreshingAnim = (AnimationDrawable) mImage.getDrawable();
                refreshingAnim.start();
                break;
            case ReleaseToRefresh:

                break;
        }
    }

    /**
     * 下拉過程中不斷呼叫此方法。第一階段從小變大的小人頭動畫,和第二階段翻跟頭動畫都在這裡設定
     */
    @Override
    public void onPullingDown(float percent, int offset, int headerHeight, int extendHeight) {
        Logger.d("percent: " + percent);

        // 下拉的百分比小於100%時,不斷呼叫 setScale 方法改變圖片大小
        if (percent < 1) {
            mImage.setScaleX(percent);
            mImage.setScaleY(percent);

            //是否執行過翻跟頭動畫的標記
            if (hasSetPullDownAnim) {
                hasSetPullDownAnim = false;
            }
        }

        //當下拉的高度達到Header高度100%時,開始載入正在下拉的初始動畫,即翻跟頭
        if (percent >= 1.0) {
            //因為這個方法是不停呼叫的,防止重複
            if (!hasSetPullDownAnim) {
                mImage.setImageResource(R.drawable.anim_pull_end);
                pullDownAnim = (AnimationDrawable) mImage.getDrawable();
                pullDownAnim.start();

                hasSetPullDownAnim = true;
            }
        }
    }

    /**
     * 動畫結束後呼叫
     */
    @Override
    public int onFinish(RefreshLayout layout, boolean success) {
        // 結束動畫
        if (pullDownAnim != null && pullDownAnim.isRunning()) {
            pullDownAnim.stop();
        }
        if (refreshingAnim != null && refreshingAnim.isRunning()) {
            refreshingAnim.stop();
        }
        //重置狀態
        hasSetPullDownAnim = false;
        return 0;
    }

    @Override
    public void onReleasing(float percent, int offset, int headerHeight, int extendHeight) {

    }

    @Override
    public void onRefreshReleased(RefreshLayout layout, int headerHeight, int extendHeight) {

    }

    @Override
    public void setPrimaryColors(int... colors) {

    }

    @Override
    public void onInitialized(RefreshKernel kernel, int height, int extendHeight) {

    }

    @Override
    public void onHorizontalDrag(float percentX, int offsetX, int offsetMax) {

    }

    @Override
    public boolean isSupportHorizontalDrag() {
        return false;
    }
}
複製程式碼

邏輯主要在 onStateChanged()onPullingDown() 方法裡,程式碼中註釋寫的很詳細。
切換狀態原理是每次都給 ImageView 設定對應的資源圖片或動畫檔案,然後得到 AnimationDrawable 開啟動畫,如下:

mImage.setImageResource(R.drawable.anim_pull_end);
pullDownAnim = (AnimationDrawable) mImage.getDrawable();
pullDownAnim.start();
複製程式碼

程式碼中呼叫:

smartRefreshLayout.setRefreshHeader(new CustomRefreshHeader(getActivity()));
        smartRefreshLayout.setOnRefreshLoadmoreListener(new OnRefreshLoadmoreListener() {
            @Override
            public void onLoadmore(RefreshLayout refreshlayout) {
                Logger.d("onLoadmore");
                smartRefreshLayout.finishLoadmore(2000, true);
            }

            @Override
            public void onRefresh(RefreshLayout refreshlayout) {
                Logger.d("onRefresh");
                smartRefreshLayout.finishRefresh(2000, true);
            }
        });
複製程式碼

貼出資源佈局檔案:
widget_custom_refresh_header.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/white"
    android:gravity="center"
    android:padding="5dp">

    <ImageView
        android:id="@+id/iv_refresh_header"
        android:layout_width="41dp"
        android:layout_height="54dp"
        android:scaleX="0"
        android:scaleY="0"
        android:translationY="0dp" />

</LinearLayout>
複製程式碼

anim_pull_end.xml

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="true">

    <item
        android:drawable="@drawable/commonui_pull_end_image_frame_01"
        android:duration="100" />

    <item
        android:drawable="@drawable/commonui_pull_end_image_frame_02"
        android:duration="100" />

    <item
        android:drawable="@drawable/commonui_pull_end_image_frame_03"
        android:duration="100" />

    <item
        android:drawable="@drawable/commonui_pull_end_image_frame_04"
        android:duration="100" />

    <item
        android:drawable="@drawable/commonui_pull_end_image_frame_05"
        android:duration="100" />

</animation-list>
複製程式碼

anim_pull_refreshing.xml

<?xml version="1.0" encoding="utf-8"?>
<animation-list android:oneshot="false"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:duration="50" android:drawable="@drawable/commonui_refreshing_image_frame_01" />
    <item android:duration="50" android:drawable="@drawable/commonui_refreshing_image_frame_02" />
    <item android:duration="50" android:drawable="@drawable/commonui_refreshing_image_frame_03" />
    <item android:duration="50" android:drawable="@drawable/commonui_refreshing_image_frame_02" />
    <item android:duration="50" android:drawable="@drawable/commonui_refreshing_image_frame_05" />
    <item android:duration="50" android:drawable="@drawable/commonui_refreshing_image_frame_06" />
    <item android:duration="50" android:drawable="@drawable/commonui_refreshing_image_frame_07" />
    <item android:duration="50" android:drawable="@drawable/commonui_refreshing_image_frame_06" />
</animation-list>
複製程式碼

好啦,以上就是仿美團下拉重新整理自定義動畫的實現過程。
原始碼地址:github.com/cachecats/L…

相關文章