(转载)Android快速开发偷懒必备,一句话搞定所有ViewGroup的Adapter . 支持自定义ViewGroup...

 

[置顶] 【Android】快速开发偷懒必备,一句话搞定所有ViewGroup的Adapter . 支持自定义ViewGroup 

标签: androidAdapter快速开发0耦合
 分类:
Android(25)    设计模式(3)    快速开发(3) 
 

目录(?)[+]

 

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

概述

开发中,经常会用到动态在ScrollView、LinearLayout里addView的事,尤其是ItemView一样时,每次都要写一大堆代码 inflater 动态addView,很烦。

还有就是在嵌套ListView、ScrollView时,想采用LinearLayout替代(这样性能更佳,不明白的看一个控件搞定嵌套ListView),但动态addView步骤神烦。

这个时候就开始期待,能不能有一种快速为任意ViewGroup添加子View的东西。

我之前为此事特意写过一篇LinearLayout封装博文,封装了一个控件使用。试图一个控件搞定嵌套ListView。但是后来发现,采用继承某个ViewGroup做这个事情不够优雅 ,对代码有侵入性,如果有其他ViewGroup需要动态addView,就会写重复的代码 。

前几天有人在群里问,如何方便的给ScrollView动态添加不同种类型的childView,类似RecyclerView那样。我之前的封装由于内部有一个简单的重用机制,只支持单一ItemType,也不支持多种类型的childView

那么需求就来了: 
快速简单使用  
* 支持任意ViewGroup 
无耦合 
无侵入性 
* Item支持多种类型

除此之外,我还加入: 
* 为ItemView设置OnItemClickListener 
* 为ItemView设置OnItemLongClickListener

本文就封装了这么一个东西。

核心: 
* 利用Adapter模式封装getView的操作 
* 搭配一个工具类,为所有ViewGroup addView。 
* 再封装出两个使用快速简单的Adapter 分别用于添加 单一Item布局、多种Item布局。

PS:所以本文也算是填了之前的一个坑,在之前适配器模式博文文末,我就提到要写一篇为流式布局增加Adapter的文章,作为Adapter的实战演练。使用本文封装的Adapter自然可以达到这一点。

由于采用Adapter隔离ViewGroup和ItemView,在切换ViewGroup时,十分方便。 
如:在需求让你把一个HorizontalScrollView包裹的水平标签转换成流式布局时,只需要在xml替换控件即可。Adapter将自动完成适配的工作。其他代码一句不用修改。

不BB了,先看看以后如何使用吧,够不够简单粗暴

使用预览

单一Item类型:

Adapter泛型传入JavaBean,构造函数传入数据集和layout布局,一句代码搞定:

        //单一ItemView
        ViewGroupUtils.addViews(mLinearLayout, new SingleAdapter<TestBean>(this, mDatas, R.layout.item_test) {
            @Override
            public void onBindView(ViewGroup parent, View itemView, TestBean data, int pos) {
                Glide.with(LinearLayoutActivity.this) .load(data.getAvatar()) .into((ImageView) itemView.findViewById(R.id.ivAvatar)); ((TextView) itemView.findViewById(R.id.tvName)).setText(data.getName()); } });

 

效果:

 
以前会用ScrollView嵌套ListView,现在只要用ScrollView套LinearLayout即可,性能更佳。

多种Item类型:

多种Item类型分两种情况:

数据结构相同:

数据结构相同依然可以给Adapter传入泛型,避免强转:

        //多种ItemViewType,但是数据结构相同,可以传入数据结构泛型,避免强转
        ViewGroupUtils.addViews(linearLayout, new MulTypeAdapter<MulTypeBean>(this, initDatas()) {
            @Override
            public void onBindView(ViewGroup parent, View itemView, MulTypeBean data, int pos) {
                ((TextView) itemView.findViewById(R.id.tvWords)).setText(data.getName() + ""); Glide.with(MulTypeActivity.this) .load(data.getAvatar()) .into((ImageView) itemView.findViewById(ivAvatar)); } });

