开源框架SmartRefreshLayout自定义上下拉刷新效果

1,426 阅读9分钟

下拉刷新

从框架自带的ClassicsHeader分析下拉刷新。

public class ClassicsHeader extends ClassicsAbstract<ClassicsHeader> implements RefreshHeader {}

继承自ClassicsAbstract类,并实现了RefreshHeader接口。我们先看看接口的继承关系。

public interface RefreshHeader extends RefreshComponent {}

RefreshHeader接口是个空实现,他继承了RefreshComponent接口。

public interface RefreshComponent extends OnStateChangedListener {}

RefreshComponent接口继承了OnStateChangedListener,用于监听刷新状态的改变。

/**
 * 刷新状态改变监听器
 */
public interface OnStateChangedListener {
    /**
     * 【仅限框架内调用】状态改变事件 {@link RefreshState}
     * @param refreshLayout RefreshLayout
     * @param oldState 改变之前的状态
     * @param newState 改变之后的状态
     */
    void onStateChanged(@NonNull RefreshLayout refreshLayout, @NonNull RefreshState oldState, @NonNull RefreshState newState);
}

我们接着看RefreshComponent接口。

public interface RefreshComponent extends OnStateChangedListener {
    /**
     * 获取实体视图
     * @return 实体视图
     */
    @NonNull
    View getView();

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

    /**
     * 【仅限框架内调用】设置主题颜色
     * @param colors 对应Xml中配置的 srlPrimaryColor srlAccentColor
     */
    @RestrictTo({LIBRARY,LIBRARY_GROUP,SUBCLASSES})
    void setPrimaryColors(@ColorInt int... colors);

    /**
     * 【仅限框架内调用】尺寸定义完成 (如果高度不改变(代码修改:setHeader),只调用一次, 在RefreshLayout#onMeasure中调用)
     * @param kernel RefreshKernel
     * @param height HeaderHeight or FooterHeight
     * @param maxDragHeight 最大拖动高度
     */
    @RestrictTo({LIBRARY,LIBRARY_GROUP,SUBCLASSES})
    void onInitialized(@NonNull RefreshKernel kernel, int height, int maxDragHeight);
    /**
     * 【仅限框架内调用】手指拖动下拉(会连续多次调用,添加isDragging并取代之前的onPulling、onReleasing)
     * @param isDragging true 手指正在拖动 false 回弹动画
     * @param percent 下拉的百分比 值 = offset/footerHeight (0 - percent - (footerHeight+maxDragHeight) / footerHeight )
     * @param offset 下拉的像素偏移量  0 - offset - (footerHeight+maxDragHeight)
     * @param height 高度 HeaderHeight or FooterHeight (offset 可以超过 height 此时 percent 大于 1)
     * @param maxDragHeight 最大拖动高度 offset 可以超过 height 参数 但是不会超过 maxDragHeight
     */
    @RestrictTo({LIBRARY,LIBRARY_GROUP,SUBCLASSES})
    void onMoving(boolean isDragging, float percent, int offset, int height, int maxDragHeight);

    /**
     * 【仅限框架内调用】释放时刻(调用一次,将会触发加载)
     * @param refreshLayout RefreshLayout
     * @param height 高度 HeaderHeight or FooterHeight
     * @param maxDragHeight 最大拖动高度
     */
    @RestrictTo({LIBRARY,LIBRARY_GROUP,SUBCLASSES})
    void onReleased(@NonNull RefreshLayout refreshLayout, int height, int maxDragHeight);

    /**
     * 【仅限框架内调用】开始动画
     * @param refreshLayout RefreshLayout
     * @param height HeaderHeight or FooterHeight
     * @param maxDragHeight 最大拖动高度
     */
    @RestrictTo({LIBRARY,LIBRARY_GROUP,SUBCLASSES})
    void onStartAnimator(@NonNull RefreshLayout refreshLayout, int height, int maxDragHeight);

    /**
     * 【仅限框架内调用】动画结束
     * @param refreshLayout RefreshLayout
     * @param success 数据是否成功刷新或加载
     * @return 完成动画所需时间 如果返回 Integer.MAX_VALUE 将取消本次完成事件,继续保持原有状态
     */
    @RestrictTo({LIBRARY,LIBRARY_GROUP,SUBCLASSES})
    int onFinish(@NonNull RefreshLayout refreshLayout, boolean success);

