打造自己的下拉刷新库(Ultra-Pull-To-Refresh)(二)

在开始本篇阅读前,建议大家先看下上一篇《 打造自己的下拉刷新库(Ultra-Pull-To-Refresh)(一)》,主要是由于本篇很多接口和设计都在上一篇提到,在这里不会做过多展开。

在本系列的上一篇文章中,我们为大家分析了整个下拉刷新库的结构,其中最关键的就是我们将Ultra-PTR封装到了PullToRefreshBaseView基类中,为我们给各种view实现下拉刷新提供了便利的接入。那么今天我们继续给大家呈上PullToRefreshRecyclerView的打造过程,继承PullToRefreshBaseView基类轻松地为RecyclerView实现下拉刷新的功能。
由于之前业务上的需求,PullToRefreshRecyclerView目前只支持LinearLayoutManager的布局方式,也就是说用RecyclerView实现ListView的模式。在后续有时间会考虑接入阿里前段时间开源的VLayout,也能非常轻松的实现各种样式的RecyclerView。
这章节的PullToRefreshRecyclerView,主要实现下拉刷新、上拉加载、数据自动装载刷新、封装统一的adapter、模拟ListView的简单分割线这几项功能。

开始

首先我们继承PullToRefreshBaseView基类创建 一个PullToRefreshRecyclerView,实现onInitContent方法,在其中返回我们要实现的内部容器RecyclerView。

    @Override
    public View onInitContent() {
        mRecyclerView = new RecyclerView(getContext());
        mRecyclerView.setLayoutParams(new RecyclerView.LayoutParams(-1, -1));
        return mRecyclerView;
    }

其次,我们初始化刷新的默认头部、底部,给RecyclerView配置LaytouManager,这样就完成了基本的封装。

    private void initView() {
        setDefaultLoadingHeaderView();
        setDefaultLoadingFooterView();
        setOnRefreshListener(this);

        mLinearLayoutManager = new LinearLayoutManager(getContext());

        mRecyclerView.setLayoutManager(mLinearLayoutManager);
        mRecyclerView.setHasFixedSize(true);    //确定每个item高度相同,提高性能
        mRecyclerView.setAdapter(new EmptyRecyclerViewAdapter(getContext()));
    }

其中setOnRefreshListener设置的是要实现下拉刷新上拉加载两个监听。
EmptyRecyclerViewAdapter是继承RecyclerView.Adapter实现的一个空的adapter,因为在实际调用过程中,会产生一个警告:

“Recycler View..No adapter attached: skipping layout”

在网上查到的资料中显示,是因为网络请求的数据还没回来就调用了notifyDataSetChanged()导致的,这里只需要加个EmptyRecyclerViewAdapter即可解决。

好啦,写到这里,实际上我们已经实现了RecyclerView下拉刷新、上拉加载的简单封装。现在的PullToRefreshRecyclerView已经具备刷新的功能,通过实现onPullDownToRefresh方法,可以捕获下拉事件;通过实现onPullUpToRefresh方法,可以捕获上拉事件。直接操作mRecyclerView即可实现数据填充、增加list头部底部等。
当然我们的追求远不止那么简单,直接操作mRecyclerView显然不是我们的风格,所以我们进一步对mRecyclerView进行封装处理。

分割线Divider

设置分割线,我们希望能像下面这么简单地调个方法,即可设置item之间间隔的宽度和颜色。

mPullRefreshRecyclerView.setDivider(R.dimen.dp_07, R.color.default_dividing_line);

当然也支持自定义RecyclerView.ItemDecoration。

mPullRefreshRecyclerView.setDivider(mItemDecoration);

首先RecyclerView提供了RecyclerView.ItemDecoration抽象类给我们自定义分割线,实现onDraw方法,利用Canvas绘画即可。

public class PTRRecyclerViewDecoration extends RecyclerView.ItemDecoration {

