给SwipeRefreshLayout换个免费的皮肤

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,最后

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

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

Guess you like

Origin juejin.im/post/6995834032458039332