    /**
     * 【仅限框架内调用】水平方向的拖动
     * @param percentX 下拉时,手指水平坐标对屏幕的占比(0 - percentX - 1)
     * @param offsetX 下拉时,手指水平坐标对屏幕的偏移(0 - offsetX - LayoutWidth)
     * @param offsetMax 最大的偏移量
     */
    @RestrictTo({LIBRARY,LIBRARY_GROUP,SUBCLASSES})
    void onHorizontalDrag(float percentX, int offsetX, int offsetMax);

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

接口的继承关系看完了,接着看实现类。

public class ClassicsHeader extends ClassicsAbstract<ClassicsHeader> implements RefreshHeader {}

继承自ClassicsAbstract类。它是一个抽象类。继承自SimpleComponent。

public abstract class ClassicsAbstract<T extends ClassicsAbstract> extends SimpleComponent implements RefreshComponent {}

SimpleComponent很有趣。它是一个抽象类RelativeLayout的子类。

/**
 * Component 初步实现
 * 实现 Header 和 Footer 时,继承 ComponentAbstract 的话可以少写很多接口方法
 */
public abstract class SimpleComponent extends RelativeLayout implements RefreshComponent {}

在构造方法中传入了上拉刷新的布局,然后SimpleComponent就保存了对应的刷新View和刷新接口。

protected View mWrappedView;
protected SpinnerStyle mSpinnerStyle;
protected RefreshComponent mWrappedInternal;

protected SimpleComponent(@NonNull View wrapped) {
    this(wrapped, wrapped instanceof RefreshComponent ? (RefreshComponent) wrapped : null);
}

protected SimpleComponent(@NonNull View wrappedView, @Nullable RefreshComponent wrappedInternal) {
    super(wrappedView.getContext(), null, 0);
    this.mWrappedView = wrappedView;
    this.mWrappedInternal = wrappedInternal;
    if (this instanceof RefreshFooter && mWrappedInternal instanceof RefreshHeader && mWrappedInternal.getSpinnerStyle() == SpinnerStyle.MatchLayout) {
        wrappedInternal.getView().setScaleY(-1);
    } else if (this instanceof RefreshHeader && mWrappedInternal instanceof RefreshFooter && mWrappedInternal.getSpinnerStyle() == SpinnerStyle.MatchLayout) {
        wrappedInternal.getView().setScaleY(-1);
    }
}

protected SimpleComponent(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}

其他的就是实现了对应的接口方法,在对应的接口里面调用传入的mWrappedInternal的接口,相当于包装了一层,在调用mWrappedInternal方法前做了一些逻辑处理。

@NonNull
public View getView() {
    return mWrappedView == null ? this : mWrappedView;
}

@Override
public int onFinish(@NonNull RefreshLayout refreshLayout, boolean success) {
    if (mWrappedInternal != null && mWrappedInternal != this) {
        return mWrappedInternal.onFinish(refreshLayout, success);
    }
    return 0;
}

@Override
public void setPrimaryColors(@ColorInt int ... colors) {
    if (mWrappedInternal != null && mWrappedInternal != this) {
        mWrappedInternal.setPrimaryColors(colors);
    }
}

@NonNull
@Override
public SpinnerStyle getSpinnerStyle() {
    if (mSpinnerStyle != null) {
        return mSpinnerStyle;
    }
    if (mWrappedInternal != null && mWrappedInternal != this) {
        return mWrappedInternal.getSpinnerStyle();
    }
    if (mWrappedView != null) {
        ViewGroup.LayoutParams params = mWrappedView.getLayoutParams();
        if (params instanceof SmartRefreshLayout.LayoutParams) {
            mSpinnerStyle = ((SmartRefreshLayout.LayoutParams) params).spinnerStyle;
            if (mSpinnerStyle != null) {
                return mSpinnerStyle;
            }
        }
        if (params != null) {
            if (params.height == 0 || params.height == MATCH_PARENT) {
                for (SpinnerStyle style : SpinnerStyle.values) {
                    if (style.scale) {
                        return mSpinnerStyle = style;
                    }
                }
            }
        }
    }
    return mSpinnerStyle = SpinnerStyle.Translate;
}

@Override
public void onInitialized(@NonNull RefreshKernel kernel, int height, int maxDragHeight) {
    if (mWrappedInternal != null && mWrappedInternal != this) {
        mWrappedInternal.onInitialized(kernel, height, maxDragHeight);
    } else if (mWrappedView != null) {
        ViewGroup.LayoutParams params = mWrappedView.getLayoutParams();
        if (params instanceof SmartRefreshLayout.LayoutParams) {
            kernel.requestDrawBackgroundFor(this, ((SmartRefreshLayout.LayoutParams) params).backgroundColor);
        }
    }
}

@Override
public boolean isSupportHorizontalDrag() {
    return mWrappedInternal != null && mWrappedInternal != this && mWrappedInternal.isSupportHorizontalDrag();
}

@Override
public void onHorizontalDrag(float percentX, int offsetX, int offsetMax) {
    if (mWrappedInternal != null && mWrappedInternal != this) {
        mWrappedInternal.onHorizontalDrag(percentX, offsetX, offsetMax);
    }
}

@Override
public void onMoving(boolean isDragging, float percent, int offset, int height, int maxDragHeight) {
    if (mWrappedInternal != null && mWrappedInternal != this) {
        mWrappedInternal.onMoving(isDragging, percent, offset, height, maxDragHeight);
    }
}

@Override
public void onReleased(@NonNull RefreshLayout refreshLayout, int height, int maxDragHeight) {
    if (mWrappedInternal != null && mWrappedInternal != this) {
        mWrappedInternal.onReleased(refreshLayout, height, maxDragHeight);
    }
}

@Override
public void onStartAnimator(@NonNull RefreshLayout refreshLayout, int height, int maxDragHeight) {
    if (mWrappedInternal != null && mWrappedInternal != this) {
        mWrappedInternal.onStartAnimator(refreshLayout, height, maxDragHeight);
    }
}

@Override
public void onStateChanged(@NonNull RefreshLayout refreshLayout, @NonNull RefreshState oldState, @NonNull RefreshState newState) {
    if (mWrappedInternal != null && mWrappedInternal != this) {
        if (this instanceof RefreshFooter && mWrappedInternal instanceof RefreshHeader) {
            if (oldState.isFooter) {
                oldState = oldState.toHeader();
            }
            if (newState.isFooter) {
                newState = newState.toHeader();
            }
        } else if (this instanceof RefreshHeader && mWrappedInternal instanceof RefreshFooter) {
            if (oldState.isHeader) {
                oldState = oldState.toFooter();
            }
            if (newState.isHeader) {
                newState = newState.toFooter();
            }
        }
        final OnStateChangedListener listener = mWrappedInternal;
        if (listener != null) {
            listener.onStateChanged(refreshLayout, oldState, newState);
        }
    }
}

@SuppressLint("RestrictedApi")
public boolean setNoMoreData(boolean noMoreData) {
    return mWrappedInternal instanceof RefreshFooter && ((RefreshFooter) mWrappedInternal).setNoMoreData(noMoreData);
}

我们需要实现经典的刷新头,ClassicsAbstract继承了SimpleComponent。调用父类的构造方法之后,设置了SpinnerStyle的类型。

public ClassicsAbstract(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mSpinnerStyle = SpinnerStyle.Translate;
}

然后实现RefreshComponent接口。根据自己的刷新布局实现逻辑。

@Override
public void onInitialized(@NonNull RefreshKernel kernel, int height, int maxDragHeight) {
    mRefreshKernel = kernel;
    mRefreshKernel.requestDrawBackgroundFor(this, mBackgroundColor);
}

@Override
public void onStartAnimator(@NonNull RefreshLayout refreshLayout, int height, int maxDragHeight) {
    final View progressView = mProgressView;
    if (progressView.getVisibility() != VISIBLE) {
        progressView.setVisibility(VISIBLE);
        Drawable drawable = mProgressView.getDrawable();
        if (drawable instanceof Animatable) {
            ((Animatable) drawable).start();
        } else {
            progressView.animate().rotation(36000).setDuration(100000);
        }
    }
}

@Override
public void onReleased(@NonNull RefreshLayout refreshLayout, int height, int maxDragHeight) {
    onStartAnimator(refreshLayout, height, maxDragHeight);// 释放时开始动画
}

@Override
public int onFinish(@NonNull RefreshLayout refreshLayout, boolean success) {
    final View progressView = mProgressView;
    Drawable drawable = mProgressView.getDrawable();
    if (drawable instanceof Animatable) {
        if (((Animatable) drawable).isRunning()) {
            ((Animatable) drawable).stop();
        }
    } else {
        progressView.animate().rotation(0).setDuration(0);
    }
    progressView.setVisibility(GONE);
    return mFinishDuration;//延迟500毫秒之后再弹回
}

@Override
public void setPrimaryColors(@ColorInt int... colors) {
    if (colors.length > 0) {
        final View thisView = this;
        if (!(thisView.getBackground() instanceof BitmapDrawable) && !mSetPrimaryColor) {
            setPrimaryColor(colors[0]);
            mSetPrimaryColor = false;
        }
        if (!mSetAccentColor) {
            if (colors.length > 1) {
                setAccentColor(colors[1]);
            }
            mSetAccentColor = false;
        }
    }
}

@Override
public void setPrimaryColors(@ColorInt int... colors) {
    if (colors.length > 0) {
        final View thisView = this;
        if (!(thisView.getBackground() instanceof BitmapDrawable) && !mSetPrimaryColor) {
            setPrimaryColor(colors[0]);
            mSetPrimaryColor = false;
        }
        if (!mSetAccentColor) {
            if (colors.length > 1) {
                setAccentColor(colors[1]);
            }
            mSetAccentColor = false;
        }
    }
}

public T setPrimaryColor(@ColorInt int primaryColor) {
    mSetPrimaryColor = true;
    mBackgroundColor = primaryColor;
    if (mRefreshKernel != null) {
        mRefreshKernel.requestDrawBackgroundFor(this, primaryColor);
    }
    return self();
}

public T setAccentColor(@ColorInt int accentColor) {
    mSetAccentColor = true;
    mTitleText.setTextColor(accentColor);
    if (mArrowDrawable != null) {
        mArrowDrawable.setColor(accentColor);
        mArrowView.invalidateDrawable(mArrowDrawable);
    }
    if (mProgressDrawable != null) {
        mProgressDrawable.setColor(accentColor);
        mProgressView.invalidateDrawable(mProgressDrawable);
    }
    return self();
}

protected T self() {
    return (T) this;
}

接着就是ClassicsHeader实现类。通过自定义布局加载View。

public ClassicsHeader(Context context) {
    this(context, null);
}

public ClassicsHeader(Context context, AttributeSet attrs) {
    super(context, attrs, 0);

    View.inflate(context, R.layout.srl_classics_header, this);

    final View thisView = this;
    final View arrowView = mArrowView = thisView.findViewById(R.id.srl_classics_arrow);
    final View updateView = mLastUpdateText = thisView.findViewById(R.id.srl_classics_update);
    final View progressView = mProgressView = thisView.findViewById(R.id.srl_classics_progress);
    mTitleText = thisView.findViewById(R.id.srl_classics_title);
}

自定义布局。

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    tools:paddingTop="20dp"
    tools:paddingBottom="20dp"
    tools:background="#eee"
    tools:parentTag="android.widget.RelativeLayout">

