1. 程式人生 > >SmartRefreshLayout 仿美團下拉重新整理

SmartRefreshLayout 仿美團下拉重新整理

先上圖:

一、分析

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

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

二、反編譯app看實現原理

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

大部分圖片都放在 res/drawable-xhdpi-v4res/drawable-xxhdpi-v4 兩個資料夾內,

仔細找下能看到多張連續的 loading 圖片。

看到圖片後知道原來它用的是最普通的幀動畫拿到資源圖片,知道了實現原理

三、實現動畫效果

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

/**
 * @author Amarao
 * @date 2018/9/12
 * 下拉重新整理動畫
 * 呼叫舉例:smartRefresh.setRefreshHeader(new CustomRefreshHeader(this));
 */
public class CustomRefreshHeader extends LinearLayout implements RefreshHeader {

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

    private boolean hasSetPullDownAnim = false;

    public CustomRefreshHeader(Context context) {
        super(context,null,0);
        View view = View.inflate(context, R.layout.custom_refresh_header,this);
        mImage = (ImageView) view.findViewById(R.id.hot_iv_refresh_header);
    }

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

    public CustomRefreshHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        View view = View.inflate(context, R.layout.custom_refresh_header,this);
        mImage = (ImageView) view.findViewById(R.id.hot_iv_refresh_header);
        LogUtils.e("CustomRefreshHeader3");
    }

    /**
     * 獲取真實檢視(必須返回,不能為null)
     */
    @NonNull
    @Override
    public View getView() {
        return this;
    }

    /**
     * 獲取變換方式(必須指定一個:平移、拉伸、固定、全屏)
     */
    @NonNull
    @Override
    public SpinnerStyle getSpinnerStyle() {
        return SpinnerStyle.Translate;
    }

    /**
     * 設定主題顏色 (如果自定義的Header沒有注意顏色,本方法可以什麼都不處理)
     * @param colors 對應Xml中配置的 srlPrimaryColor srlAccentColor
     */
    @Override
    public void setPrimaryColors(int... colors) {

    }


    /**
     * 尺寸定義初始化完成 (如果高度不改變(程式碼修改:setHeader),只調用一次, 在RefreshLayout#onMeasure中呼叫)
     * @param kernel RefreshKernel 核心介面(用於完成高階Header功能)
     * @param height HeaderHeight or FooterHeight
     * @param maxDragHeight 最大拖動高度
     */
    @Override
    public void onInitialized(@NonNull RefreshKernel kernel, int height, int maxDragHeight) {

    }

    /**
     * 開始動畫(開始重新整理或者開始載入動畫)
     * @param refreshLayout RefreshLayout
     * @param height HeaderHeight or FooterHeight
     * @param maxDragHeight 最大拖動高度
     */
    @Override
    public void onStartAnimator(@NonNull RefreshLayout refreshLayout, int height, int maxDragHeight) {

    }

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

                break;
        }
    }

    /**
     * 手指拖動下拉(會連續多次呼叫,新增isDragging並取代之前的onPulling、onReleasing)
     * @param isDragging true 手指正在拖動 false 回彈動畫
     * @param percent 下拉的百分比 值 = offset/footerHeight (0 - percent - (footerHeight+maxDragHeight) / footerHeight )
     * @param offset 下拉的畫素偏移量  0 - offset - (footerHeight+maxDragHeight)
     * @param height 高度 HeaderHeight or FooterHeight
     * @param maxDragHeight 最大拖動高度
     */
    @Override
    public void onMoving(boolean isDragging, float percent, int offset, int height, int maxDragHeight) {
        //LogUtils.e("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;
            }
        }
    }

    /**
     * 動畫結束
     * @param refreshLayout RefreshLayout
     * @param success 資料是否成功重新整理或載入
     * @return 完成動畫所需時間 如果返回 Integer.MAX_VALUE 將取消本次完成事件,繼續保持原有狀態
     */
    @Override
    public int onFinish(@NonNull RefreshLayout refreshLayout, boolean success) {
        // 結束動畫
        if (pullDownAnim != null && pullDownAnim.isRunning()) {
            pullDownAnim.stop();
        }
        if (refreshingAnim != null && refreshingAnim.isRunning()) {
            refreshingAnim.stop();
        }
        //重置狀態
        hasSetPullDownAnim = false;

        /*if (success ){
            LogUtils.e("TRUE");
        }else {
            LogUtils.e("FALSE");
        }*/
        return 0;
    }

    /**
     * 釋放時刻(呼叫一次,將會觸發載入)
     * @param refreshLayout RefreshLayout
     * @param height 高度 HeaderHeight or FooterHeight
     * @param maxDragHeight 最大拖動高度
     */
    @Override
    public void onReleased(@NonNull RefreshLayout refreshLayout, int height, int maxDragHeight) {

    }

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

    }

    /**
     * 是否支援水平方向的拖動(將會影響到onHorizontalDrag的呼叫)
     * @return 水平拖動需要消耗更多的時間和資源,所以如果不支援請返回false
     */
    @Override
    public boolean isSupportHorizontalDrag() {
        return false;
    }

}

邏輯主要在 onStateChanged()onMoving() 方法裡,程式碼中註釋寫的很詳細。 切換狀態原理是每次都給 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>