SmartRefreshLayout lottie 打造自己的刷新動(dòng)畫

前言:

看著自己簡(jiǎn)書創(chuàng)建時(shí)間2016年,CSDN2015年。自己在這最好的年華居然連技術(shù)文章或者論文都沒(méi)發(fā)表過(guò),怎么對(duì)的起我看過(guò)前輩們的心血和付出。在此我還是決定了今后的總結(jié)方向不在是單一的筆記和書本,還是為IT大軍做一份貢獻(xiàn)。

正文:

寫這篇文章主要是為了當(dāng)前日益增多三方庫(kù)和開(kāi)發(fā)中的一些日常造輪子,加自己的經(jīng)驗(yàn)總結(jié)

效果圖:

效果圖.gif

庫(kù):

SmartRefreshLayout
Lottie
LottieFiles

布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
 <com.scwang.smartrefresh.layout.SmartRefreshLayout
        android:id="@+id/sfl"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <com.example.xxx.lottie.DesginLottieHeadRefresh
            android:id="@+id/headRefresh"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
        <TextView
            android:id="@+id/text"
            android:layout_width="match_parent"
            android:gravity="center"
            android:text="測(cè)試代碼"
            android:layout_height="wrap_content" />
    </com.scwang.smartrefresh.layout.SmartRefreshLayout>
</LinearLayout>

簡(jiǎn)單的布局和自定義的HeadRefresh

DesginLottieHeadRefresh:

public class DesginLottieHeadRefresh extends ViewGroup implements RefreshHeader {
    private LottieAnimationView lav;
    private String asset_loading_json = "desgin/newAnimation.json";
    //中心點(diǎn)
    private int mCircleDiameter;
    @VisibleForTesting
    private static final int CIRCLE_DIAMETER = 160;
    private RefreshState mState;

    public DesginLottieHeadRefresh(Context context) {
        this(context, null);
    }

    public DesginLottieHeadRefresh(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView(context);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (getChildCount() == 0) {
            return;
        }
        final int width = getMeasuredWidth();
        int lottieWidth = lav.getMeasuredWidth();
        int lottieHeight = lav.getMeasuredHeight();
        int leftLav = width / 2 - lottieWidth / 2;
        int topLav = 0;
        lav.layout(leftLav, topLav, leftLav + lottieWidth, topLav + lottieHeight / 2);
    }

    private void initView(Context context) {
        final DisplayMetrics metrics = getResources().getDisplayMetrics();
        mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density);
        lav = new LottieAnimationView(context);
        lav.setAnimation(asset_loading_json);
        lav.loop(true);
        addView(lav);

    }

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

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

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

    }

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

    }

    @Override
    public void onMoving(boolean isDragging, float percent, int offset, int height, int extendHeight) {
    }

    @Override
    public void onReleased(@NonNull RefreshLayout refreshLayout, int height, int extendHeight) {
    }

    @Override
    public void onStartAnimator(@NonNull RefreshLayout refreshLayout, int height, int extendHeight) {
        lav.playAnimation();
    }

    @Override
    public int onFinish(@NonNull RefreshLayout refreshLayout, boolean success) {
//        lav.clearAnimation();
        if (lav != null) {
            lav.cancelAnimation();
            lav.clearAnimation();
        }
        return 0;
    }

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

    }

    @Override
    public boolean isSupportHorizontalDrag() {
        return false;
    }

    @Override
    public void onStateChanged(@NonNull RefreshLayout refreshLayout, @NonNull RefreshState oldState, @NonNull RefreshState newState) {
        mState = newState;
        switch (newState) {
            case None:
                lav.setFrame(0);
                lav.setProgress(0);
                break;
            case PullDownToRefresh:
                lav.setVisibility(View.VISIBLE);
                break;
            case PullDownCanceled:
                break;
            case ReleaseToRefresh:
                lav.setVisibility(View.VISIBLE);
                break;
            case Refreshing:
                break;
            case RefreshFinish:
                lav.setVisibility(View.GONE);
                break;
        }

    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
//        canvas.save();
//        lav.draw(canvas);
//        canvas.restore();
    }

    @Override
    public void invalidateDrawable(@NonNull Drawable drawable) {
        super.invalidateDrawable(drawable);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(getSize(widthMeasureSpec), getSize(heightMeasureSpec));
        lav.measure(MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY));
    }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        if (hasWindowFocus) {
        }
    }