    <ImageView
        android:id="@+id/srl_classics_arrow"
        android:layout_width="20dp"
        android:layout_height="20dp"
        android:layout_marginEnd="20dp"
        android:layout_marginRight="20dp"
        android:layout_centerVertical="true"
        android:layout_toLeftOf="@+id/srl_classics_center"
        android:layout_toStartOf="@+id/srl_classics_center"
        android:contentDescription="@android:string/untitled"
        />

    <ImageView
        android:id="@+id/srl_classics_progress"
        android:layout_width="20dp"
        android:layout_height="20dp"
        android:layout_marginEnd="20dp"
        android:layout_marginRight="20dp"
        android:layout_centerVertical="true"
        android:layout_toLeftOf="@+id/srl_classics_center"
        android:layout_toStartOf="@+id/srl_classics_center"
        android:contentDescription="@android:string/untitled"
        tools:tint="#666666"
        tools:src="@android:drawable/stat_notify_sync"/>

    <LinearLayout
        android:id="@+id/srl_classics_center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:gravity="center_horizontal"
        android:layout_centerInParent="true">
        <TextView
            android:id="@+id/srl_classics_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:maxLines="1"
            android:textColor="#666666"
            android:textSize="15sp"
            android:text="@string/srl_header_pulling"/>
        <TextView
            android:id="@+id/srl_classics_update"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:maxLines="1"
            android:textColor="#7c7c7c"
            android:textSize="12sp"
            android:text="@string/srl_header_update"/>
    </LinearLayout>

</merge>

因为涉及到自定义View,所以配置了自定义属性

<declare-styleable name="ClassicsHeader">
    <attr name="srlClassicsSpinnerStyle"/>
    <attr name="srlPrimaryColor"/>
    <attr name="srlAccentColor"/>
    <attr name="srlFinishDuration"/>
    <attr name="srlDrawableArrow"/>
    <attr name="srlDrawableProgress"/>
    <attr name="srlDrawableMarginRight"/>
    <attr name="srlDrawableSize"/>
    <attr name="srlDrawableArrowSize"/>
    <attr name="srlDrawableProgressSize"/>
    <attr name="srlTextSizeTitle"/>
    <attr name="srlTextSizeTime"/>
    <attr format="dimension" name="srlTextTimeMarginTop"/>
    <attr format="boolean" name="srlEnableLastTime"/>