效果:

多种Item,数据结构相同。

数据结构不同:

如果数据结构不同,则不用传入泛型,但是使用时需要强转:

        //多种Item类型:数据结构不同 不传泛型了 使用时需要强转javaBean,判断ItemLayoutId
        ViewGroupUtils.addViews((ViewGroup) findViewById(R.id.activity_mul_type_mul_bean), new MulTypeAdapter(this, datas) {
            @Override
            public void onBindView(ViewGroup parent, View itemView, IMulTypeHelper data, int pos) {
                switch (data.getItemLayoutId()) {
                    case R.layout.item_mulbean_1: MulBean1 mulBean1 = (MulBean1) data; Glide.with(MulTypeMulBeanActivity.this) .load(mulBean1.getUrl()) .into((ImageView) itemView); break; case R.layout.item_mulbean_2: MulBean2 mulBean2 = (MulBean2) data; TextView tv = (TextView) itemView; tv.setText(mulBean2.getName()); } } });

数据结构:

public class MulBean1 implements IMulTypeHelper { private String url; @Override public int getItemLayoutId() { return R.layout.item_mulbean_1; } }
public class MulBean2 implements IMulTypeHelper { private String name; @Override public int getItemLayoutId() { return R.layout.item_mulbean_2; } }

Item1布局是一个ImageView,Item2布局是一个TextView  
效果: 
这次用横向展示 多种Item,数据结构不同。

Item点击事件

item的点击和长按等事件,有两种方法设置,这里以点击事件为例,长按事件同理:

Adapter.onBindView()里设置

Adapter.onBindView()方法里能拿到ItemView,自然就可以设置各种事件。类似RecyclerView。

在这里设置优先级更高。原因后文会提到。

@Override
            public void onBindView(ViewGroup parent, View itemView, final MulTypeBean data, int pos) { .... itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Toast.makeText(mContext, "onBindView里设置:文字是:" + data.getName(), Toast.LENGTH_SHORT).show(); } }); }
通过ViewGroupUtils设置

可以在ViewGroupUtils.addViews直接作为参数传入.

也可以用ViewGroupUtils.setOnItemClickListener()设置 。

优先级比Adapter.onBindView()里设置低,原因后文会提到。

        //设置OnItemClickListener
        OnItemClickListener onItemClickListener = new OnItemClickListener() {
            @Override
            public void onItemClick(ViewGroup parent, View itemView, int position) { Toast.makeText(MulTypeActivity.this, "通过OnItemClickListener设置:" + position, Toast.LENGTH_SHORT).show(); } }; //可以在`ViewGroupUtils.addViews`直接作为参数传入.\ ViewGroupUtils.addViews(linearLayout, adapter ,onItemClickListener); //或者 也可以用`ViewGroupUtils.setOnItemClickListener()`设置 ViewGroupUtils.setOnItemClickListener(linearLayout,onItemClickListener); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

看起来还是挺好的,嗯~至少我自己这么觉得,我个人比较喜欢这种0耦合,每一个库都像可组装拆卸的机关枪一样,拿起来就用。而不是笨重功能繁多的重装坦克。 
搭配我的得意之作,每次必安利的史上集成最简单侧滑菜单控件。 
效果如下:

文首提到的,一开始是个水平ScrollView

替换成流式布局

无特殊设置,仅仅替换ViewGroup为流式布局,替换Item根布局为我撸的侧滑菜单库,能感受到这种0耦合的库的魅力了么。23333333 。

设计思路

下面就让我手摸手带大家实现它。 
先看类图。

UML类图:

