给SwipeRefreshLayout换个免费的皮肤

![origin.gif](https://upload-images.jianshu.io/upload_images/5928293-6adbc13b242e4af1.gif?imageMogr2/auto-orient/strip)

SwipeRefreshLayout是Androidx提供了提供的下拉刷新组件,具体如何使用就不说了,相信大家也都经常用。

1,效果

首先看一下SwipeRefreshLayout的默认效果:

origin.gif

为了不耽误你的时间,先看一下最终效果:

GIF.gif

2,常用方法

方法 解释
setColorSchemeResources(int…colorReslds) 设置下拉进度条的颜色主题,参数可变,并且是资源id,最多设置四种不同的颜色。
setProgressBackgroundSchemeResource(int coloRes) 设置下拉进度条的背景颜色,默认白色。
isRefreshing() 判断当前的状态是否是刷新状态。
setOnRefreshListener(SwipeRefreshLayout.OnRefreshListener listener) 设置监听,需要重写onRefresh()方法,顶部下拉时会调用这个方法,在里面实现请求数据的逻辑,设置下拉进度条消失等等。
setRefreshing(boolean refreshing) 设置刷新状态,true表示正在刷新,false表示取消刷新。

尽管SwipeRefreshLayout提供了setColorSchemeResources()方法可以设置几个'圈圈'的颜色,但效果还是差强人意,作为高级代码搬运工的我实在看不下去了。

于是帮SwipeRefreshLayout换个免费的皮肤吧。

3,分析

SwipeRefreshLayout的代码很简单,就三个类

  • SwipeRefreshLayout

    就是我们在xml中使用的布局,根据情况拦截并消费手势事件,更新Loading视图的位置,控制其转动角度等,具体逻辑不再这里展开,有需要的大佬自己去看吧。

  • CircleImageView

    下拉时拉出来的圆圈

  • CircularProgressDrawable

    下拉及刷新时转动的圈圈

他们三个关系非常紧密,在SwipeRefreshLayout中创建了CircleImageViewCircularProgressDrawable,将CircularProgressDrawable设置给CircleImageView,然后又将CircleImageView添加到了SwipeRefreshLayout中,代码如下:

public class SwipeRefreshLayout {
    // 1,声明一个CircleImageView
    CircleImageView mCircleVew;
    // 1,声明一个CircularProgressDrawable
    CircularProgressDrawable mProgress;

    public SwipeRefreshLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        ...
        // 构造方法中初始化
        createProgressView();
        ...
    }

    private void createProgressView() {
        // 2,初始化mCircleView
        mCircleView = new CircleImageView(getContext());
        // 2,初始化mProgress
        mProgress = new CircularProgressDrawable(getContext());
        // 3,将mProgress设置为mCircleView的Drawable
        mCircleView.setImageDrawable(mProgress);
        mCircleView.setVisibility(View.GONE);
        // 4,添加mCircleView在当前ViewGroup中
        addView(mCircleView);
    }
}
结论

1,由于CircularProgressDrawable是控制这个'皮肤'的关键,因此需要先定义一个Drawable,并实现相应的效果。

2,由于SwipeRefreshLayout没有提供扩展的接口,无法直接使用自定义的Drawable,因此需要将SwipeRefreshLayout拷贝一份,修改其中的代码。

3,CircleImageView貌似是可以使用的,但是其中添加了一个半透明背景,无法通过设置的方式去除,因此也需要重写一下。

4,撸码

1,自定义LoadingDrawable

自定义一个Drawable并实现Animatable接口,在其中绘制图形及动画,具体如何实现可以参考LoadingDrawable,效果如下:

dialog.gif
2,重写SwipeRefreshLayout

SwipeRefreshLayout的代码拷贝一份,重命名为MySwipeRefreshLayout,如下:

public class MySwipeRefreshLayout extends ViewGroup implements NestedScrollingParent3,
        NestedScrollingParent2, NestedScrollingChild3, NestedScrollingChild2, NestedScrollingParent,
        NestedScrollingChild {
    ...
    CircularProgressDrawable mProgress;
    ...
}

只需要将其中的mProgress改为我们上面定义的LoadingDrawable即可,然后删除一些无用的代码即可。看一下效果:

loading0.gif