    <attr name="srlTextPulling"/>
    <attr name="srlTextLoading"/>
    <attr name="srlTextRelease"/>
    <attr name="srlTextFinish"/>
    <attr name="srlTextFailed"/>
    <attr name="srlTextUpdate"/>
    <attr name="srlTextSecondary"/>
    <attr name="srlTextRefreshing"/>

</declare-styleable>

加载自定义属性。

public static String REFRESH_HEADER_PULLING = null;//"下拉可以刷新";
public static String REFRESH_HEADER_REFRESHING = null;//"正在刷新...";
public static String REFRESH_HEADER_LOADING = null;//"正在加载...";
public static String REFRESH_HEADER_RELEASE = null;//"释放立即刷新";
public static String REFRESH_HEADER_FINISH = null;//"刷新完成";
public static String REFRESH_HEADER_FAILED = null;//"刷新失败";
public static String REFRESH_HEADER_UPDATE = null;//"上次更新 M-d HH:mm";
public static String REFRESH_HEADER_SECONDARY = null;//"释放进入二楼";
//    public static String REFRESH_HEADER_UPDATE = "'Last update' M-d HH:mm";

protected String KEY_LAST_UPDATE_TIME = "LAST_UPDATE_TIME";

protected Date mLastTime;
protected TextView mLastUpdateText;
protected SharedPreferences mShared;
protected DateFormat mLastUpdateFormat;
protected boolean mEnableLastTime = true;

protected String mTextPulling;//"下拉可以刷新";
protected String mTextRefreshing;//"正在刷新...";
protected String mTextLoading;//"正在加载...";
protected String mTextRelease;//"释放立即刷新";
protected String mTextFinish;//"刷新完成";
protected String mTextFailed;//"刷新失败";
protected String mTextUpdate;//"上次更新 M-d HH:mm";
protected String mTextSecondary;//"释放进入二楼";

TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ClassicsHeader);

RelativeLayout.LayoutParams lpArrow = (RelativeLayout.LayoutParams) arrowView.getLayoutParams();
RelativeLayout.LayoutParams lpProgress = (RelativeLayout.LayoutParams) progressView.getLayoutParams();
LinearLayout.LayoutParams lpUpdateText = new LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
lpUpdateText.topMargin = ta.getDimensionPixelSize(R.styleable.ClassicsHeader_srlTextTimeMarginTop, SmartUtil.dp2px(0));
lpProgress.rightMargin = ta.getDimensionPixelSize(R.styleable.ClassicsHeader_srlDrawableMarginRight, SmartUtil.dp2px(20));
lpArrow.rightMargin = lpProgress.rightMargin;

lpArrow.width = ta.getLayoutDimension(R.styleable.ClassicsHeader_srlDrawableArrowSize, lpArrow.width);
lpArrow.height = ta.getLayoutDimension(R.styleable.ClassicsHeader_srlDrawableArrowSize, lpArrow.height);
lpProgress.width = ta.getLayoutDimension(R.styleable.ClassicsHeader_srlDrawableProgressSize, lpProgress.width);
lpProgress.height = ta.getLayoutDimension(R.styleable.ClassicsHeader_srlDrawableProgressSize, lpProgress.height);

lpArrow.width = ta.getLayoutDimension(R.styleable.ClassicsHeader_srlDrawableSize, lpArrow.width);
lpArrow.height = ta.getLayoutDimension(R.styleable.ClassicsHeader_srlDrawableSize, lpArrow.height);
lpProgress.width = ta.getLayoutDimension(R.styleable.ClassicsHeader_srlDrawableSize, lpProgress.width);
lpProgress.height = ta.getLayoutDimension(R.styleable.ClassicsHeader_srlDrawableSize, lpProgress.height);