    private Drawable mDivider;
    private int dividerHeight;
    private int dividerWidth;
    private int mOrientation;

    public boolean isHadHeader = false;
    public boolean isHadFooter = false;

    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;

    public PTRRecyclerViewDecoration(Context context, int orientation, Drawable drawable) {
        this.mDivider = drawable;
        this.dividerHeight = mDivider != null ? mDivider.getIntrinsicHeight() : 0;
        this.dividerWidth = mDivider != null ? mDivider.getIntrinsicWidth() : 0;
        setOrientation(orientation);
    }

    public PTRRecyclerViewDecoration(Context context, int orientation, Drawable drawable, int dividerHeight) {
        this.mDivider = drawable;
        this.dividerHeight = dividerHeight;
        this.dividerWidth = mDivider != null ? mDivider.getIntrinsicWidth() : 0;
        setOrientation(orientation);
    }

    //设置屏幕方向
    public void setOrientation(int orientation) {
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
            throw new IllegalArgumentException("invalid orientation");
        }
        mOrientation = orientation;
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (parent.getChildCount() > 2) {
            if (mOrientation == HORIZONTAL_LIST) {
                drawVerticalLine(c, parent, state);
            } else {
                drawHorizontalLine(c, parent, state);
            }
        }
    }

    //横向
    public void drawHorizontalLine(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (parent.getAdapter() == null) {
            return;
        }
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();
        final int childCount = parent.getChildCount();

        int dataEndPosition = parent.getAdapter().getItemCount();
        for (int i = 1; i < childCount - 1; i++) {
            if (mDivider == null) {
                break;
            }
            View child = parent.getChildAt(i);
            int position = parent.getChildAdapterPosition(child);

            //获取child的布局信息
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            int bottom = top + dividerHeight;

            //处理第一个HeaderView、最后一个FooterView分割线
            if ((isHadHeader && position <= 1) || (isHadFooter && position == dataEndPosition - 1)) {
                bottom = top;
            }
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    //竖向
    public void drawVerticalLine(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (parent.getAdapter() == null) {
            return;
        }
        int top = parent.getPaddingTop();
        int bottom = parent.getHeight() - parent.getPaddingBottom();
        final int childCount = parent.getChildCount();

        int dataEndPosition = parent.getAdapter().getItemCount();
        for (int i = 1; i < childCount - 1; i++) {
            if (mDivider == null) {
                break;
            }
            View child = parent.getChildAt(i);
            int position = parent.getChildAdapterPosition(child);

            //获取child的布局信息
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            final int left = child.getRight() + params.rightMargin;
            int right = left + mDivider.getIntrinsicWidth();
            //处理第一个HeaderView、最后一个FooterView分割线
            if ((isHadHeader && position <= 1) || (isHadFooter && position == dataEndPosition - 1)) {
                right = left;
            }
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);

        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if (mOrientation == HORIZONTAL_LIST) {
            outRect.set(0, 0, dividerWidth, 0);
        } else {
            outRect.set(0, 0, 0, dividerHeight);
        }
    }
}

核心主要还是绘图的代码,这里就不做展开,如果有一些复杂需求的分割线,可以网上搜搜,这方面的资料还是挺多的。
这里主要提一下两个关键的标记变量isHadHeader和isHadFooter。由于我们在下面要加入HeaderView和FooterView的支持,而我们在增加RecyclerView头部和底部的时候,都是不希望加入分割线的。所以这用两个标记变量isHadHeader和isHadFooter来判断,当前是否有加入头部/底部,从而“隐藏”分割线。
有了自定义的PTRRecyclerViewDecoration,我们就实现上面的setDivider方法了。

    public void setDivider(int padding, int divider) {
        if (padding > 0 && divider >= 0) {
            Drawable _divider = divider != 0 ? getResources().getDrawable(divider) : null;
            myDecoration = new PTRRecyclerViewDecoration(getContext(), PTRRecyclerViewDecoration.VERTICAL_LIST, _divider, (int) getResources().getDimension(padding));
            mRecyclerView.addItemDecoration(myDecoration);
        }
    }

优雅地添加HeaderView和FooterView

RcyclerView本身是不提供添加HeaderView和FooterView方法的,需要使用RecyclerView.Adapter来实现。而如果我们直接在我们已经实现的adapter上修改,增加头部和底部,这样我们需要为每个adapter加入同样的代码,显然不符合我们的封装思想。
这一节我们参考了鸿洋大神的下面这篇文章。

Android 优雅的为RecyclerView添加HeaderView和FooterView

采用装饰者模式的思想,给adapter包装一层,专门负责管理头部、底部的添加和删除。

/**
 * HeaderAndFooterWrapper .java
 */
public class HeaderAndFooterWrapper extends RecyclerView.Adapter{

    private static final int BASE_ITEM_TYPE_HEADER = 100000;
    private static final int BASE_ITEM_TYPE_FOOTER = 200000;

    private SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>();
    private SparseArrayCompat<View> mFooterViews = new SparseArrayCompat<>();

    private RecyclerView.Adapter mInnerAdapter;

    public HeaderAndFooterWrapper(RecyclerView.Adapter adapter) {
        mInnerAdapter = adapter;
    }

    private boolean isHeaderViewPos(int position) {
        return position < getHeadersCount();
    }

    private boolean isFooterViewPos(int position) {
        return position >= getHeadersCount() + getRealItemCount();
    }

    public void addHeaderView(View view) {
        mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
    }
    public void addFooterView(View view) {
        mFooterViews.put(mFooterViews.size() + BASE_ITEM_TYPE_FOOTER, view);
    }
    public void addHeaderView(List<View> view) {
        for (int i = 0; i < view.size(); i++) {
            addHeaderView(view.get(i));
        }
    }
    public void addFooterView(List<View> view) {
        for (int i = 0; i < view.size(); i++) {
            addFooterView(view.get(i));
        }
    }
    public void removeFooterView(View view) {
        int idx = mFooterViews.indexOfValue(view);
        if (idx != -1) {
            mFooterViews.removeAt(idx);
        }
    }


    public int getHeadersCount() {
        return mHeaderViews.size();
    }

    public int getFootersCount() {
        return mFooterViews.size();
    }
    private int getRealItemCount()
    {
        return mInnerAdapter.getItemCount();
    }


    @Override
    public int getItemViewType(int position) {
        if (isHeaderViewPos(position)) {
            return mHeaderViews.keyAt(position);
        }else if (isFooterViewPos(position)) {
            return mFooterViews.keyAt(position - getHeadersCount() - getRealItemCount());
        }
        return mInnerAdapter.getItemViewType(position - getHeadersCount());
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (mHeaderViews.get(viewType) != null) {
            View headerView = mHeaderViews.get(viewType);
            headerView.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT));

            CommonViewHolder myViewHolder = new CommonViewHolder(headerView);
            return myViewHolder;
        }else if (mFooterViews.get(viewType) != null) {
            View footerView = mFooterViews.get(viewType);
            footerView.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT));

            CommonViewHolder myViewHolder = new CommonViewHolder(footerView);
            return myViewHolder;
        }
        return mInnerAdapter.onCreateViewHolder(parent, viewType);
    }


    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (isHeaderViewPos(position)) {
            return;
        }else if (isFooterViewPos(position)) {
            return;
        }
        mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount());
    }

    @Override
    public int getItemCount() {
        return getHeadersCount() + getFootersCount() + getRealItemCount();
    }
}

