BaseProject快速构建自己的APP

关于BaseProject


BaseProject是一个快速构建app工程的开源项目,目的是为了更加方便的初始化一个工程,省去编写或者导入BaseActivity,BaseFragment,网络请求,工具类等基础又实用的代码。让你更加专注去实现自己产品需求,业务逻辑,而不是浪费时间在重复的工作上!
https://github.com/flyzend/BaseProject

如何依赖BaseProject


[图片上传失败...(image-4d154a-1595469573432)]

  1. 在project.gradle 文件中添加 maven { url "https://jitpack.io" } 如下所示
allprojects {
    repositories {
        jcenter()
        maven { url "https://jitpack.io" }
    }
}```

2. 在app.gradle 文件中添加依赖,如下所示  

dependencies {
compile 'com.github.flyzend:BaseProject:V1.0.1'
}


# BaseProject帮你完成了下面的工作
---
## BaseApplication
**自己的工程定义一个Application并继承BaseApplication  
并且在AndroidManifest.xml中对Application节点改成你自己的Application如:**
android:name=".MyApplication"

**PreferenceUtils 以及FileDownloader 初始化操作** 
*PreferenceUtils 是SharePreference管理类,封装SharePreference的put ,get ,remove,clear操作。*
/**
 * 存入某个key对应的value值
 *
 * @param key
 * @param value
 */
public void put(String key, Object value) {
    Editor edit = mSharedPreferences.edit();
    if (value instanceof String) {
        edit.putString(key, (String) value);
    } else if (value instanceof Integer) {
        edit.putInt(key, (Integer) value);
    } else if (value instanceof Boolean) {
        edit.putBoolean(key, (Boolean) value);
    } else if (value instanceof Float) {
        edit.putFloat(key, (Float) value);
    } else if (value instanceof Long) {
        edit.putLong(key, (Long) value);
    }
    edit.apply();
}

/**
 * 得到某个key对应的值
 *
 * @param key
 * @param defValue
 * @return
 */
public Object get(String key, Object defValue) {
    if (defValue instanceof String) {
        return mSharedPreferences.getString(key, (String) defValue);
    } else if (defValue instanceof Integer) {
        return mSharedPreferences.getInt(key, (Integer) defValue);
    } else if (defValue instanceof Boolean) {
        return mSharedPreferences.getBoolean(key, (Boolean) defValue);
    } else if (defValue instanceof Float) {
        return mSharedPreferences.getFloat(key, (Float) defValue);
    } else if (defValue instanceof Long) {
        return mSharedPreferences.getLong(key, (Long) defValue);
    }
    return null;
}
***你可以在代码中直接使用PreferenceUtils来存储配置项***
比如常见配置项:用户是否登录

PreferenceUtils.getInstance().put("is_login",true);//设置登录配置项

PreferenceUtils.getInstance().get("is_login",false);//获取登录配置项,false为默认值

***或者你可以更优雅的在自己的工程中编写一个设置辅助类:SettingUtils.Java***,并且写一个存储用户是否登录的的配置。你可以根据需求来写自己需要的配置项。

public class SettingUtils {
private static SettingUtils instance;
private PreferenceUtils preferenceUtils;
private static final String IS_LOGIN = "is_login";//用户是否登录

public void setIsLogin(boolean isLogin){
    preferenceUtils.put(IS_LOGIN,isLogin);
}

public boolean getIsLogin(){
    return (Boolean) preferenceUtils.get(IS_LOGIN,false);
}


/**
* 创建一个新的实例 SettingsUtils.
*/
private SettingUtils() {
    preferenceUtils = PreferenceUtils.getInstance();
}

public static synchronized SettingUtils getInstance(){
    if (instance == null) {
        instance = new SettingUtils();
    }
    return instance;
}

public void clearSettings() {
    preferenceUtils.clear();
}

}

***这样你代码中不用输入令人讨厌的key***,即可方便存储配置:

SettingUtils.getInstance().setIsLogin(true);//设置登录配置项

SettingUtils.getInstance().getIsLogin();//获取登录配置项,false为默认值

**而关于FileDownloader,它是一个非常方便的下载工具类,使用方法 [github传送门](https://github.com/lingochamp/FileDownloader)**
FileDownloader提供了FileDownloadListener作为下载状态的监听,但是我们并非关心每一个状态,大多数时候我们只关心进度和完成时刻监听。
**BaseProject 提供给你一个SimpleFileDownloadListener,默认需要复写completed方法,表示下载完成的回调。你也可以手动复写updateProgress(int percent)方法,percent就是下载的百分比进度。**
    FileDownloader.getImpl()
            //下载地址
            .create("https://ss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/logo/bd_logo1_31bdc765.png")
            //保存路径
            .setPath(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "baidu.png")
            .setListener(new SimpleFileDownloadListener() {
                @Override
                protected void completed(BaseDownloadTask task) {
                    Bitmap bitmap = BitmapFactory.decodeFile(task.getTargetFilePath());
                    image.setImageBitmap(bitmap);
                }
            })
            .start();

#BaseActivity
---
**所有Activity类的基类,当你的Activity继承了它以后,将会拥有以下的技能:**
###1. 更方便的Toast
在需要弹出Toast的地方可直接调用***showToast(String msg);***或者***showLongToast(String msg);***来弹出Toast,并且已经做到防止反复弹出Toast
public void showToast(final String msg) {
    if (mToast == null) {
        mToast = Toast.makeText(mContext, msg, Toast.LENGTH_SHORT);
    } else {
        mToast.setText(msg);
        mToast.setDuration(Toast.LENGTH_SHORT);
    }
    mToast.show();
}
###2. 一行代码弹出系统加载对话框(ProgressDialog)
在耗时操作时弹出对话框,你只需要这样做***showLoading();*** **默认提示语"正在努力加载中..."**,你也可以传入你想弹出的提示语。
在结束对话框的地方调用***dismissLoading();***即可
###3. 极致精简的Log输出
在你需要用到Log的地方,你只需要这么做:***e("This is an error log");***
**同理v,i,d不同级别Log输出也是一样。**省去烦人的TAG。
而TAG已经在BaseActivity中定义为当前调用的Activity名字

protected final String TAG = getClass().getSimpleName();
.....
protected void e(String logBody) {LogUtil.e(TAG, logBody);}
protected void v(String logBody) {LogUtil.v(TAG, logBody);}
protected void i(String logBody) {LogUtil.i(TAG, logBody);}
protected void d(String logBody) {LogUtil.d(TAG, logBody);}

###4. 简单的Activity跳转
对于基础的Activity跳转进行简单的封装,让跳转更加的简单。
***需要返回结果的跳转,requestCode不为-1才能得到返回结果***

//jumpToActivity Activiy跳转
jumpToActivity(FragmentActivity.class);
//传参数跳转
Bundle bundle = new Bundle();
bundle.putString("name","123");
jumpToActivity(FragmentActivity.class,bundle);
//需要返回结果的跳转(startActivityForResult),requestCode不为-1才能得到返回结果
jumpToActivity(FragmentActivity.class,1001);
//需要返回结果的跳转,同时携带参数
jumpToActivity(FragmentActivity.class,bundle,1001);


###5. 省心的Fragment切换
BaseActivity已经为你提供了Fragment管理的方法。让你在Fragment之间能够随意自如的切换,你只用参考下面的方式就可以get到此技能!
效果图:
![frgmentSwitch.gif](http://upload-images.jianshu.io/upload_images/2839355-876afce52e25bfd1.gif?imageMogr2/auto-orient/strip)
先看看布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.design.widget.TabLayout
    android:id="@+id/tab"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

<FrameLayout
    android:layout_weight="1"
    android:id="@+id/fragmentLayout"
    android:layout_width="match_parent"
    android:layout_height="0dp"/>

</LinearLayout>

就是一个TabLayout 和一个FrameLayout,TabLayout是切换的标签,FrameLayout则是一个占位的布局,实际的Fragment会替换它。

下面就是Fragment切换的使用方式:

private List<Fragment> mFragments = new ArrayList<>();//存储Fragment的列表
...
protected void initViews() {
mTabLayout = findAviewById(R.id.tab);
mTabLayout.setTabMode(TabLayout.MODE_FIXED);
mTabLayout.addTab(mTabLayout.newTab().setText("one"));
mTabLayout.addTab(mTabLayout.newTab().setText("two"));
mTabLayout.addTab(mTabLayout.newTab().setText("three"));
mTabLayout.addTab(mTabLayout.newTab().setText("four"));
mTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
switchPages(tab.getPosition());
}

        @Override
        public void onTabUnselected(TabLayout.Tab tab) {

        }

        @Override
        public void onTabReselected(TabLayout.Tab tab) {

        }
    });
}

protected void initData() {
    mFragments.add(new OneFragment());
    mFragments.add(new TwoFragment());
    mFragments.add(new ThreeFragment());
    mFragments.add(new FourFragment());
    setFragmentContainer(R.id.fragmentLayout);//fragment所展示的布局id
    addFragments(mFragments);//添加Fragment列表
    switchFragment(0);//默认显示第一个Fragment
}
核心方法就是三个,setFragmentContainer,addFragments,switchFragment。
**setFragmentContainer方法设置所占位的FrameLayout布局id。**
**addFragments添加Fragment列表,列表保存了所有的Fragment,当然你也可以调用addFragment(Fragment fragment)方法去单独的添加fragment**
**switchFragment方法则是在你需要的时候随心所欲去切换Fragment吧!**

###6.显示系统样式对话框
protected void showDialog(String title, String msg, DialogInterface.OnClickListener listener){
    new BaseDialog(mContext).showDialog(title,msg,listener);
}
此方法可以快速显示一个系统样式对话框,三个参数分别为标题,内容以及确定按钮的点击事件。

#BaseFragment
---
BaseFragment当中的方法和BaseActivity差不多,不同的是去掉了Fragment管理相关的方法,其他的用法几乎相同,此处不再赘述!
#RxJava2 & Retrofit2 异步与网络访问操作
---
BaseProject 已经为你集成了[RxJava2](https://github.com/ReactiveX/RxJava) 与 [Retrofit2](https://github.com/square/retrofit),能够优雅的进行异步与网络访问操作。
关于RxJava的使用方式网上有很多资料,这里主要就介绍下BaseProject的网络请求方式。

关于Retrofit使用,包括官方资料都是一个API对应去定义一个接口,使用起来并不方便。BaseProject通过统一的请求方式,更加简单的进行网络请求的操作。
##GET请求
对于不需要参数的get请求,你可以这样做:
    ApiService.build(this).get(url).subscribe(new BaseSubscriber() {
        @Override
        public void doOnNext(String result) {
            mTextView.setText("get请求结果:"+result);
            WeatherBean bean = Util.parseGson(result, WeatherBean.class);
            if (bean != null){
                //do whatever you want
            }
        }
    });
**this 是继承BaseActivity 或者BaseFragment 的对象,url是请求地址,BaseSubscriber是BaseProject封装的网络请求的订阅者,你可以理解为网络请求结果的回调。
Util.parseGson是封装的一个json解析的工具类。通过它我们可以快速将json字符串解析成我们的javabean对象,然后就可以去愉快的使用bean啦!**

如果需要参数,也非常简单,构造一个Map对象传参即可
    Map<String,String> map = new HashMap<>();
    map.put("key","520520test");
    map.put("city","南京");
    map.put("province","江苏");
    ApiService.build(this).get(url,map).subscribe(new BaseSubscriber() {
        @Override
        public void doOnNext(String result) {
            mTextView.setText("get请求结果:"+result);
            WeatherBean bean = Util.parseGson(result, WeatherBean.class);
            if (bean != null){
                //do whatever you want
            }
        }
    });
##Post请求以及postJson数据
只用修改请求方法get 为post  或者postJson 。

//post
ApiService.build(this).post(url,map).subscribe(new BaseSubscriber() {
@Override
public void doOnNext(String result) {
mTextView.setText("post请求结果:"+result);
}
});
//postJson
ApiService.build(this).postJson(url,map).subscribe(new BaseSubscriber() {
@Override
public void doOnNext(String result) {
mTextView.setText("postJson请求结果:"+result);
}
});

***BaseSubscriber已经默认为你提供了网络加载的对话框,默认提示语为“正在努力加载中...”,如果你想自定义提示语可以通过构造方法传入***

ApiService.build(this).post(url,map).subscribe(new BaseSubscriber("你的加载提示语") {...}

***如果你不想要加载对话框,可以在构造方法直接传入false***

ApiService.build(this).post(url,map).subscribe(new BaseSubscriber(false) {...}



#What's more?
---
##BasePagerAdapter,通用Viewpager的Adapter

public class ViewpagerActivity extends BaseActivity {
private ViewPager mViewPager;
private List<View> mViews;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_viewpager);
initViews();
initData();
}

@Override
protected void initViews() {
    mViewPager = findAviewById(R.id.viewpager);
    mViews = new ArrayList<>();
}

@Override
protected void initData() {
    Flowable.just("view1","view2","view3","view4").subscribe(new Consumer<String>() {
        @Override
        public void accept(String s) throws Exception {
            TextView textView = new TextView(mContext);
            textView.setGravity(Gravity.CENTER);
            textView.setText(s);
            mViews.add(textView);
        }
    });
    mViewPager.setAdapter(new BasePagerAdapter<>(mViews));
}

}

你只需要将你需要滑动的view都添加到一个List中,再传入BasePageAdapter的构造方法即可。
##DimImageView 图片点击变灰
一般点击效果可以通过设置background的选择器,两张背景图点击和常规的即可实现。那么如果图片来自网络呢?接收两张图片然后设置点击样式?**很明显这种多图方式既麻烦又占资源**
BaseProject为你带来点击图片变灰的图片控件DimImageView,***使用上只需要将ImageView替换成DimImageView即可。***
*实际上是通过给原图添加滤镜来实现的效果。*
@Override
public boolean onTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        setFilter();
    } else if (event.getAction() == MotionEvent.ACTION_UP) {
        removeFilter();
    }
    return super.onTouchEvent(event);
}

/**
 * 设置滤镜
 */
public void setFilter() {
    //先获取设置的src图片
    Drawable drawable = getDrawable();
    //当src图片为Null,获取背景图片
    if (drawable == null) {
        drawable = getBackground();
    }
    if (drawable != null) {
        //设置滤镜
        drawable.setColorFilter(Color.parseColor("#FFD1D1D1"), PorterDuff.Mode.MULTIPLY);
    }
}

/**
 * 清除滤镜
 */
public void removeFilter() {
    //先获取设置的src图片
    Drawable drawable = getDrawable();
    //当src图片为Null,获取背景图片
    if (drawable == null) {
        drawable = getBackground();
    }
    if (drawable != null) {
        //清除滤镜
        drawable.clearColorFilter();
    }
}

效果图:
![DimImageView.gif](http://upload-images.jianshu.io/upload_images/2839355-332020127f41485f.gif?imageMogr2/auto-orient/strip)

##BannerLayout 简单实用的轮播图
[BannerLayout](https://github.com/dongjunkun/BannerLayout) 是github上的开源控件,使用起来非常容易。

##SwipeRecyclerView
将SwipeLayout 和RecyclerView结合在一起,同时使用[BaseRecyclerViewAdapterHelper](https://github.com/CymChad/BaseRecyclerViewAdapterHelper)作为RecyclerView的Adapter,即可快速拥有一个下拉刷新,上拉更多,多ITEM多布局,滑动删除等功能的列表。
BaseRecyclerViewAdapterHelper,是三方提供的非常好用的Adapter,其强大的程度令人叹为观止,使用上之后爱不释手所以我在这里安利一波,并且将它加入到了BaseProject中。

**SwipeRecyclerView 默认为你加载了垂直的线性布局管理器,如果你只用普通的列表那么可以不用去写布局管理器。**

以下是假设你已经了解[BaseRecyclerViewAdapterHelper](https://github.com/CymChad/BaseRecyclerViewAdapterHelper)的使用方法。我们直接进入SwipeRecyclerView的使用。

效果图:
![swipeRecyView.gif](http://upload-images.jianshu.io/upload_images/2839355-019bd6b3cf374401.gif?imageMogr2/auto-orient/strip)

**新建一个Adapter**

public class SwipeRecyAdapter extends BaseQuickAdapter<String,BaseViewHolder> {
public SwipeRecyAdapter(List<String> data) {
super(R.layout.swipe_recy_item, data);
}

@Override
protected void convert(BaseViewHolder baseViewHolder, String s) {
    baseViewHolder.setText(R.id.text,s);
}

}

Adapter 非常简单,Item的布局也只有一个TextView用于显示传进来的字符串。

**新建一个Activity**

public class SwipeRecyActivity extends BaseActivity
implements BaseQuickAdapter.RequestLoadMoreListener,SwipeRefreshLayout.OnRefreshListener{
private SwipeRecyclerView mSwipeRecyclerView;
private SwipeRecyAdapter mSwipeRecyAdapter;
private final int PAGE_SIZE = 10;
private int PAGE = 1;
private List<String> datas = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_swipe_recy);
initViews();
initData();
}

@Override
protected void initViews() {
    mSwipeRecyclerView = findAviewById(R.id.swipeRecyclerView);
    //设置下拉刷新监听
    mSwipeRecyclerView.setOnRefreshListener(this);
    //第一次进入需要加载数据,设置刷新为true
    mSwipeRecyclerView.setRefreshing(true);
}

@Override
protected void initData() {
    //模拟网络耗时操作,2秒后执行接下来操作
    Flowable.timer(2, TimeUnit.SECONDS).observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<Long>() {
        @Override
        public void accept(Long aLong) throws Exception {
            //获取数据
            datas = DataServer.getData(PAGE,PAGE_SIZE);
            mSwipeRecyAdapter = new SwipeRecyAdapter(datas);
            //设置加载更多的监听
            mSwipeRecyAdapter.setOnLoadMoreListener(SwipeRecyActivity.this);
            //设置刷新为false 此刻已经刷新完成
            mSwipeRecyclerView.setRefreshing(false);
            //设置adapter
            mSwipeRecyclerView.setBaseQuickAdapter(mSwipeRecyAdapter);
        }
    });
}

//下拉刷新回调
@Override
public void onRefresh() {
    PAGE = 1;
    Flowable.timer(2, TimeUnit.SECONDS).observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<Long>() {
        @Override
        public void accept(Long aLong) throws Exception {
            datas = DataServer.getData(PAGE,PAGE_SIZE);
            mSwipeRecyAdapter.setNewData(datas);
            mSwipeRecyclerView.setRefreshing(false);
        }
    });
}

//加载更多回调
@Override
public void onLoadMoreRequested() {
    Flowable.timer(2, TimeUnit.SECONDS).observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<Long>() {
        @Override
        public void accept(Long aLong) throws Exception {
            PAGE++;
            datas = DataServer.getData(PAGE,PAGE_SIZE);
            mSwipeRecyAdapter.addData(datas);
            mSwipeRecyAdapter.loadMoreComplete();
        }
    });
}

}

以上是SwipeRecyclerView的简单用法。更多复杂用法,主要在adapter上面,你可以去[BaseRecyclerViewAdapterHelper](https://github.com/CymChad/BaseRecyclerViewAdapterHelper) Get到更多高级玩法,相信它足以满足大家大部分的需求!

#支持开发者
---
你的支持是我完善BaseProject的最大动力!觉得不错点个赞吧,看到这里就去start一下呗![BaseProject传送门](https://github.com/flyzend/BaseProject)

#关于我
---
本人Android开发芸芸众生当中的一个新手,正在学习的路上不断爬坑!
**不论文章还是代码,当然肯定有很多不够好的地方,希望各位大神不吝赐教,我一定虚心学习,可以评论或者在github上提issue,欢迎关注转发!转发请带上原文链接!谢谢**
github:[https://github.com/flyzend](https://github.com/flyzend)
CSDN: [http://blog.csdn.net/u010302764](http://blog.csdn.net/u010302764)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容