mFinishDuration = ta.getInt(R.styleable.ClassicsHeader_srlFinishDuration, mFinishDuration);
mEnableLastTime = ta.getBoolean(R.styleable.ClassicsHeader_srlEnableLastTime, mEnableLastTime);
mSpinnerStyle = SpinnerStyle.values[ta.getInt(R.styleable.ClassicsHeader_srlClassicsSpinnerStyle,mSpinnerStyle.ordinal)];

if (ta.hasValue(R.styleable.ClassicsHeader_srlDrawableArrow)) {
    mArrowView.setImageDrawable(ta.getDrawable(R.styleable.ClassicsHeader_srlDrawableArrow));
} else if (mArrowView.getDrawable() == null){
    mArrowDrawable = new ArrowDrawable();
    mArrowDrawable.setColor(0xff666666);
    mArrowView.setImageDrawable(mArrowDrawable);
}

if (ta.hasValue(R.styleable.ClassicsHeader_srlDrawableProgress)) {
    mProgressView.setImageDrawable(ta.getDrawable(R.styleable.ClassicsHeader_srlDrawableProgress));
} else if (mProgressView.getDrawable() == null) {
    mProgressDrawable = new ProgressDrawable();
    mProgressDrawable.setColor(0xff666666);
    mProgressView.setImageDrawable(mProgressDrawable);
}

if (ta.hasValue(R.styleable.ClassicsHeader_srlTextSizeTitle)) {
    mTitleText.setTextSize(TypedValue.COMPLEX_UNIT_PX, ta.getDimensionPixelSize(R.styleable.ClassicsHeader_srlTextSizeTitle, SmartUtil.dp2px(16)));
//        } else {
//            mTitleText.setTextSize(16);
}

if (ta.hasValue(R.styleable.ClassicsHeader_srlTextSizeTime)) {
    mLastUpdateText.setTextSize(TypedValue.COMPLEX_UNIT_PX, ta.getDimensionPixelSize(R.styleable.ClassicsHeader_srlTextSizeTime, SmartUtil.dp2px(12)));
//        } else {
//            mLastUpdateText.setTextSize(12);
}

if (ta.hasValue(R.styleable.ClassicsHeader_srlPrimaryColor)) {
    super.setPrimaryColor(ta.getColor(R.styleable.ClassicsHeader_srlPrimaryColor, 0));
}
if (ta.hasValue(R.styleable.ClassicsHeader_srlAccentColor)) {
    setAccentColor(ta.getColor(R.styleable.ClassicsHeader_srlAccentColor, 0));
}

if(ta.hasValue(R.styleable.ClassicsHeader_srlTextPulling)){
    mTextPulling = ta.getString(R.styleable.ClassicsHeader_srlTextPulling);
} else if(REFRESH_HEADER_PULLING != null) {
    mTextPulling = REFRESH_HEADER_PULLING;
} else {
    mTextPulling = context.getString(R.string.srl_header_pulling);
}
if(ta.hasValue(R.styleable.ClassicsHeader_srlTextLoading)){
    mTextLoading = ta.getString(R.styleable.ClassicsHeader_srlTextLoading);
} else if(REFRESH_HEADER_LOADING != null) {
    mTextLoading = REFRESH_HEADER_LOADING;
} else {
    mTextLoading = context.getString(R.string.srl_header_loading);
}
if(ta.hasValue(R.styleable.ClassicsHeader_srlTextRelease)){
    mTextRelease = ta.getString(R.styleable.ClassicsHeader_srlTextRelease);
} else if(REFRESH_HEADER_RELEASE != null) {
    mTextRelease = REFRESH_HEADER_RELEASE;
} else {
    mTextRelease = context.getString(R.string.srl_header_release);
}
if(ta.hasValue(R.styleable.ClassicsHeader_srlTextFinish)){
    mTextFinish = ta.getString(R.styleable.ClassicsHeader_srlTextFinish);
} else if(REFRESH_HEADER_FINISH != null) {
    mTextFinish = REFRESH_HEADER_FINISH;
} else {
    mTextFinish = context.getString(R.string.srl_header_finish);
}
if(ta.hasValue(R.styleable.ClassicsHeader_srlTextFailed)){
    mTextFailed = ta.getString(R.styleable.ClassicsHeader_srlTextFailed);
} else if(REFRESH_HEADER_FAILED != null) {
    mTextFailed = REFRESH_HEADER_FAILED;
} else {
    mTextFailed = context.getString(R.string.srl_header_failed);
}
if(ta.hasValue(R.styleable.ClassicsHeader_srlTextSecondary)){
    mTextSecondary = ta.getString(R.styleable.ClassicsHeader_srlTextSecondary);
} else if(REFRESH_HEADER_SECONDARY != null) {
    mTextSecondary = REFRESH_HEADER_SECONDARY;
} else {
    mTextSecondary = context.getString(R.string.srl_header_secondary);
}
if(ta.hasValue(R.styleable.ClassicsHeader_srlTextRefreshing)){
    mTextRefreshing = ta.getString(R.styleable.ClassicsHeader_srlTextRefreshing);
} else if(REFRESH_HEADER_REFRESHING != null) {
    mTextRefreshing = REFRESH_HEADER_REFRESHING;
} else {
    mTextRefreshing = context.getString(R.string.srl_header_refreshing);
}
if(ta.hasValue(R.styleable.ClassicsHeader_srlTextUpdate)){
    mTextUpdate = ta.getString(R.styleable.ClassicsHeader_srlTextUpdate);
} else if(REFRESH_HEADER_UPDATE != null) {
    mTextUpdate = REFRESH_HEADER_UPDATE;
} else {
    mTextUpdate = context.getString(R.string.srl_header_update);
}
mLastUpdateFormat = new SimpleDateFormat(mTextUpdate, Locale.getDefault());