有了HeaderAndFooterWrapper这个包装类,我们就可以增加一些方法,来管理PullToRefreshRecyclerView拥有的HeaderView和FooterView。

    private List<View> mHeaderViewList;
    private List<View> mFooterViewList;


    public void addHeaderView(View headerView) {
        if (mHeaderViewList == null){
            mHeaderViewList = new ArrayList<>();
        }
        mHeaderViewList.add(headerView);
        //指定分割线标记,当前拥有头部
        if(myDecoration != null) {
            myDecoration.isHadHeader = true;
        }
    }

    /**
     * 增加FooterView
     */
    public void addFooterView(View footerView) {
        if (mFooterViewList == null){
            mFooterViewList = new ArrayList<>();
        }
        mFooterViewList.add(footerView);
        //指定分割线标记,当前拥有底部
        if(myDecoration != null) {
            myDecoration.isHadFooter = true;
        }

        RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
        if (adapter != null && !(adapter instanceof EmptyRecyclerViewAdapter)) {
            if (adapter instanceof HeaderAndFooterWrapper) {
                ((HeaderAndFooterWrapper) adapter).addFooterView(footerView);
                adapter.notifyDataSetChanged();
            }else {
                HeaderAndFooterWrapper headerAndFooterWrapper = new HeaderAndFooterWrapper(adapter);
                headerAndFooterWrapper.addFooterView(footerView);
                mRecyclerView.setAdapter(headerAndFooterWrapper);
            }
        }
    }

    /**
     * 移除FooterView
     */
    public void removeFooterView(View footerView) {
        RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
        if (adapter != null && (adapter instanceof HeaderAndFooterWrapper)) {
            ((HeaderAndFooterWrapper) adapter).removeFooterView(footerView);

            if (mFooterViewList != null && mFooterViewList.indexOf(footerView) != -1) {
                mFooterViewList.remove(footerView);
            }
        }
    }

    /**
     * 包装adapter,增加HeaderView,FooterView
     */
    private RecyclerView.Adapter getWrappedListAdapter(RecyclerView.Adapter adapter) {

        if ((mHeaderViewList != null && mHeaderViewList.size() != 0) || (mFooterViewList != null && mFooterViewList.size() != 0)) {
            HeaderAndFooterWrapper headerAndFooterWrapper = new HeaderAndFooterWrapper(adapter);
            //增加HeaderView
            if (mHeaderViewList != null && mHeaderViewList.size() != 0) {
                headerAndFooterWrapper.addHeaderView(mHeaderViewList);
            }
            //增加FooterView
            if (mFooterViewList != null && mFooterViewList.size() != 0) {
                headerAndFooterWrapper.addFooterView(mFooterViewList);
            }
            return headerAndFooterWrapper;
        }
        return adapter;
    }

