Android快速实现仿美团选择城市界面,微信通讯录界面

转自: http://blog.csdn.net/zxt0601/article/details/53389835

欢迎Follow我的GitHub, 关注我的CSDN. 其余参考Android目录.

Android快速实现仿美团选择城市界面,微信通讯录界面  --有吧主转载

推荐文章:

如果你喜欢上了一个程序员小伙,献给所有的程序员女友

概述

本文是这个系列的第三篇,不出意外也是终结篇。因为使用经过重构后的控件已经可以快速实现市面上带 索引导航、悬停分组的列表界面了。 
在前两篇里,我们从0开始,一步一步实现了仿微信通讯录、饿了么选餐界面。 
第一篇戳我 第二篇戳我) 
这篇文章作为终结篇,和前文相比,主要涉及以下内容: 
* 重构悬停分组,将TitleItemDecoration更名为SuspensionDecoration,数据源依赖ISuspensionInterface接口。 
* 重构索引导航,将IndexBar对数据源的操作,如排序,转拼音等分离出去,以接口IIndexBarDataHelper通信。 
* 有N多兄弟给我留言、加QQ问的:如何实现美团选择城市列表页面, 
* 添加一个不带悬停分组的HeaderView(微信通讯录界面)

代码传送门:喜欢的话,随手点个star。多谢 
https://github.com/mcxtzhang/SuspensionIndexBar

老规矩,先上图: 
。 
美团选择城市界面,先刷新Body主体数据,再定向刷新头部数据

微信通讯录界面