这样就有了我们想要的效果,但是发现动画中有一个圆形的阴影背景。分析一下,首先这个背景我不是我们定义的Drawable中的,那就看看Drawable所在的CircleImageView吧,代码如下:

class CircleImageView extends ImageView {
    ...
    CircleImageView(Context context) {
        super(context);
        // 创建一个OvalShape的ShapeDrawable
        ShapeDrawable circle = new ShapeDrawable(new OvalShape());
        // 绘制
        ViewCompat.setBackground(this, circle);
    }
    
    private static class OvalShadow extends OvalShape {
        ...
        @Override
        public void draw(Canvas canvas, Paint paint) {
            // 绘制圆形
            canvas.drawCircle(x, y, x, mShadowPaint);
            canvas.drawCircle(x, y, x - mShadowRadius, paint);
        }
    }
    
}   

可见这个背景是在CircleImageView中绘制的,由于CircleImageView没有提供对应的方法控制是否绘制背景,因此要想去掉这个背景就需要自定义一个CircleImageView

3,自定义CircleImageView

由于我们的目的是要去掉背景,因此这个就非常简单了,只需要把绘制背景的代码删掉即可:

public class MyCircleImageView extends androidx.appcompat.widget.AppCompatImageView {

    private Animation.AnimationListener mListener;

    public MyCircleImageView(Context context) {
        super(context);
    }
    ...

    public void setAnimationListener(Animation.AnimationListener listener) {
        mListener = listener;
    }

    @Override
    public void onAnimationStart() {
        super.onAnimationStart();
        if (mListener != null) {
            mListener.onAnimationStart(getAnimation());
        }
    }

    @Override
    public void onAnimationEnd() {
        super.onAnimationEnd();
        if (mListener != null) {
            mListener.onAnimationEnd(getAnimation());
        }
    }
}

然后将第二步创建的MySwipeRefreshLayout中的CircleImageView换成MyCircleImageView即可,直接看效果吧:

loading1.gif
4,进一步优化

SwipeRefreshLayout的换肤已经完成,但是感觉哪里总是不对劲,再往下拖动时让布局跟着一起动效果会不会更好呢?试试吧

思路也很简单,就是再下拉时根据拉动的距离让内部的子View ScrollTo到指定位置,最后松手时ScrollTo到原来的位置即可。

具体代码分两步:

1,跟着滑动

onTouchEvent()ACTION_MOVE事件中,如果时Dragged状态就会调用moveSpinner(overscrollTop),在其中添加代码即可:

private void moveSpinner(float overscrollTop) {
    float originalDragPercent = overscrollTop / mTotalDragDistance;

    float dragPercent = Math.min(1f, Math.abs(originalDragPercent));
    float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3;
    float extraOS = Math.abs(overscrollTop) - mTotalDragDistance;
    float slingshotDist = mCustomSlingshotDistance > 0
        ? mCustomSlingshotDistance
        : (mUsingCustomStart
           ? mSpinnerOffsetEnd - mOriginalOffsetTop
           : mSpinnerOffsetEnd);
    float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2)
                                             / slingshotDist);
    float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow(
        (tensionSlingshotPercent / 4), 2)) * 2f;
    float extraMove = slingshotDist * tensionPercent * 2;

    int targetY = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove);
    ...
    // 增加以下代码即可
    if (targetY >= 0) {
        ((ViewGroup) mTarget).getChildAt(0).scrollTo(0, -targetY / 2);
    }
}

只需要在targetY>=0时执行scrollTo()即可。

2,返回原处

onTouchEvent()ACTION_UP事件中,在Dragged状态松手时会执行finishSpinner(),在其中将子View回复到原位即可:

private void finishSpinner(float overscrollTop) {
    // 返回原处
    ((ViewGroup) mTarget).getChildAt(0).scrollTo(0, 0);
    ...
}

如果你觉得scrollTo(0, 0)太突兀,可以搞个属性动画让回弹速度变慢一点,这里就不多说了,看下效果吧:

GIF.gif

5,最后

如果你想进一步扩展,比如加上一些文字,最近更新时间等也是可以实现的。

最后安利一波我的开源项目:风云天气(https://github.com/wdsqjq/FengYunWeather),以上效果就在这里哦~

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

推荐阅读更多精彩内容