SmartRefreshLayout lottie 打造自己的刷新动画

前言:

看着自己简书创建时间2016年,CSDN2015年。自己在这最好的年华居然连技术文章或者论文都没发表过,怎么对的起我看过前辈们的心血和付出。在此我还是决定了今后的总结方向不在是单一的笔记和书本,还是为IT大军做一份贡献。

正文:

写这篇文章主要是为了当前日益增多三方库和开发中的一些日常造轮子,加自己的经验总结

效果图:

效果图.gif

库:

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="测试代码"
            android:layout_height="wrap_content" />
    </com.scwang.smartrefresh.layout.SmartRefreshLayout>
</LinearLayout>

简单的布局和自定义的HeadRefresh

DesginLottieHeadRefresh:

public class DesginLottieHeadRefresh extends ViewGroup implements RefreshHeader {
    private LottieAnimationView lav;
    private String asset_loading_json = "desgin/newAnimation.json";
    //中心点
    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) {
        }
    }

实现步骤:

1.implements RefreshHead View或者ViewGroup
2.实现 onMeasure-onLayout 或者实现onDraw(Canvas canvas)
3.Lottiview 加载assets目录下面 .json动画完成初步显示
4.根据Lottie和SmartRefreshLayout api 完成动画连贯和状态更新

RefreshHead > RefreshInternal

 /**
     * 获取实体视图
     * @return 实体视图
     */
    @NonNull
    View getView();

    /**
     * 获取变换方式 {@link SpinnerStyle} 必须返回 非空
     * @return 变换方式
     */
    @NonNull
    SpinnerStyle getSpinnerStyle();

    /**
     * 设置主题颜色
     * @param colors 对应Xml中配置的 srlPrimaryColor srlAccentColor
     */
    void setPrimaryColors(@ColorInt int... colors);

    /**
     * 尺寸定义完成 (如果高度不改变(代码修改:setHeader),只调用一次, 在RefreshLayout#onMeasure中调用)
     * @param kernel RefreshKernel
     * @param height HeaderHeight or FooterHeight
     * @param extendHeight extendHeaderHeight or extendFooterHeight
     */
    void onInitialized(@NonNull RefreshKernel kernel, int height, int extendHeight);
    /**
     * 手指拖动下拉(会连续多次调用)
     * @param percent 下拉的百分比 值 = offset/footerHeight (0 - percent - (footerHeight+extendHeight) / footerHeight )
     * @param offset 下拉的像素偏移量  0 - offset - (footerHeight+extendHeight)
     * @param height 高度 HeaderHeight or FooterHeight
     * @param extendHeight 扩展高度  extendHeaderHeight or extendFooterHeight
     */
    void onPulling(float percent, int offset, int height, int extendHeight);
    /**
     * 手指释放之后的持续动画(会连续多次调用)
     * @param percent 下拉的百分比 值 = offset/footerHeight (0 - percent - (footerHeight+extendHeight) / footerHeight )
     * @param offset 下拉的像素偏移量  0 - offset - (footerHeight+extendHeight)
     * @param height 高度 HeaderHeight or FooterHeight
     * @param extendHeight 扩展高度  extendHeaderHeight or extendFooterHeight
     */
    void onReleasing(float percent, int offset, int height, int extendHeight);

    /**
     * 释放时刻(调用一次,将会触发加载)
     * @param refreshLayout RefreshLayout
     * @param height 高度 HeaderHeight or FooterHeight
     * @param extendHeight 扩展高度  extendHeaderHeight or extendFooterHeight
     */
    void onReleased(RefreshLayout refreshLayout, int height, int extendHeight);

    /**
     * 开始动画
     * @param refreshLayout RefreshLayout
     * @param height HeaderHeight or FooterHeight
     * @param extendHeight extendHeaderHeight or extendFooterHeight
     */
    void onStartAnimator(@NonNull RefreshLayout refreshLayout, int height, int extendHeight);

    /**
     * 动画结束
     * @param refreshLayout RefreshLayout
     * @param success 数据是否成功刷新或加载
     * @return 完成动画所需时间 如果返回 Integer.MAX_VALUE 将取消本次完成事件,继续保持原有状态
     */
    int onFinish(@NonNull RefreshLayout refreshLayout, boolean success);

    /**
     * 水平方向的拖动
     * @param percentX 下拉时,手指水平坐标对屏幕的占比(0 - percentX - 1)
     * @param offsetX 下拉时,手指水平坐标对屏幕的偏移(0 - offsetX - LayoutWidth)
     * @param offsetMax 最大的偏移量
     */
    void onHorizontalDrag(float percentX, int offsetX, int offsetMax);

    /**
     * 是否支持水平方向的拖动(将会影响到onHorizontalDrag的调用)
     * @return 水平拖动需要消耗更多的时间和资源,所以如果不支持请返回false
     */
    boolean isSupportHorizontalDrag();

代码很简单,源码中有中文注解就不一一说明


LottileAnimation

Lottie官方使用手册

结合官网和部分源码很好实现Lottie在RefreshHead 中的实现 !!!

重点:

View,ViewGroup的生命周期和Wind上面的渲染过程,刚开始的时候去继承View拿到当前.json动画的宽高和在onMeasure中一直是0,0后来改为ViewGroup 子类重新自测measure宽和高得到的也是0,0。这下搞的我翻了波笔记本
笔记本mark入口

后来才决定改为CIRCLE_DIAMETER 和mCircleDiameter根据自己的分辨率和中心点来绘制动画.json的大小
(主要为了适配动画在不同分辨率手机里面的效果)

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

小伙伴也可以使用AT_MOST来根据父布局的指定获取当前自定义View的宽和高

OK到这里基本完善了Lottie动画能在Header里面指定的位置跳动了,接下run了一次发现动画确实是在Head里面跳动,但是因为设置了looper(true)的属性本身是不和Refresh onRefreshing 时间冲突,但是后面再次下拉刷新的时候出现了动画的帧数不是原来第一帧,这下纠结了,我一般不喜欢手动导入三方源码修改别人的源码主要以前被(XXX)坑哭过,升级一次,我基本要上重构一次我的项目。好在快速浏览了一遍LottieAnimationView的源码,好在和我猜测的一样 LottieDraw 和LottieAnimator 果然是根据Frame帧来实现动画的过程


LottieCom.png

那么现在来了不是给我机会为所欲为最大帧和最小帧 整个图片绘制过程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;
        }

配合上层接口对代码做了最后的处理
喜欢效果小伙伴可以在去关注SmartRefreshLayout refresh-heads 和fresh-foot代码的实现,其中Vector向量和对View,ViewGroup,Drawable绘制 是很不错的学习源码。
OK 效果做出来了


经验分享:

记得几年前做Android开发的时拿到第三方库或者框架很是头疼和烦躁,后面接触多了能心平气和的写代码,反而觉得开发过程在别人车轮下面还是相对容易的,api 知识体系清楚的情况下,功能实现反而很轻松! 时代在进步,人也在进步,学习是IT的必经之路,找准自己爱好坚持下去就行。
以后博主每周五分享一篇博客(Kotilin React-native Android Flutter Java)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容