配合我另一个库组装的效果(SuspensionIndexBar + SwipeMenuLayout) 
(SwipeDelMenuLayout : https://github.com/mcxtzhang/SwipeDelMenuLayout)

本文将先举例子如何写,并对其中涉及到的重构部分进行讲解。 
如有不明者,建议先观看(第一篇戳我 第二篇戳我), 
以及下载Demo,边看代码边阅读,效果更佳。


转载请标明出处: 
http://blog.csdn.net/zxt0601/article/details/53389835 
本文出自:【张旭童的博客】(http://blog.csdn.net/zxt0601
代码传送门:喜欢的话,随手点个star。多谢 
https://github.com/mcxtzhang/SuspensionIndexBar


微信通讯录界面写法

先从简单的用法看起,微信通讯录界面和普通的 分组悬停&索引导航 的列表相比: 
* 多了四个HeaderView 
* 这些HeaderView布局和主体Item一样 
* 这些HeaderView 没有分组悬停title 
* 这些HeaderView是一组的,索引title自定义

实现: 
HeaderView不是本文讨论重点,随意实现之。我用的是我自己之前写的,戳我

布局和主体Item一致

由于布局一致,则我们肯定偷懒直接用主体Item的Bean,将city设置为相应的数据即可,如 “新的朋友”:

public class CityBean extends BaseIndexPinyinBean {
    private String city;//城市名字
 
 
  • 1
  • 2
  • 1
  • 2

没有分组悬停

去掉分组悬停,我们需要重写isShowSuspension()方法,返回false。

索引title自定义

它们是一组的,则索引title一致,且需要自定义。 
四个头部的Bean调用setBaseIndexTag()方法,set自定义的title,且一致即可。

        mDatas.add((CityBean) new CityBean("新的朋友").setTop(true).setBaseIndexTag(INDEX_STRING_TOP));
        mDatas.add((CityBean) new CityBean("群聊").setTop(true).setBaseIndexTag(INDEX_STRING_TOP));
        mDatas.add((CityBean) new CityBean("标签").setTop(true).setBaseIndexTag(INDEX_STRING_TOP));
        mDatas.add((CityBean) new CityBean("公众号").setTop(true).setBaseIndexTag(INDEX_STRING_TOP));
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

核心代码:

CityBean里引入一个字段 isTop

public class CityBean extends BaseIndexPinyinBean {
    private String city;//城市名字
    private boolean isTop;//是否是最上面的 不需要被转化成拼音的
    ...
    @Override
    public String getTarget() {
        return city;
    }
    @Override
    public boolean isNeedToPinyin() {
        return !isTop;
    }
    @Override
    public boolean isShowSuspension() {
        return !isTop;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

初始化:

        mRv.addItemDecoration(mDecoration = new SuspensionDecoration(this, mDatas));
        //indexbar初始化
        mIndexBar.setmPressedShowTextView(mTvSideBarHint)//设置HintTextView
                .setNeedRealIndex(true)//设置需要真实的索引
                .setmLayoutManager(mManager);//设置RecyclerView的LayoutManager
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

数据加载:

        mDatas = new ArrayList<>();
        //微信的头部 也是可以右侧IndexBar导航索引的,
        // 但是它不需要被ItemDecoration设一个标题titile
        mDatas.add((CityBean) new CityBean("新的朋友").setTop(true).setBaseIndexTag(INDEX_STRING_TOP));
        mDatas.add((CityBean) new CityBean("群聊").setTop(true).setBaseIndexTag(INDEX_STRING_TOP));
        mDatas.add((CityBean) new CityBean("标签").setTop(true).setBaseIndexTag(INDEX_STRING_TOP));
        mDatas.add((CityBean) new CityBean("公众号").setTop(true).setBaseIndexTag(INDEX_STRING_TOP));
        for (int i = 0; i < data.length; i++) {
            CityBean cityBean = new CityBean();
            cityBean.setCity(data[i]);//设置城市名称
            mDatas.add(cityBean);
        }
        ...
        mIndexBar.setmSourceDatas(mDatas)//设置数据
                .invalidate();
        mDecoration.setmDatas(mDatas);
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

涉及到的重构代码

上文提到,重构后,SuspensionDecoration数据源依赖的接口是ISuspensionInterface, 
如下:

public interface ISuspensionInterface {
    //是否需要显示悬停title
    boolean isShowSuspension();
    //悬停的title
    String getSuspensionTag();
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

BaseIndexBean里实现,默认显示悬停,分组title和IndexBar的Tag是一样的。

public abstract class BaseIndexBean implements ISuspensionInterface {
    private String baseIndexTag;//所属的分类(城市的汉语拼音首字母)

    @Override
    public String getSuspensionTag() {
        return baseIndexTag;
    }

    @Override
    public boolean isShowSuspension() {
        return true;
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

BaseIndexPinyinBean类,现在如下:

public abstract class BaseIndexPinyinBean extends BaseIndexBean {
    private String baseIndexPinyin;//城市的拼音

    //是否需要被转化成拼音, 类似微信头部那种就不需要 美团的也不需要
    //微信的头部 不需要显示索引
    //美团的头部 索引自定义
    //默认应该是需要的
    public boolean isNeedToPinyin() {
        return true;
    }

    //需要转化成拼音的目标字段
    public abstract String getTarget();

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

所以我们需要实现微信那种效果,只需要重写isShowSuspension()isNeedToPinyin()这两个方法,并setBaseIndexTag()直接设置tag即可。

仿美团选择城市

这个页面还是挺麻烦的,所以步骤也最多。建议结合代码阅读Demo及库地址。 
分析美团选择城市列表: 
* 主体部分仍旧是一个普通的 分组悬停&索引导航 的列表(美团没有悬停功能)。 
* 头部是由若干复杂HeaderView组成。 
* 从右侧索引栏可以看出,定位、最近、热门这三个Item对应了列表三个HeaderView。 
* 最顶部的HeaderView是不需要分组,也不需要索引的。

那么逐一实现:

主体部分

很简单,根据前文最后的封装( 第二篇戳我),如果只有主体部分,我们需要让主体部分的JavaBean继承自BaseIndexPinyinBean,然后正常构建数据,最终设置给IndexBar和SuspensionDecoration即可。

public class MeiTuanBean extends BaseIndexPinyinBean {
    private String city;//城市名字
    ...
    @Override
    public String getTarget() {
        return city;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

头部若干HeaderViews

这里不管是通过HeaderView添加进来头部布局,还是通过itemViewType自己去实现,核心都是通过itemViewType去做的。 
也就是说头部的HeaderView也是RecyclerView的Item。 
既然是Item一定对应着相应的JavaBean。 
我们需要针对这些JavaBean让其分别继承BaseIndexPinyinBean。 
具体怎么实现头部布局不是本文重点,不再赘述,Demo里有代码可细看Demo及库地址

定、近、热三个HeaderView的处理

定、近、热三个HeaderView有如下特点: 
* 右侧导航索引的title 为自定义,不是拼音首字母则也不需要排序。 
* 悬停分组的title 和 右侧导航索引的title 不一样,则悬停分组的title也需要自定义

做法: 
不过既然是RecyclerView里的Item,又有 悬停分组、索引导航 特性。那么就要继承BaseIndexPinyinBean。 
* 不需要转化成拼音且不排序,则重写isNeedToPinyin()返回false,并调用setBaseIndexTag(indexBarTag)给右侧索引赋值。 
* 需要自定义悬停分组的title,则重写getSuspensionTag()返回title。

public class MeituanHeaderBean extends BaseIndexPinyinBean {
    private List<String> cityList;
    //悬停ItemDecoration显示的Tag
    private String suspensionTag;

    public MeituanHeaderBean(List<String> cityList, String suspensionTag, String indexBarTag) {
        this.cityList = cityList;
        this.suspensionTag = suspensionTag;
        this.setBaseIndexTag(indexBarTag);
    }

    @Override
    public String getTarget() {
        return null;
    }

    @Override
    public boolean isNeedToPinyin() {
        return false;
    }

    @Override
    public String getSuspensionTag() {
        return suspensionTag;
    }


}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

private List<MeituanHeaderBean> mHeaderDatas; 保存定、近、热头部数据源,最终需要将其设置给IndexBarSuspensionDecoration

        mHeaderDatas = new ArrayList<>();
        List<String> locationCity = new ArrayList<>();
        locationCity.add("定位中");
        mHeaderDatas.add(new MeituanHeaderBean(locationCity, "定位城市", "定"));
        List<String> recentCitys = new ArrayList<>();
        mHeaderDatas.add(new MeituanHeaderBean(recentCitys, "最近访问城市", "近"));
        List<String> hotCitys = new ArrayList<>();
        mHeaderDatas.add(new MeituanHeaderBean(hotCitys, "热门城市", "热"));
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

最顶部的HeaderView

最顶部的HeaderView,由于不需要右侧索引,也没有悬停分组。它只是一个普通的HeaderView即可。 
对于这种需求的HeaderView,只需要将它们的数量传给IndexBarSuspensionDecoration 即可。 
在内部我已经做了处理,保证联动坐标和数据源下标的正确。

mDecoration.setHeaderViewCount(mHeaderAdapter.getHeaderViewCount() - mHeaderDatas.size()));
mIndexBar.setHeaderViewCount(mHeaderAdapter.getHeaderViewCount() - mHeaderDatas.size());
 
 
  • 1
  • 2
  • 1
  • 2

这里用headerView一共的count=4,减去上步中mHeaderDatas的size =3,得出不需要右侧索引,也没有悬停分组 头部的数量。

将主体数据集和头部数据集合并

我们前几步中,设计到了三部分数据集, 
一部分是主体数据集,

    //主体部分数据源(城市数据)
    private List<MeiTuanBean> mBodyDatas;
 
 
  • 1
  • 2
  • 1
  • 2

第二部分是需要特性的头部数据集

    //头部数据源
    private List<MeituanHeaderBean> mHeaderDatas;
 
 
  • 1
  • 2
  • 1
  • 2

第三部分是不需要特性的数据集,这里忽略。我们只用到它的count。 
我们需要将第一和第二部分融合,并且设置给IndexBarSuspensionDecoration。 
则我们利用它们共同的基类,BaseIndexPinyinBean来存储。 
核心代码如下:

    //设置给InexBar、ItemDecoration的完整数据集
    private List<BaseIndexPinyinBean> mSourceDatas;

    mSourceDatas.addAll(mHeaderDatas);
    mSourceDatas.addAll(mBodyDatas);
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

设置给IndexBar

        mIndexBar.setmPressedShowTextView(mTvSideBarHint)//设置HintTextView
                .setNeedRealIndex(true)//设置需要真实的索引
                .setmLayoutManager(mManager)//设置RecyclerView的LayoutManager
                .setHeaderViewCount(mHeaderAdapter.getHeaderViewCount() - mHeaderDatas.size());
                .setmSourceDatas(mSourceDatas)//设置数据
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

设置给SuspensionDecoration

        mRv.addItemDecoration(new SuspensionDecoration(this, mSourceDatas)
                .setHeaderViewCount(mHeaderAdapter.getHeaderViewCount() - mHeaderDatas.size()));
 
 
  • 1
  • 2
  • 1
  • 2

效果图如文首。

核心代码

这里再提一点,我已经将排序功能抽离至IndexBarIIndexBarDataHelper类型变量中去做, 
mIndexBar.setmSourceDatas(mSourceDatas)时会自动排序。 
也可以手动调用mIndexBar.getDataHelper().sortSourceDatas(mBodyDatas);排序。 
像本节的案例,可以选择先排序bodyDatas,然后再合并至sourceDatas,最终设置给IndexBarSuspensionDecoration。 
如:

                //先排序
                mIndexBar.getDataHelper().sortSourceDatas(mBodyDatas);
                mSourceDatas.addAll(mBodyDatas);
                mIndexBar.setmSourceDatas(mSourceDatas)//设置数据
                        .invalidate();
                mDecoration.setmDatas(mSourceDatas);
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

涉及到的重构代码:

除了上节提到的那些数据结构的重构, 
我还将以前在IndexBar里完成的: 
* 1 将汉语转成拼音 
* 2 填充indexTag 
* 3 排序源数据源 
* 4 根据排序后的源数据源->indexBar的数据源

抽成一个接口表示,与IndexBar分离。

/**
 * 介绍:IndexBar 的 数据相关帮助类
 * 1 将汉语转成拼音
 * 2 填充indexTag
 * 3 排序源数据源
 * 4 根据排序后的源数据源->indexBar的数据源
 * 作者:zhangxutong
 * 邮箱:mcxtzhang@163.com
 * 主页:http://blog.csdn.net/zxt0601
 * 时间: 2016/11/28.
 */

public interface IIndexBarDataHelper {
    //汉语-》拼音
    IIndexBarDataHelper convert(List<? extends BaseIndexPinyinBean> data);

    //拼音->tag
    IIndexBarDataHelper fillInexTag(List<? extends BaseIndexPinyinBean> data);

    //对源数据进行排序(RecyclerView)
    IIndexBarDataHelper sortSourceDatas(List<? extends BaseIndexPinyinBean> datas);

    //对IndexBar的数据源进行排序(右侧栏),在 sortSourceDatas 方法后调用
    IIndexBarDataHelper getSortedIndexDatas(List<? extends BaseIndexPinyinBean> sourceDatas, List<String> datas);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

IndexBar内部持有这个接口的变量,调用其中方法完成需求:

 public IndexBar setmSourceDatas(List<? extends BaseIndexPinyinBean> mSourceDatas) {
        this.mSourceDatas = mSourceDatas;
        initSourceDatas();//对数据源进行初始化
        return this;
    }


    /**
     * 初始化原始数据源,并取出索引数据源
     *
     * @return
     */
    private void initSourceDatas() {
        //add by zhangxutong 2016 09 08 :解决源数据为空 或者size为0的情况,
        if (null == mSourceDatas || mSourceDatas.isEmpty()) {
            return;
        }
        if (!isSourceDatasAlreadySorted) {
            //排序sourceDatas
            mDataHelper.sortSourceDatas(mSourceDatas);
        } else {
            //汉语->拼音
            mDataHelper.convert(mSourceDatas);
            //拼音->tag
            mDataHelper.fillInexTag(mSourceDatas);
        }
        if (isNeedRealIndex) {
            mDataHelper.getSortedIndexDatas(mSourceDatas, mIndexDatas);
            computeGapHeight();
        }
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

我在sortSourceDatas()实现里,已经调用了convert(datas); 和 fillInexTag(datas);

    @Override
    public IIndexBarDataHelper sortSourceDatas(List<? extends BaseIndexPinyinBean> datas) {
        if (null == datas || datas.isEmpty()) {
            return this;
        }
        convert(datas);
        fillInexTag(datas);
        //对数据源进行排序
        Collections.sort(datas, new Comparator<BaseIndexPinyinBean>() {
            @Override
            public int compare(BaseIndexPinyinBean lhs, BaseIndexPinyinBean rhs) {
                if (!lhs.isNeedToPinyin()) {
                    return 0;
                } else if (!rhs.isNeedToPinyin()) {
                    return 0;
                } else if (lhs.getBaseIndexTag().equals("#")) {
                    return 1;
                } else if (rhs.getBaseIndexTag().equals("#")) {
                    return -1;
                } else {
                    return lhs.getBaseIndexPinyin().compareTo(rhs.getBaseIndexPinyin());
                }
            }
        });
        return this;
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

通过如下变量控制,是否需要排序,是否需要提取索引:

     //是否需要根据实际的数据来生成索引数据源(例如 只有 A B C 三种tag,那么索引栏就 A B C 三项)
    private boolean isNeedRealIndex;
    //源数据 已经有序?
    private boolean isSourceDatasAlreadySorted;
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

好处

这样做的好处是,当你不喜欢我这种排序方式,亦或你想自定义特殊字符的索引,现在是”#”,你都可以通过继承重写IndexBarDataHelperImpl类的方法来完成。或者干脆实现IIndexBarDataHelper接口,这就能满足扩展和不同的定制需求,不用每次修改IndexBar类。

总结

灵活重写ISuspensionInterface接口中的方法,可控制: 
* 是否需要显示悬停title 
* 悬停显示的titles

灵活重写BaseIndexPinyinBean中的方法,可控制: 
* 是否需要被转化成拼音, 类似微信头部那种就不需要 美团的也不需要 
* 微信的头部 不需要显示索引 
* 美团的头部 索引自定义 
* 默认应该是需要的 
* 在isNeedToPinyin()返回false时,不要忘了手动setBaseIndexTag()设置IndexBar的Tag值.

IndexBarIIndexBarDataHelper都提供了setHeaderViewCount(int headerViewCount)方法,供设置 不需要右侧索引,也没有悬停分组的HeaderView数量。

推荐文章

1、Android面试经验大解密

2、Android的viewHolder模式解剖

3、Android中必须学习的七大开源项目(开发项目必看)

4、如何自学Android, 教大家玩爆Android(成为大神必看)

5、2016 Google hosts 持续更新【更新 于:2016-08-27】(免费翻墙必备)

6、Android面试经验总结(面试成功必备)

7、Android Studio 个性化设置(装逼必备)

8、Android Studio 2.2 正式起航(玩爆Android Studio 2.2必备)

Android Studio 2.2 新功能实例代码:

Android Studio 2.2新功能实例源码(玩爆Android Studio 2.2必备)

Android Studio 2.2新功能介绍:

What's new in Android development tools - Google I/O 2016(YouTube视频需要自备梯子)

【GitHub】https://github.com/xiaole0310

【csdn博客】http://blog.csdn.net/xiaole0313

【新浪微博】http://weibo.com/u/5439466748

【知乎】http://www.zhihu.com/people/yang-shou-le

【简书】http://www.jianshu.com/users/1a47e8afa34a

【技术群】279126311 [满]

【技术群】484572225 [未]

【Email】ysle0313@gmail.com

Android Studio 2.2 新功能实例代码:

Android Studio 2.2新功能实例源码(玩爆Android Studio 2.2必备)

如果你有好的文章想和大家分享,欢迎投稿,直接向我投递文章链接即可。

欢迎扫描关注我们的微信公众号(ysle_0313),不要错过每一篇干货~


一键关注我们微信公众号 ysle_0313

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值