先简要概括 
* 我们的顶层接口IViewGroupAdapter暴露出两个方法供ViewGroup使用。

  • ViewGroupUtils 是为任意ViewGroup 动态addView的工具类,只依赖于 IViewGroupAdapter 接口即可完成工作。

  • BaseAdapter是第二层,在这一层引入了数据集,用List<T>保存。实现IViewGroupAdapter的方法,重载一个三参数的getView()方法,供子类去实现。

  • SingleAdapter是第三层,一个简化的Adapter,只支持单种Item,以LayoutId 构建View。实现getView()方法,并暴露出onBindView()供用户快速使用。

  • MulTypeAdapter也同处第三层,一个支持多种Item的Adapter。依赖IMulTypeHelper接口,利用其getItemLayoutId() 方法去实现getView()方法,并暴露出onBindView()供用户快速使用。

顶层接口设计

顶层接口,即IViewGroupAdapter

根据迪米特法则(最少知道原则),我们应该抽象出一个顶层的接口,对ViewGroup暴露出最少的方法供使用。

我们想一下,对于ViewGroup,它最少只需要哪些就能完成我们的需求。 
* ChildView是什么—> View 
* 有多少ChildView 需要 添加—>count 
所以,我们的最顶层接口如下编写:

public interface IViewGroupAdapter {
    /** * ViewGroup调用获取ItemView * * @param parent * @param pos * @return */ View getView(ViewGroup parent, int pos); /** * ViewGroup调用,得到ItemCount * * @return */ int getCount(); }

ok,代码写到这里,后面的我们暂且不提,我们就可以写动态addView的工具类了。因为我们的ViewGroup依赖的所有信息都由IViewGroupAdapter这个接口提供了。

工具类

ViewGroupUtils 是为任意ViewGroup 动态addView的工具类,不考虑点击事件的情况下,只依赖于 IViewGroupAdapter 接口即可完成工作。 
如下编写:

    /**
     * 为任意ViewGroup 添加ItemViews.
     *
     * @param viewGroup               必传
     * @param adapter                 必传,至少提供要add的View和需要add的count
     * @param removeViews             是否需要remove掉之前的Views
     */
    public static void addViews(final ViewGroup viewGroup, IViewGroupAdapter adapter , boolean removeViews) { if (viewGroup == null || adapter == null) { return; } //如果需要remove掉之前的Views if (removeViews && viewGroup.getChildCount() > 0) { viewGroup.removeAllViews(); } //开始添加子Views,通过Adapter获得需要添加的Count int count = adapter.getCount(); for (int i = 0; i < count; i++) { //通过Adapter获得ItemView View itemView = adapter.getView(viewGroup, i); viewGroup.addView(itemView); } }

如此即可完成 动态给任意ViewGroup addView 的工作。

不过我们开头提过,我还是想引入ItemView的点击和长按事件的。但是关于这两个ItemListener,还是有一些东西要考虑的。

ItemListener的设计

为ViewGroup提供OnItemClickListener,有个问题需要考虑: 
如果使用者调用了setOnItemClickListener,且在Adapter里自己又对ItemView设置了OnClickListener,那么究竟该触发哪个Listener,即它们的优先级。

我们不应该自己靠脑子想答案,还是参照系统原有的设计比较好。既然ListView提供了OnItemClickListener,那么我们参照它的设计来就行。

先说结论:通过参照ListView的源码,以及实验得知,对ItemViewOnClickListener优先级 > ViewGroup的OnItemClickListener

为什么?答案在源码中,感兴趣看,不感兴趣直接跳过。

从AbsListView的onTouchEvent()->onTouchUp()->PerformClick->performItemClick-> AdapterView.performItemClick() -> AdapterView.mOnItemClickListener,即可找到答案。

这里不详细分析源码,不是本文重点,简单的说,从入口处是AbsListView的onTouchEvent() ,我们可以知道,ListView本身并没有干预ItemView的点击事件(即没有为其设置OnClickListener),是在ItemView不消耗Touch事件时 才进行Item点击事件的触发。 
因此若ItemView设置了OnClickListener,AbsListView的onTouchEvent()将收不到MotionEvent.ACTION_UP事件,因而也不会触发OnItemClickListener。所以这决定了ItemViewOnClickListener优先级高。

还有一个问题,我们可以通过View.hasOnClickListeners()这个方法来判断View是否设置了OnClickListener,但是这个方法在API15才加入,为了能兼容低版本,我采用了另一种方法判断,itemView.isClickable(),如果true,我当做有点击事件,如果false,我当做没有。

其实在ListView中这个问题也是一样的,itemView.isClickable()为true的话,点击事件就被拦截住,不会分发至AbsListView的onTouchEvent()里了。所以我们这么写是没问题,并且是正确的。

ItemListener的完整实现:

既然如此,那么我们在程序中,应该如下编写:

        for (int i = 0; i < count; i++) {
            View itemView = adapter.getView(viewGroup, i);
            viewGroup.addView(itemView);
            //添加点击事件
            if (null != onItemClickListener && !itemView.isClickable()) { final int finalI = i; itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { onItemClickListener.onItemClick(viewGroup, view, finalI); } }); } //添加点击事件 if (null != onItemLongClickListener && !itemView.isLongClickable()) { final int finalI = i; itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { return onItemLongClickListener.onItemLongClick(viewGroup, view, finalI); } }); } }