ta.recycle();

初始化自定义下拉View逻辑。

progressView.animate().setInterpolator(null);
updateView.setVisibility(mEnableLastTime ? VISIBLE : GONE);
mTitleText.setText(thisView.isInEditMode() ? mTextRefreshing : mTextPulling);

if (thisView.isInEditMode()) {
    arrowView.setVisibility(GONE);
} else {
    progressView.setVisibility(GONE);
}

try {//try 不能删除-否则会出现兼容性问题
    if (context instanceof FragmentActivity) {
        FragmentManager manager = ((FragmentActivity) context).getSupportFragmentManager();
        if (manager != null) {
            @SuppressLint("RestrictedApi")
            List<Fragment> fragments = manager.getFragments();
            if (fragments.size() > 0) {
                setLastUpdateTime(new Date());
                return;
            }
        }
    }
} catch (Throwable e) {
    e.printStackTrace();
}

KEY_LAST_UPDATE_TIME += context.getClass().getName();
mShared = context.getSharedPreferences("ClassicsHeader", Context.MODE_PRIVATE);
setLastUpdateTime(new Date(mShared.getLong(KEY_LAST_UPDATE_TIME, System.currentTimeMillis())));

public ClassicsHeader setLastUpdateTime(Date time) {
    final View thisView = this;
    mLastTime = time;
    mLastUpdateText.setText(mLastUpdateFormat.format(time));
    if (mShared != null && !thisView.isInEditMode()) {
        mShared.edit().putLong(KEY_LAST_UPDATE_TIME, time.getTime()).apply();
    }
    return this;
}

结合自定义View,处理下拉逻辑。

@Override
public int onFinish(@NonNull RefreshLayout layout, boolean success) {
    if (success) {
        mTitleText.setText(mTextFinish);
        if (mLastTime != null) {
            setLastUpdateTime(new Date());
        }
    } else {
        mTitleText.setText(mTextFailed);
    }
    return super.onFinish(layout, success);//延迟500毫秒之后再弹回
}

@Override
public void onStateChanged(@NonNull RefreshLayout refreshLayout, @NonNull RefreshState oldState, @NonNull RefreshState newState) {
    final View arrowView = mArrowView;
    final View updateView = mLastUpdateText;
    switch (newState) {
        case None:
            updateView.setVisibility(mEnableLastTime ? VISIBLE : GONE);
        case PullDownToRefresh:
            mTitleText.setText(mTextPulling);
            arrowView.setVisibility(VISIBLE);
            arrowView.animate().rotation(0);
            break;
        case Refreshing:
        case RefreshReleased:
            mTitleText.setText(mTextRefreshing);
            arrowView.setVisibility(GONE);
            break;
        case ReleaseToRefresh:
            mTitleText.setText(mTextRelease);
            arrowView.animate().rotation(180);
            break;
        case ReleaseToTwoLevel:
            mTitleText.setText(mTextSecondary);
            arrowView.animate().rotation(0);
            break;
        case Loading:
            arrowView.setVisibility(GONE);
            updateView.setVisibility(mEnableLastTime ? INVISIBLE : GONE);
            mTitleText.setText(mTextLoading);
            break;
    }
}

上拉加载

类似下拉刷新,上拉加载也是继承了ClassicsAbstract,并实现了RefreshFooter。

public class ClassicsFooter extends ClassicsAbstract<ClassicsFooter> implements RefreshFooter {}

RefreshFooter也继承了RefreshComponent接口,不过多了一个功能设置没有更多数据的提示。

public interface RefreshFooter extends RefreshComponent {

    /**
     * 【仅限框架内调用】设置数据全部加载完成,将不能再次触发加载功能
     * @param noMoreData 是否有更多数据
     * @return true 支持全部加载完成的状态显示 false 不支持
     */
    boolean setNoMoreData(boolean noMoreData);
}

加载自定义布局。

public ClassicsFooter(Context context) {
    this(context, null);
}

public ClassicsFooter(Context context, AttributeSet attrs) {
    super(context, attrs, 0);

    View.inflate(context, R.layout.srl_classics_footer, this);

    final View thisView = this;
    final View arrowView = mArrowView = thisView.findViewById(R.id.srl_classics_arrow);
    final View progressView = mProgressView = thisView.findViewById(R.id.srl_classics_progress);

    mTitleText = thisView.findViewById(R.id.srl_classics_title);
}

自定义布局。

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    tools:paddingTop="20dp"
    tools:paddingBottom="20dp"
    tools:background="#eee"
    tools:parentTag="android.widget.RelativeLayout">

    <ImageView
        android:id="@+id/srl_classics_arrow"
        android:layout_width="20dp"
        android:layout_height="20dp"
        android:layout_marginEnd="20dp"
        android:layout_marginRight="20dp"
        android:layout_centerVertical="true"
        android:layout_toLeftOf="@+id/srl_classics_title"
        android:layout_toStartOf="@+id/srl_classics_title"
        android:contentDescription="@android:string/untitled"
        />

    <ImageView
        android:id="@+id/srl_classics_progress"
        android:layout_width="20dp"
        android:layout_height="20dp"
        android:layout_marginEnd="20dp"
        android:layout_marginRight="20dp"
        android:layout_centerVertical="true"
        android:layout_toLeftOf="@+id/srl_classics_title"
        android:layout_toStartOf="@+id/srl_classics_title"
        android:contentDescription="@android:string/untitled"
        tools:tint="#666"
        tools:src="@android:drawable/stat_notify_sync"/>

    <TextView
        android:id="@+id/srl_classics_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:maxLines="1"
        android:textColor="#666666"
        android:textSize="15sp"
        android:text="@string/srl_footer_pulling"/>