一键式数据装载与Item布局

上一篇文章中,我们提到了OnPullListActionListener接口,提供数据加载、item点击、item初始化、刷新完成的回调方法。主要用于在封装统一adapter的时候,使界面只需要关系接口请求和界面布局,实现一键式的数据装载与item布局指定。

/**
 * OnPullListActionListener .java
 */
public interface OnPullListActionListener<T> {
    void loadData(int pageIndex, String tips);
    void clickItem(T item, int position);
    void createListItem(ViewHolder holder, T currentItem, List<T> list, int position);
    void onRefreshComplete();
}
  • loadData:发起获取数据请求
  • clickItem:item点击事件
  • createListItem:初始化item布局,其中ViewHolder是统一View控制器,避免要定义一系列view的变量;currentItem是当前item的数据
  • onRefreshComplete:加载完成事件

/**
 * ViewHolder.java
 */
public class ViewHolder {
    private final SparseArray<View> mViews;
    private View mConvertView;
    private OnClickListener mOnClickListener;

    public ViewHolder(View parent) {
        mConvertView = parent;
        mViews = new SparseArray<View>();
    }

    public ViewHolder(View parent, OnClickListener clickListener) {
        mOnClickListener = clickListener;
        mConvertView = parent;
        mViews = new SparseArray<View>();
    }


    private ViewHolder(Context context, ViewGroup parent, int layoutId) {
        this.mViews = new SparseArray<View>();
        mConvertView = LayoutInflater.from(context).inflate(layoutId, parent, false);
        mConvertView.setTag(this);
    }

