查看原文
其他

给你的SwipeRefreshLayout换个免费的皮肤~

giswangsj 技术最TOP 2022-08-26

作者:giswangsj, 链接:https://juejin.cn/post/6995834032458039332

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

1、效果

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

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

2 、常用方法

尽管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,效果如下:
  • 2,重写SwipeRefreshLayoutSwipeRefreshLayout的代码拷贝一份,重命名为MySwipeRefreshLayout,如下:
public class MySwipeRefreshLayout extends ViewGroup implements NestedScrollingParent3,
        NestedScrollingParent2, NestedScrollingChild3, NestedScrollingChild2, NestedScrollingParent,
        NestedScrollingChild 
{
    ...
    CircularProgressDrawable mProgress;
    ...
}

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

这样就有了我们想要的效果,但是发现动画中有一个圆形的阴影背景。分析一下,首先这个背景我不是我们定义的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即可,直接看效果吧:

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 - .40) * 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(00);
 ...
}

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

5、最后

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

Github: https://github.com/wsj1024/FengYunWeather



---END---


推荐阅读:
MotionLayout动画从未如此简单!
官方推荐 Flow 取代 LiveData,有必要吗?
Kotlin诞生10周年——经典回顾
Android自如客APP裸眼3D效果的实现
【Kotlin协程】Channel 与 Flow 深入解析

更文不易,点个“在看”支持一下👇

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存