</merge>

加载自定义属性。

public static String REFRESH_FOOTER_PULLING = null;//"上拉加载更多";
public static String REFRESH_FOOTER_RELEASE = null;//"释放立即加载";
public static String REFRESH_FOOTER_LOADING = null;//"正在加载...";
public static String REFRESH_FOOTER_REFRESHING = null;//"正在刷新...";
public static String REFRESH_FOOTER_FINISH = null;//"加载完成";
public static String REFRESH_FOOTER_FAILED = null;//"加载失败";
public static String REFRESH_FOOTER_NOTHING = null;//"没有更多数据了";

protected String mTextPulling;//"上拉加载更多";
protected String mTextRelease;//"释放立即加载";
protected String mTextLoading;//"正在加载...";
protected String mTextRefreshing;//"正在刷新...";
protected String mTextFinish;//"加载完成";
protected String mTextFailed;//"加载失败";
protected String mTextNothing;//"没有更多数据了";

protected boolean mNoMoreData = false;
    
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ClassicsFooter);

LayoutParams lpArrow = (LayoutParams) arrowView.getLayoutParams();
LayoutParams lpProgress = (LayoutParams) progressView.getLayoutParams();
lpProgress.rightMargin = ta.getDimensionPixelSize(R.styleable.ClassicsFooter_srlDrawableMarginRight, SmartUtil.dp2px(20));
lpArrow.rightMargin = lpProgress.rightMargin;

lpArrow.width = ta.getLayoutDimension(R.styleable.ClassicsFooter_srlDrawableArrowSize, lpArrow.width);
lpArrow.height = ta.getLayoutDimension(R.styleable.ClassicsFooter_srlDrawableArrowSize, lpArrow.height);
lpProgress.width = ta.getLayoutDimension(R.styleable.ClassicsFooter_srlDrawableProgressSize, lpProgress.width);
lpProgress.height = ta.getLayoutDimension(R.styleable.ClassicsFooter_srlDrawableProgressSize, lpProgress.height);

lpArrow.width = ta.getLayoutDimension(R.styleable.ClassicsFooter_srlDrawableSize, lpArrow.width);
lpArrow.height = ta.getLayoutDimension(R.styleable.ClassicsFooter_srlDrawableSize, lpArrow.height);
lpProgress.width = ta.getLayoutDimension(R.styleable.ClassicsFooter_srlDrawableSize, lpProgress.width);
lpProgress.height = ta.getLayoutDimension(R.styleable.ClassicsFooter_srlDrawableSize, lpProgress.height);

mFinishDuration = ta.getInt(R.styleable.ClassicsFooter_srlFinishDuration, mFinishDuration);
mSpinnerStyle = SpinnerStyle.values[ta.getInt(R.styleable.ClassicsFooter_srlClassicsSpinnerStyle, mSpinnerStyle.ordinal)];

if (ta.hasValue(R.styleable.ClassicsFooter_srlDrawableArrow)) {
    mArrowView.setImageDrawable(ta.getDrawable(R.styleable.ClassicsFooter_srlDrawableArrow));
} else if (mArrowView.getDrawable() == null) {
    mArrowDrawable = new ArrowDrawable();
    mArrowDrawable.setColor(0xff666666);
    mArrowView.setImageDrawable(mArrowDrawable);
}

if (ta.hasValue(R.styleable.ClassicsFooter_srlDrawableProgress)) {
    mProgressView.setImageDrawable(ta.getDrawable(R.styleable.ClassicsFooter_srlDrawableProgress));
} else if (mProgressView.getDrawable() == null) {
    mProgressDrawable = new ProgressDrawable();
    mProgressDrawable.setColor(0xff666666);
    mProgressView.setImageDrawable(mProgressDrawable);
}

if (ta.hasValue(R.styleable.ClassicsFooter_srlTextSizeTitle)) {
    mTitleText.setTextSize(TypedValue.COMPLEX_UNIT_PX, ta.getDimensionPixelSize(R.styleable.ClassicsFooter_srlTextSizeTitle, SmartUtil.dp2px(16)));
}

if (ta.hasValue(R.styleable.ClassicsFooter_srlPrimaryColor)) {
    super.setPrimaryColor(ta.getColor(R.styleable.ClassicsFooter_srlPrimaryColor, 0));
}
if (ta.hasValue(R.styleable.ClassicsFooter_srlAccentColor)) {
    super.setAccentColor(ta.getColor(R.styleable.ClassicsFooter_srlAccentColor, 0));
}