所以完整的addViews()如下:

    /**
     * 为任意ViewGroup 添加ItemViews.
     *
     * @param viewGroup               必传
     * @param adapter                 必传,至少提供要add的View和需要add的count
     * @param removeViews             是否需要remove掉之前的Views
     * @param onItemClickListener Item点击事件 * @param onItemLongClickListener Item长按事件 */ public static void addViews(final ViewGroup viewGroup, IViewGroupAdapter adapter , boolean removeViews , final OnItemClickListener onItemClickListener , final OnItemLongClickListener onItemLongClickListener) { if (viewGroup == null || adapter == null) { return; } //如果需要remove掉之前的Views if (removeViews && viewGroup.getChildCount() > 0) { viewGroup.removeAllViews(); } //开始添加子Views,通过Adapter获得需要添加的Count int count = adapter.getCount(); for (int i = 0; i < count; i++) { //通过Adapter获得ItemView View itemView = adapter.getView(viewGroup, i); viewGroup.addView(itemView); //添加点击事件,itemView之前没有点击事件才会去设置 if (null != onItemClickListener && !itemView.isClickable()) { final int finalI = i; itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { onItemClickListener.onItemClick(viewGroup, view, finalI); } }); } //添加长按事件itemView之前没有长按事件才会去设置 if (null != onItemLongClickListener && !itemView.isLongClickable()) { final int finalI = i; itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { return onItemLongClickListener.onItemLongClick(viewGroup, view, finalI); } }); } } }

 

实际中,我们可能不需要设置Listener,为了快速使用,我又提供了两个重载方法:

    /**
     * 为任意ViewGroup 添加ItemViews.
     * 并且会清除掉之前所有add过的View
     *
     * @param viewGroup 必传
     * @param adapter   必传,至少提供要add的View和需要add的count
     */
    public static void addViews(final ViewGroup viewGroup, IViewGroupAdapter adapter) { addViews(viewGroup, adapter, true, null, null); } /** * 为任意ViewGroup 添加ItemViews. * 并且会清除掉之前所有add过的View * * @param viewGroup 必传 * @param adapter 必传,至少提供要add的View和需要add的count * @param onItemClickListener Item点击事件 */ public static void addViews(final ViewGroup viewGroup, IViewGroupAdapter adapter , final OnItemClickListener onItemClickListener) { addViews(viewGroup, adapter, true, onItemClickListener, null); }

 

若不在addViews()里设置ItemListener,也可以通过setOnItemClickListener()setOnItemLongClickListener() 设置,不过这两个方法必须在addViews()方法之后调用:

    /**
     * 为任意ViewGroup设置OnItemClickListener.
     * 该方法必须在addViews()方法之后调用,否则无效。
     * 因为ItemView 必须被添加在ViewGroup里才能遍历到。
     * 建议直接在addViews()方法里传入OnItemClickListener进行设置,性能更高
     *
     * @param viewGroup
     * @param onItemClickListener
     */
    public static void setOnItemClickListener(final ViewGroup viewGroup, final OnItemClickListener onItemClickListener) { if (viewGroup == null || onItemClickListener == null) { return; } int childCount = viewGroup.getChildCount(); for (int i = 0; i < childCount; i++) { final View itemView = viewGroup.getChildAt(i); //itemView之前没有点击事件才会去设置 if (null != itemView && !itemView.isClickable()) { final int finalI = i; itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { onItemClickListener.onItemClick(viewGroup, itemView, finalI); } }); } } } /** * 为任意ViewGroup设置OnItemLongClickListener. * 该方法必须在addViews()方法之后调用,否则无效。 * 因为ItemView 必须被添加在ViewGroup里才能遍历到。 * 建议直接在addViews()方法里传入OnItemLongClickListener进行设置,性能更高 * * @param viewGroup * @param onItemLongClickListener */ public static void setOnItemLongClickListener(final ViewGroup viewGroup, final OnItemLongClickListener onItemLongClickListener) { if (viewGroup == null || onItemLongClickListener == null) { return; } int childCount = viewGroup.getChildCount(); for (int i = 0; i < childCount; i++) { final View itemView = viewGroup.getChildAt(i); //itemView之前没有长按事件才会去设置 if (null != itemView && !itemView.isLongClickable()) { final int finalI = i; itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { return onItemLongClickListener.onItemLongClick(viewGroup, itemView, finalI); } }); } } }

Adapter

终于到了我们的重头戏,Adapter。

BaseAdapter

BaseAdapter是第二层,在这一层引入了数据集,用List<T>保存。实现IViewGroupAdapter的方法,重载一个三参数的getView()方法,供子类去实现。

它和我们平时写的ListView、RecyclerView的Adapter就比较像了,我也是参照平时的写法。

核心就是实现IViewGroupAdaptergetView(ViewGroup parent, int pos)方法,增加一个数据,工作转交给三参数的getView(ViewGroup parent, int pos, T data)方法。

子类应该 实现 getView(ViewGroup parent, int pos, T data)方法,在其中inflate or new 出 ItemView,并绑定数据

public abstract class BaseAdapter<T> implements IViewGroupAdapter { protected List<T> mDatas; protected Context mContext; protected LayoutInflater mInflater; /** * ViewGroup调用获取ItemView,create bind一起做 * * @param parent * @param pos * @return */ @Override public View getView(ViewGroup parent, int pos) { return getView(parent, pos, mDatas.get(pos)); } /** * 实际的createItemView的地方 * * @param parent * @param pos * @param data * @return */ public abstract View getView(ViewGroup parent, int pos, T data); /** * ViewGroup调用,得到ItemCount * * @return */ @Override public int getCount() { return mDatas != null ? mDatas.size() : 0; } }

SingleAdapter

SingleAdapter是第三层,一个简化的Adapter,只支持单种Item,以LayoutId 构建View。实现getView()方法,并暴露出onBindView()供用户快速使用。

使用时,一般将数据结构的泛型传入,配合构造函数传入的ItemLayoutId使用。

它继承自BaseAdapter,所以它了实现getView(ViewGroup parent, int pos, T data)方法。在根据传入的itemLayoutId inflate出ItemView后,会回调onBindView(ViewGroup parent, View itemView, T data, int pos)方法,并返回ItemView供ViewGroup使用。

onBindView(ViewGroup parent, View itemView, T data, int pos)方法里,我们完成数据绑定的工作。例如加载图片,也可以设置点击事件。


public abstract class SingleAdapter<T> extends BaseAdapter<T> { private int mItemLayoutId; @Override public View getView(ViewGroup parent, int pos, T data) { //实现getView View itemView = /*onCreateView(parent, pos)*/mInflater.inflate(mItemLayoutId, parent, false); onBindView(parent, itemView, data, pos); return itemView; } /** * 暴漏这个 让外部bind数据 * * @param parent * @param itemView * @param data * @param pos */ public abstract void onBindView(ViewGroup parent, View itemView, T data, int pos); }

MulTypeAdapter

MulTypeAdapter也同处第三层,一个支持多种Item的Adapter。依赖IMulTypeHelper接口,利用其getItemLayoutId() 方法去实现getView()方法,并暴露出onBindView()供用户快速使用。

public abstract class MulTypeAdapter<T extends IMulTypeHelper> extends BaseAdapter<T> { @Override public View getView(ViewGroup parent, int pos, T data) { View itemView = mInflater.inflate(data.getItemLayoutId(), parent, false); onBindView(parent, itemView, data, pos); return itemView; } /** * 暴漏这个 让外部bind数据 * * @param parent * @param itemView * @param data * @param pos */ public abstract void onBindView(ViewGroup parent, View itemView, T data, int pos); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

IMulTypeHelper接口,同样简单:

public interface IMulTypeHelper {
    int getItemLayoutId();
}
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

总结

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

因为想支持任意ViewGroup,对ViewGroup代码无侵入性,因而对部分功能进行了取舍,例如设置OnItemXXXListener,如果采用继承ViewGroup,嵌入代码,可以做到不强制addViews()setOnItemClickListener()顺序。

通过上文我们能感受到一些面向接口编程的奥义,例如我们只需要设计好顶层的接口IViewGroupAdapter,在没写剩下的xxxAdapter时,工具类都可以写好,编译通过。 
这在以后扩展时,例如,我数据集不能用List来保存了,我可能是多个List or Map 等等,只需要实现IViewGroupAdapter接口即可定制一个Adapter。其他代码不需任何修改。

有人说过,设计模式怎么学,就是先学完一遍所有的设计模式。然后再全部忘掉他们,只要记住SOLID原则即可。 
我觉得很有道理,就像我之前一直在纠结代理模式和装饰者模式的区别,后来我想,编码时,我管它是什么模式,只要写出来的是易维护的代码即可。

to do list

  • 考虑加入复用缓存池
  • 考虑替换onBindView()ItemView->通用的ViewHolder,这样可以少写一些findViewById()代码。
  • 整合DataBinding 的通用Adapter入库。
  • 整合 RecyclerView、ListView的通用Adapter入库。
  • 加入一些自定义ViewGroup入库,例如流式布局,九宫格,Banner轮播图。

下文预告:

一个用ScrollView很容易实现,但RecyclerView、ListView就无法实现的动画效果,仿淘宝会员中心等级动画。凸显为所有ViewGroup增加Adapter模式的重要性。

DataBinding篇隆重登场

感兴趣可以去阅读 
http://blog.csdn.net/zxt0601/article/details/53618694

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

 
14
1
 
 
 
 
查看评论
8楼  Xiaohui783 2017-05-04 11:59发表  [回复]
大神,看了你Git上的代码,添加了ViewHolder ,但是发现还是创建很多ViewHolder,这里的ViewHolder只是单纯的减少findViewById 的代码吗?
7楼  Xiaohui783 2017-05-04 11:56发表  [回复]
大神,刚去看了你Git 上的代码,看到你写了复用ViewHolder,但是日志发现还是会创建datas长度个数的的ViewHolder ,这里单单只是为了少用findViewById吗
6楼  Only_Engineer 2016-12-28 15:43发表  [回复]
好文很多啊
5楼  zzr吻火 2016-12-15 15:57发表  [回复]
为楼主的分享精神点赞,写得很详细。
4楼  verzqli 2016-12-12 14:35发表  [回复]
UML都画了 腻害
3楼  cmdos 2016-12-12 13:47发表  [回复]
图片挺好看的
2楼  梁山boy 2016-12-12 13:15发表  [回复]
已star, 期待加入ViewHolder以后的样子
1楼  ccc95 2016-12-12 12:20发表  [回复]
不明觉厉
 
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
 
 
 
 
 

转载于:https://www.cnblogs.com/liupengfei005257/p/7448882.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值