實(shí)現(xiàn)步驟:

1.implements RefreshHead View或者ViewGroup
2.實(shí)現(xiàn) onMeasure-onLayout 或者實(shí)現(xiàn)onDraw(Canvas canvas)
3.Lottiview 加載assets目錄下面 .json動(dòng)畫完成初步顯示
4.根據(jù)Lottie和SmartRefreshLayout api 完成動(dòng)畫連貫和狀態(tài)更新

RefreshHead > RefreshInternal

 /**
     * 獲取實(shí)體視圖
     * @return 實(shí)體視圖
     */
    @NonNull
    View getView();

    /**
     * 獲取變換方式 {@link SpinnerStyle} 必須返回 非空
     * @return 變換方式
     */
    @NonNull
    SpinnerStyle getSpinnerStyle();

    /**
     * 設(shè)置主題顏色
     * @param colors 對(duì)應(yīng)Xml中配置的 srlPrimaryColor srlAccentColor
     */
    void setPrimaryColors(@ColorInt int... colors);

    /**
     * 尺寸定義完成 (如果高度不改變(代碼修改:setHeader),只調(diào)用一次, 在RefreshLayout#onMeasure中調(diào)用)
     * @param kernel RefreshKernel
     * @param height HeaderHeight or FooterHeight
     * @param extendHeight extendHeaderHeight or extendFooterHeight
     */
    void onInitialized(@NonNull RefreshKernel kernel, int height, int extendHeight);
    /**
     * 手指拖動(dòng)下拉(會(huì)連續(xù)多次調(diào)用)
     * @param percent 下拉的百分比 值 = offset/footerHeight (0 - percent - (footerHeight+extendHeight) / footerHeight )
     * @param offset 下拉的像素偏移量  0 - offset - (footerHeight+extendHeight)
     * @param height 高度 HeaderHeight or FooterHeight
     * @param extendHeight 擴(kuò)展高度  extendHeaderHeight or extendFooterHeight
     */
    void onPulling(float percent, int offset, int height, int extendHeight);
    /**
     * 手指釋放之后的持續(xù)動(dòng)畫(會(huì)連續(xù)多次調(diào)用)
     * @param percent 下拉的百分比 值 = offset/footerHeight (0 - percent - (footerHeight+extendHeight) / footerHeight )
     * @param offset 下拉的像素偏移量  0 - offset - (footerHeight+extendHeight)
     * @param height 高度 HeaderHeight or FooterHeight
     * @param extendHeight 擴(kuò)展高度  extendHeaderHeight or extendFooterHeight
     */
    void onReleasing(float percent, int offset, int height, int extendHeight);

    /**
     * 釋放時(shí)刻(調(diào)用一次,將會(huì)觸發(fā)加載)
     * @param refreshLayout RefreshLayout
     * @param height 高度 HeaderHeight or FooterHeight
     * @param extendHeight 擴(kuò)展高度  extendHeaderHeight or extendFooterHeight
     */
    void onReleased(RefreshLayout refreshLayout, int height, int extendHeight);

    /**
     * 開(kāi)始動(dòng)畫
     * @param refreshLayout RefreshLayout
     * @param height HeaderHeight or FooterHeight
     * @param extendHeight extendHeaderHeight or extendFooterHeight
     */
    void onStartAnimator(@NonNull RefreshLayout refreshLayout, int height, int extendHeight);

    /**
     * 動(dòng)畫結(jié)束
     * @param refreshLayout RefreshLayout
     * @param success 數(shù)據(jù)是否成功刷新或加載
     * @return 完成動(dòng)畫所需時(shí)間 如果返回 Integer.MAX_VALUE 將取消本次完成事件,繼續(xù)保持原有狀態(tài)
     */
    int onFinish(@NonNull RefreshLayout refreshLayout, boolean success);

    /**
     * 水平方向的拖動(dòng)
     * @param percentX 下拉時(shí),手指水平坐標(biāo)對(duì)屏幕的占比(0 - percentX - 1)
     * @param offsetX 下拉時(shí),手指水平坐標(biāo)對(duì)屏幕的偏移(0 - offsetX - LayoutWidth)
     * @param offsetMax 最大的偏移量
     */
    void onHorizontalDrag(float percentX, int offsetX, int offsetMax);

    /**
     * 是否支持水平方向的拖動(dòng)(將會(huì)影響到onHorizontalDrag的調(diào)用)
     * @return 水平拖動(dòng)需要消耗更多的時(shí)間和資源,所以如果不支持請(qǐng)返回false
     */
    boolean isSupportHorizontalDrag();