if(ta.hasValue(R.styleable.ClassicsFooter_srlTextPulling)){
    mTextPulling = ta.getString(R.styleable.ClassicsFooter_srlTextPulling);
} else if(REFRESH_FOOTER_PULLING != null) {
    mTextPulling = REFRESH_FOOTER_PULLING;
} else {
    mTextPulling = context.getString(R.string.srl_footer_pulling);
}
if(ta.hasValue(R.styleable.ClassicsFooter_srlTextRelease)){
    mTextRelease = ta.getString(R.styleable.ClassicsFooter_srlTextRelease);
} else if(REFRESH_FOOTER_RELEASE != null) {
    mTextRelease = REFRESH_FOOTER_RELEASE;
} else {
    mTextRelease = context.getString(R.string.srl_footer_release);
}
if(ta.hasValue(R.styleable.ClassicsFooter_srlTextLoading)){
    mTextLoading = ta.getString(R.styleable.ClassicsFooter_srlTextLoading);
} else if(REFRESH_FOOTER_LOADING != null) {
    mTextLoading = REFRESH_FOOTER_LOADING;
} else {
    mTextLoading = context.getString(R.string.srl_footer_loading);
}
if(ta.hasValue(R.styleable.ClassicsFooter_srlTextRefreshing)){
    mTextRefreshing = ta.getString(R.styleable.ClassicsFooter_srlTextRefreshing);
} else if(REFRESH_FOOTER_REFRESHING != null) {
    mTextRefreshing = REFRESH_FOOTER_REFRESHING;
} else {
    mTextRefreshing = context.getString(R.string.srl_footer_refreshing);
}
if(ta.hasValue(R.styleable.ClassicsFooter_srlTextFinish)){
    mTextFinish = ta.getString(R.styleable.ClassicsFooter_srlTextFinish);
} else if(REFRESH_FOOTER_FINISH != null) {
    mTextFinish = REFRESH_FOOTER_FINISH;
} else {
    mTextFinish = context.getString(R.string.srl_footer_finish);
}
if(ta.hasValue(R.styleable.ClassicsFooter_srlTextFailed)){
    mTextFailed = ta.getString(R.styleable.ClassicsFooter_srlTextFailed);
} else if(REFRESH_FOOTER_FAILED != null) {
    mTextFailed = REFRESH_FOOTER_FAILED;
} else {
    mTextFailed = context.getString(R.string.srl_footer_failed);
}
if(ta.hasValue(R.styleable.ClassicsFooter_srlTextNothing)){
    mTextNothing = ta.getString(R.styleable.ClassicsFooter_srlTextNothing);
} else if(REFRESH_FOOTER_NOTHING != null) {
    mTextNothing = REFRESH_FOOTER_NOTHING;
} else {
    mTextNothing = context.getString(R.string.srl_footer_nothing);
}

ta.recycle();

自定义属性。

<declare-styleable name="ClassicsFooter">
    <attr name="srlClassicsSpinnerStyle"/>
    <attr name="srlPrimaryColor"/>
    <attr name="srlAccentColor"/>
    <attr name="srlFinishDuration"/>
    <attr name="srlTextSizeTitle"/>
    <attr name="srlDrawableArrow"/>
    <attr name="srlDrawableProgress"/>
    <attr name="srlDrawableMarginRight"/>
    <attr name="srlDrawableSize"/>
    <attr name="srlDrawableArrowSize"/>
    <attr name="srlDrawableProgressSize"/>

    <attr name="srlTextPulling"/>
    <attr name="srlTextRelease"/>
    <attr name="srlTextLoading"/>
    <attr name="srlTextRefreshing"/>
    <attr name="srlTextFinish"/>
    <attr name="srlTextFailed"/>
    <attr name="srlTextNothing"/>

</declare-styleable>

处理自定义View属性。

progressView.animate().setInterpolator(null);
mTitleText.setText(thisView.isInEditMode() ? mTextLoading : mTextPulling);

if (thisView.isInEditMode()) {
    arrowView.setVisibility(GONE);
} else {
    progressView.setVisibility(GONE);
}

根据自己的下拉刷新处理业务逻辑。

@Override
public int onFinish(@NonNull RefreshLayout layout, boolean success) {
    /*
     * 2020-5-15 修复BUG
     * https://github.com/scwang90/SmartRefreshLayout/issues/1003
     * 修复 没有更多数据之后 loading 还在显示问题
     */
    super.onFinish(layout, success);
    if (!mNoMoreData) {
        mTitleText.setText(success ? mTextFinish : mTextFailed);
        return mFinishDuration;
    }
    return 0;
}

/**
 * ClassicsFooter 在(SpinnerStyle.FixedBehind)时才有主题色
 */
@Override@Deprecated
public void setPrimaryColors(@ColorInt int ... colors) {
    if (mSpinnerStyle == SpinnerStyle.FixedBehind) {
        super.setPrimaryColors(colors);
    }
}

/**
 * 设置数据全部加载完成,将不能再次触发加载功能
 */
@Override
public boolean setNoMoreData(boolean noMoreData) {
    if (mNoMoreData != noMoreData) {
        mNoMoreData = noMoreData;
        final View arrowView = mArrowView;
        if (noMoreData) {
            mTitleText.setText(mTextNothing);
            arrowView.setVisibility(GONE);
        } else {
            mTitleText.setText(mTextPulling);
            arrowView.setVisibility(VISIBLE);
        }
    }
    return true;
}

@Override
public void onStateChanged(@NonNull RefreshLayout refreshLayout, @NonNull RefreshState oldState, @NonNull RefreshState newState) {
    final View arrowView = mArrowView;
    if (!mNoMoreData) {
        switch (newState) {
            case None:
                arrowView.setVisibility(VISIBLE);
            case PullUpToLoad:
                mTitleText.setText(mTextPulling);
                arrowView.animate().rotation(180);
                break;
            case Loading:
            case LoadReleased:
                arrowView.setVisibility(GONE);
                mTitleText.setText(mTextLoading);
                break;
            case ReleaseToLoad:
                mTitleText.setText(mTextRelease);
                arrowView.animate().rotation(0);
                break;
            case Refreshing:
                mTitleText.setText(mTextRefreshing);
                arrowView.setVisibility(GONE);
                break;
        }
    }
}