美文网首页UI
给SwipeRefreshLayout换个免费的皮肤

给SwipeRefreshLayout换个免费的皮肤

作者: Camellia666 | 来源:发表于2021-08-13 16:45 被阅读0次
    ![origin.gif](https://img.haomeiwen.com/i5928293/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),以上效果就在这里哦~

    相关文章

      网友评论

        本文标题:给SwipeRefreshLayout换个免费的皮肤

        本文链接:https://www.haomeiwen.com/subject/vnfjbltx.html