    public static ViewHolder get(Context context, View convertView,
                                 ViewGroup parent, int layoutId, int position) {
        if (convertView == null) {
            return new ViewHolder(context, parent, layoutId);
        }
        return (ViewHolder) convertView.getTag();
    }

    public View getConvertView() {
        return mConvertView;
    }

    public <T extends View> T getView(int viewId) {
        View view = mViews.get(viewId);
        if (view == null) {
            view = mConvertView.findViewById(viewId);
            mViews.put(viewId, view);
        }
        return (T) view;
    }

    public View setOnClickListener(int viewId) {
        View view = getView(viewId);
        setClickListener(view);
        return view;
    }

    public TextView setText(int viewId, CharSequence text) {
        TextView view = getView(viewId);
        if (view != null) {
            view.setText(text);
        }
        return view;
    }

    public TextView setTextColor(int viewId, int Color) {
        TextView view = getView(viewId);
        if (view != null) {
            view.setTextColor(Color);
        }
        return view;
    }

    public ImageView setImageResource(int viewId, int drawableId) {
        ImageView view = getView(viewId);
        if (view != null) {
            view.setImageResource(drawableId);
        }
        return view;
    }

    public View setBackgroundResource(int viewId, int drawableId) {
        View view = getView(viewId);
        if (view != null) {
            view.setBackgroundResource(drawableId);
        }
        return view;
    }

    public View setVisibility(int viewId, int visibility) {
        View view = getView(viewId);
        view.setVisibility(visibility);
        return view;
    }

    public int getVisibility(int viewId) {
        View view = getView(viewId);
        return view.getVisibility();
    }

    public void setClickListener(View view) {
        if (mOnClickListener != null && view != null) {
            view.setOnClickListener(mOnClickListener);
        }
    }

    public void setClickListener(OnClickListener clickListener) {
        mOnClickListener = clickListener;
    }

}

我们有了ViewHolder对View的统一控制,就可以在adapter中使用起来,继承RecyclerView.Adapter我们可以创建一个公共的CommonBaseAdapter,封装CommonViewHolder用来包装上面的ViewHolder,即可实现一个通用的Adapter,而不需要自己每次单独实现RecyclerView.ViewHolder。

/**
 * CommonViewHolder.java
 * 这部分代码非常简单,就封装了一个上面的ViewHolder
 */
public class CommonViewHolder extends RecyclerView.ViewHolder {

    public CommonViewHolder(View view) {
        super(view);
    }

    ViewHolder viewHolder;
}

公共的CommonBaseAdapter。

/**
 * CommonBaseAdapter.java
 */
public abstract class CommonBaseAdapter<T> extends RecyclerView.Adapter<CommonViewHolder>
{
    private Context mContext;
    private List<T> mData;
    protected final int mItemLayoutId;


    public CommonBaseAdapter(Context context, List<T> mData, int itemLayoutId)
    {
        this.mContext = context;
        this.mItemLayoutId = itemLayoutId;
        this.mData = mData;
    }

    @Override
    public CommonViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewHolder viewHolder = getViewHolder(0, null, parent);
        CommonViewHolder myViewHolder = new CommonViewHolder(viewHolder.getConvertView());
        myViewHolder.viewHolder = viewHolder;

        return myViewHolder;
    }

    @Override
    public void onBindViewHolder(final CommonViewHolder holder, final int position) {
        holder.itemView.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                onItemClick(holder.itemView, position);
            }
        });

        convert(holder.viewHolder, mData.get(position), mData, position);
    }

    private ViewHolder getViewHolder(int position, View convertView, ViewGroup parent) {
        return ViewHolder.get(mContext, convertView, parent, mItemLayoutId, position);
    }

    protected abstract void convert(ViewHolder holder, T item,List<T> list,int position);

    protected abstract void onItemClick(View itemView, int position);


    @Override
    public int getItemCount() {
        return mData.size();
    }

}