代碼很簡(jiǎn)單,源碼中有中文注解就不一一說(shuō)明


LottileAnimation

Lottie官方使用手冊(cè)

結(jié)合官網(wǎng)和部分源碼很好實(shí)現(xiàn)Lottie在RefreshHead 中的實(shí)現(xiàn) ?。?!

重點(diǎn):

View,ViewGroup的生命周期和Wind上面的渲染過(guò)程,剛開(kāi)始的時(shí)候去繼承View拿到當(dāng)前.json動(dòng)畫的寬高和在onMeasure中一直是0,0后來(lái)改為ViewGroup 子類重新自測(cè)measure寬和高得到的也是0,0。這下搞的我翻了波筆記本
筆記本mark入口

后來(lái)才決定改為CIRCLE_DIAMETER 和mCircleDiameter根據(jù)自己的分辨率和中心點(diǎn)來(lái)繪制動(dòng)畫.json的大小
(主要為了適配動(dòng)畫在不同分辨率手機(jī)里面的效果)

        final DisplayMetrics metrics = getResources().getDisplayMetrics();
        mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density);

小伙伴也可以使用AT_MOST來(lái)根據(jù)父布局的指定獲取當(dāng)前自定義View的寬和高

OK到這里基本完善了Lottie動(dòng)畫能在Header里面指定的位置跳動(dòng)了,接下run了一次發(fā)現(xiàn)動(dòng)畫確實(shí)是在Head里面跳動(dòng),但是因?yàn)樵O(shè)置了looper(true)的屬性本身是不和Refresh onRefreshing 時(shí)間沖突,但是后面再次下拉刷新的時(shí)候出現(xiàn)了動(dòng)畫的幀數(shù)不是原來(lái)第一幀,這下糾結(jié)了,我一般不喜歡手動(dòng)導(dǎo)入三方源碼修改別人的源碼主要以前被(XXX)坑哭過(guò),升級(jí)一次,我基本要上重構(gòu)一次我的項(xiàng)目。好在快速瀏覽了一遍L(zhǎng)ottieAnimationView的源碼,好在和我猜測(cè)的一樣 LottieDraw 和LottieAnimator 果然是根據(jù)Frame幀來(lái)實(shí)現(xiàn)動(dòng)畫的過(guò)程


LottieCom.png

那么現(xiàn)在來(lái)了不是給我機(jī)會(huì)為所欲為最大幀和最小幀 整個(gè)圖片繪制過(guò)程Progress等

 switch (newState) {
            case None:
                lav.setFrame(0);
                lav.setProgress(0);
                break;
            case PullDownToRefresh:
                lav.setVisibility(View.VISIBLE);
                break;
            case PullDownCanceled:
                break;
            case ReleaseToRefresh:
                lav.setVisibility(View.VISIBLE);
                break;
            case Refreshing:
                break;
            case RefreshFinish:
                lav.setVisibility(View.GONE);
                break;
        }

配合上層接口對(duì)代碼做了最后的處理
喜歡效果小伙伴可以在去關(guān)注SmartRefreshLayout refresh-heads 和fresh-foot代碼的實(shí)現(xiàn),其中Vector向量和對(duì)View,ViewGroup,Drawable繪制 是很不錯(cuò)的學(xué)習(xí)源碼。
OK 效果做出來(lái)了


經(jīng)驗(yàn)分享:

記得幾年前做Android開(kāi)發(fā)的時(shí)拿到第三方庫(kù)或者框架很是頭疼和煩躁,后面接觸多了能心平氣和的寫代碼,反而覺(jué)得開(kāi)發(fā)過(guò)程在別人車輪下面還是相對(duì)容易的,api 知識(shí)體系清楚的情況下,功能實(shí)現(xiàn)反而很輕松! 時(shí)代在進(jìn)步,人也在進(jìn)步,學(xué)習(xí)是IT的必經(jīng)之路,找準(zhǔn)自己愛(ài)好堅(jiān)持下去就行。
以后博主每周五分享一篇博客(Kotilin React-native Android Flutter Java)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容