这样,我们可以在PullToRefreshRecyclerView创建一个内部类MyListAdapter,用来实现CommonBaseAdapter,从而将adapter内的创建、点击事件通过OnPullListActionListener传递到上层。

    private class MyListAdapter extends CommonBaseAdapter<T> {
        public MyListAdapter(Context context, List<T> mData, int itemLayoutId) {
            super(context, mData, itemLayoutId);
        }

        @Override
        protected void onItemClick(View itemView, int position) {
            if (position >= 0 && mList.size() > 0) {
                T item = mList.get(position);
                if (mOnPullListActionListener != null && item != null) {

                    int numHeaderView = mHeaderViewList != null ? mHeaderViewList.size() : 0;
                    mOnPullListActionListener.clickItem(item, position + numHeaderView);
                }
            }
        }

        @Override
        protected void convert(ViewHolder holder, T item, List<T> list, int position) {
            if (mOnPullListActionListener != null && item != null) {
                mOnPullListActionListener.createListItem(holder, item, list, position);
            }
        }
    }

这里,我们先简单回顾一下,看看我们上面都实现了哪些功能。

  • 添加分割线:setDivider(int padding, int divider)
  • 添加HeaderView:addHeaderView(View headerView),
  • 添加FooerView:addFooterView(View footerView)
  • 移除FooterView:removeFooterView(View footerView)
  • 初始化Item布局:通过接口OnPullListActionListener的createListItem,可以拿到ViewHolder,轻松实现布局和填充item的数据
  • Item点击:同样通过接口OnPullListActionListener的clickItem,可以捕获Item点击事件

一切似乎都已经非常强大了,但貌似还漏了些什么。没错万事具备,只欠东风,我们还缺少了最关键的加载数据,和数据展示。不急,马上为您呈上!
OnPullListActionListener接口还有一个关键的loadData()方法,这当然就是用来为我们加载数据调用的。

    /**
     * 下拉刷新加载数据
     */
    public void loadRefreshData(boolean isShowTops) {
        String tips = isShowTops ? TIPS_LOAD_DATA : "";
        mPageIndex = 1;

        if (mOnPullListActionListener != null) {
            mOnPullListActionListener.loadData(mPageIndex, tips);
        }
    }
    /**
     * 上拉刷新加载更多数据
     */
    public void loadMoreData(int taskId, boolean isShowTops) {
        String tips = isShowTops ? TIPS_LOAD_DATA : "";

        if (mOnPullListActionListener != null) {
            mOnPullListActionListener.loadData(mPageIndex, tips);
        }
    }

在上面我们已经实现了对adapter的一个包装类MyListAdapter,因此这里可以很简单的实现数据的装载与刷新。

    /**
     * 显示数据
     * 传入数据数组list,和指定的item布局itemLayoutId
     */
    public void showAllData(List<T> list, int itemLayoutId) {
        if (commonBaseAdapter == null) {
            commonBaseAdapter = new MyListAdapter(getContext(), list, itemLayoutId);
            mRecyclerView.setAdapter(getWrappedListAdapter(commonBaseAdapter));

        } else {
            getAdapter().notifyDataSetChanged();
        }
    }

写在最后

到这里,我们就将PullToRefreshRecyclerView封装RecyclerView的打造过程,完整地分析给了大家。完整的源码这里就不再贴出来了,我们在上一篇已经贴给大家了。
其实封装PullToRefreshRecyclerView的时候,更多是从我们项目的需求出发,所以我们暂时只实现了LinearLayoutManager列表式布局,在后面如果有时间,我打算接入阿里的VLayout,这样就能实现各种样式的下拉刷新RecyclerView。这里大家如果有兴趣,也可以继承PullToRefreshBaseView尝试自己实现一下,欢迎一起交流,共同学习!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值