BaseActivity的封装思想及YzsBaseActivity详解

这里写图片描述

BaseActivity在我们的项目中非常常用,所以,有一个好的封装会让我们的开发事半功倍 ,但是他怎么样封装才能方便我们的开发呢,这就是我们这片博客要讲解的内容。

在开始之前先安利两个项目,其实也可以说是一个项目,只不过被我拆分出来了,

YzsLib

一个共享的开发框架

https://github.com/yaozs/YzsLib

YzsBaseActivity

BaseActivity的框架

https://github.com/yaozs/YzsBaseActivity

这两个项目都正在维护中,baseactivity就是yzslib中拆分出来的,过年期间没有维护,给自己放了个假,嘿嘿,现在有开始继续维护。感兴趣的可以star下。里面有详细介绍,今天讲的所有东西都是YzsBaseActivity中的内容。

有人说现在流行的是mvp架构的程序,但我想问你,mvp架构的目的是什么呢? 他就是为了让代码的阅读性和可拓展性更强,同样,我写这篇博客的目的也是,而且我认为,在一些界面,根本就没必要使用mvp架构,架构是人写出来的,是死的东西,而人是活的,不要舍本逐末。当然,我也知道mvp架构的好处,所以,在这里提一句,我的这个封装是为了让一些简单的界面,常用的界面更加简洁的书写出来。没有必要说我这个app的架构是mvp就全用他。

Mvp与mvvm对比:

mvp结构图

这里写图片描述

mvvm结构图

这里写图片描述

Mvp与mvvm都是现在比较流行的架构,mvvm是基于google的data-binding绑定机制,现在用的人较MVP的少一些,但是两种架构的原理是一样的,但本人对mvvm接触的也不深,这里不做过多评价。 mvp架构呢,是一个将视图层和操作层拆分的非常明确是架构,虽然代码写起来会比普通写法多一些,但是他的维护性更强,对于大型项目有非常好的帮助。

但是,我们如果选择了mvp架构,就要把所有界面都写成MVP的形式吗?看一下下面的关于登录界面的截图:

这里写图片描述

这是我之前写的一个mvp架构的登录界面,类接口爆炸有没有,他确实有他的优点,但是缺点也非常明显,就是类的数量爆炸,那么我们是不是应该在一些简单的界面不使用这种架构呢,这就是我开发这个框架的目的,服务于各种架构,但又脱离这些架构,简单来讲,就是我代码写的少,并且清晰明了。

BaseActivity的封装:

我们一般会在baseActivity中封装一些使用频率比较高的方法,方便界面调用,下面我们看一下github中一个star数在2000左右的一个开源框架在base中封装了什么

这里写图片描述

这里写图片描述

这两张图片是FastDev4Android框架的base封装,他在里面都封装了什么呢,生命周期的各个方法,Activity的堆栈管理,toast显示,和intent跳转,这些东西都是我们在开发中常用的方法,封装到里面方便我们调用,但是我认为这些东西不全面,下面看看我在base中封装了什么

  • 跳转方法
  • 接收传值方法
  • Android 4.4版本以上沉浸式的封装
  • Toolbar的封装
  • 界面之间消息传递的封装(EventBus)
  • 加载中动画的封装

有了这些封装,我们在基本的activity中就减少了很多代码,比如加载动画,我们封装完成之后,可能只需要一个方法的调用,loading界面就出现了。

那么就让我们来一点点把这些常用方法封装进去。

1.跳转方法

          private static final String TAG = "YzsBaseActivity";
    public YzsBaseActivity() { /* compiled code */ }
    /**
     * RecyclerView空界面默认布局
     */
    protected View emptyView;
    public boolean useTitle = true;
    public Toolbar mToolbar;
    public TextView title;
    public ImageView back;
    public TextView tv_menu;
    public ImageView iv_menu;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            setTranslucentStatus(true);
            SystemBarTintManager tintManager = new SystemBarTintManager(this);
            tintManager.setStatusBarTintEnabled(true);
            tintManager.setStatusBarTintResource(R.color.colorPrimaryDark);//通知栏所需颜色
        }
        Bundle extras = getIntent().getExtras();
        if (null != extras) {
        }
        EventBus.getDefault().register(this);
        initContentView(savedInstanceState);
        mToolbar = (Toolbar) findViewById(R.id.toolbar);
        if (null != mToolbar) {
            setSupportActionBar(mToolbar);
            getSupportActionBar().setDisplayShowTitleEnabled(false);
            initTitle();
        }
        initView();
        initLogic();
    }
    /**
     * 替代onCreate的使用
     */
    protected abstract void initContentView(android.os.Bundle bundle);
    /**
     * 初始化view
     */
    protected abstract void initView();
    /**
     * 初始化逻辑
     */
    protected abstract void initLogic();

首先,baseActivity中oncreate的第一个方法4.4版本以上的沉浸式,这里面new出来的对象是我们自己写的,sdk不提供,具体代码大家可以参考我的项目源码,在baseac中封装了这个方法,我们只需要在style中声明一组属性,在xml布局中使用他,就可以完成沉浸式了。 这里写图片描述

这里写图片描述 第二个方法就是获取Intent的传值,我们在这里进行获取工作,下面会有方法去实现它,接下来的是eventbus的注册,初始化ac布局,在oncreat中都是初始化处理,下面都有指定方法去完成它,最后一个是toolbar初始化,我在这里findid使用的id是在ids中注册的,如果我们想使用base提供的toolbar,在xml中声明出来toolbar控件后让他使用这个存在的id就可以了,如果不想使用,有其他需求,则只需在xml中不使用这个id就可以,代码自己判断就不会走到下面的toolbar初始化。

接下来的是oncreat的结尾,下面是这几个初始化的接口方法,继承baseac就会强制重写,让我们分别在指定的位置写指定的代码,条理更加清晰。

/**
     * Bundle  传递数据
     *
     * @param extras
     */
    protected abstract void getBundleExtras(Bundle extras);
    protected void initTitle() {
        title = (TextView) findViewById(R.id.toolbar_title);
        back = (ImageView) findViewById(R.id.toolbar_back);
        iv_menu = (ImageView) findViewById(R.id.toolbar_iv_menu);
        tv_menu = (TextView) findViewById(R.id.toolbar_tv_menu);
        if (null != back) {
            back.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    finish();
                }
            });
        }
    }
    public void setTitle(String string) {
        if (null != title)
            title.setText(string);
    }
    public void setTitle(int id) {
        if (null != title)
            title.setText(id);
    }
 @TargetApi(19)
    private void setTranslucentStatus(boolean on) {
        Window win = getWindow();
        WindowManager.LayoutParams winParams = win.getAttributes();
        final int bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
        if (on) {
            winParams.flags |= bits;
        } else {
            winParams.flags &= ~bits;
        }
        win.setAttributes(winParams);
    }

这里的第一个方法是在前面说的获取intent的bundle消息,我们强制使用bundle,可以传递各种数据,获取时只需在这个方法里面对指定对象赋值就可以了,如果bundle为空,则不走该方法。InitTitle,oncreat中初始化toolbar的方法,同样如果不使用指定id,该方法也不会进入,setTItle,很明显,为title赋值,这是在允许toolbar初始化时可以调用的方法,最后的方法是oncreat中提到的4.4沉浸式,在上方的api19可以看出,只有在19及以上时才可以执行。

        /**
     * EventBus接收消息
     *
     * @param center 消息接收
     */
    @Subscribe
    public void onEventMainThread(EventCenter center) {
        if (null != center) {
            onEventComing(center);
        }
    }
    /**
     * EventBus接收消息
     *
     * @param center 获取事件总线信息
     */
    protected abstract void onEventComing(EventCenter center);

下面是对eventbus的封装,我讲eventbus的发送消息基类已经封装好了,可以放任意对象,我们在继承base后,后自动重写eventbus接受消息方法,在提供的数据里进行判断,获取数据,比如我们让所有存在界面接收刷新消息,Eventcenter中有code这个属性,我们设定刷新的指定code,界面在接收消息判断得到这个code知道是刷新指令,就执行刷新命令了。

    /**
 * Author: 姚智胜
 * Version: V1.0版本
 * Description: EventBus传递消息总体类
 * Date: 2016/11/17
 */
public class EventCenter<T> {
    private int eventCode = -1;
    private T data;
    public EventCenter(int eventCode) {
        this.eventCode = eventCode;
    }
    public EventCenter(int eventCode, T data) {
        this.eventCode = eventCode;
        this.data = data;
    }
    public int getEventCode() {
        return eventCode;
    }
    public T getData() {
        return data;
    }
}

这个就是我们是eventbus消息基类,我们发送的data是泛型结构,所以在指定位置进行强转就可获得指定对象了。

    /**
     * 显示默认加载动画 默认加载文字
     */
    protected void showLoadingDialog() {
        LoadingDialog.showLoadingDialog(this);
    }
    /**
     * 显示加载动画 默认加载文字
     *
     * @param type
     */
    protected void showLoadingDialog(int type) {
        LoadingDialog.showLoadingDialog(this, type);
    }
    /**
     * 显示加载动画 默认加载文字,自定义图片
     *
     * @param type
     */
    protected void showLoadingDialog(int type, int drawableId) {
        LoadingDialog.showLoadingDialog(this, type, drawableId);
    }
    /**
     * 显示默认加载动画 自定义加载文字
     *
     * @param str
     */
    protected void showLoadingDialog(String str) {
        LoadingDialog.showLoadingDialog(this, str);
    }
    /**
     * 显示加载动画 自定义加载文字
     *
     * @param type
     * @param str
     */
    protected void showLoadingDialog(int type, String str) {
        LoadingDialog.showLoadingDialog(this, type, str);
    }
    /**
     * 显示加载动画 自定义加载文字 自定义图片(只对YzsDialog有效果)
     *
     * @param type
     * @param str
     */
    protected void showLoadingDialog(int type, String str, int drawable) {
        LoadingDialog.showLoadingDialog(this, type, str, drawable);
    }
    /**
     * 取消加载动画
     */
    protected void cancelLoadingDialog() {
        LoadingDialog.cancelLoadingDialog();
    }
    //Toast显示
    protected void showShortToast(String string) {
        ToastUtils.showShortToast(this, string);
    }
    protected void showShortToast(int stringId) {
        ToastUtils.showShortToast(this, stringId);
    }
    protected void showLongToast(String string) {
        ToastUtils.showShortToast(this, string);
    }
    protected void showLongToast(int stringId) {
        ToastUtils.showShortToast(this, stringId);
    }

5.界面之间消息传递的封装(EventBus)

      mToolbar = (Toolbar) findViewById(R.id.toolbar);
        if (null != mToolbar) {
            setSupportActionBar(mToolbar);
            getSupportActionBar().setDisplayShowTitleEnabled(false);
            initTitle();
        }
    /**
     * 初始化toolbar
     */
   protected void initTitle() {
        title = (TextView) findViewById(R.id.toolbar_title);
        back = (ImageView) findViewById(R.id.toolbar_back);
        iv_menu = (ImageView) findViewById(R.id.toolbar_iv_menu);
        tv_menu = (TextView) findViewById(R.id.toolbar_tv_menu);
        if (null != back) {
            back.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    finish();
                }
            });
        }
    }

在这里说明下,我在我的库中findid不是吧他写死在xml中,而是在valuse中的创建了ids文件,

这里写图片描述

在这里把他引用出来,而在我们使用过程中如果你用到了,在你的xml把这个id使用出来就可以(id一致),如果不用,过着有更复杂的写法,将initTitle这个方法重写,就可以。

6.加载中动画的封装

在我的baseActivity框架中,我封装成的是一个loadingdialog,一个仿win10的loading,一个是自己写的动画

这里写图片描述

     /**
     * 显示默认加载动画 默认加载文字
     */
    protected void showLoadingDialog() {
        LoadingDialog.showLoadingDialog(this);
    }
    /**
     * 显示加载动画 默认加载文字
     *
     * @param type
     */
    protected void showLoadingDialog(int type) {
        LoadingDialog.showLoadingDialog(this, type);
    }
    /**
     * 显示加载动画 默认加载文字,自定义图片
     *
     * @param type
     */
    protected void showLoadingDialog(int type, int drawableId) {
        LoadingDialog.showLoadingDialog(this, type, drawableId);
    }
    /**
     * 显示默认加载动画 自定义加载文字
     *
     * @param str
     */
    protected void showLoadingDialog(String str) {
        LoadingDialog.showLoadingDialog(this, str);
    }
    /**
     * 显示加载动画 自定义加载文字
     *
     * @param type
     * @param str
     */
    protected void showLoadingDialog(int type, String str) {
        LoadingDialog.showLoadingDialog(this, type, str);
    }
    /**
     * 显示加载动画 自定义加载文字 自定义图片(只对YzsDialog有效果)
     *
     * @param type
     * @param str
     */
    protected void showLoadingDialog(int type, String str, int drawable) {
        LoadingDialog.showLoadingDialog(this, type, str, drawable);
    }
    /**
     * 取消加载动画
     */
    protected void cancelLoadingDialog() {
        LoadingDialog.cancelLoadingDialog();
    }

这写写的注释就非常清晰,加载loadingdialog的方法,toast的方法,和界面销毁解绑eventbus。其中这里的loading效果有两个,一个是放win10加载小球转动的效果,一个是我自己写的动画,支持定制图片。效果如图 这里写图片描述

 @Override
    protected void onDestroy() {
        emptyView = null;
        EventBus.getDefault().unregister(this);
        super.onDestroy();
    }
    /**
     * 界面跳转
     *
     * @param clazz 目标Activity
     */
    protected void readyGo(Class<?> clazz) {
        readyGo(clazz, null);
    }
    /**
     * 跳转界面,  传参
     *
     * @param clazz  目标Activity
     * @param bundle 数据
     */
    protected void readyGo(Class<?> clazz, Bundle bundle) {
        Intent intent = new Intent(this, clazz);
        if (null != bundle)
            intent.putExtras(bundle);
        startActivity(intent);
    }
    /**
     * 跳转界面并关闭当前界面
     *
     * @param clazz 目标Activity
     */
    protected void readyGoThenKill(Class<?> clazz) {
        readyGoThenKill(clazz, null);
    }
    /**
     * @param clazz  目标Activity
     * @param bundle 数据
     */
    protected void readyGoThenKill(Class<?> clazz, Bundle bundle) {
        readyGo(clazz, bundle);
        finish();
    }
    /**
     * startActivityForResult
     *
     * @param clazz       目标Activity
     * @param requestCode 发送判断值
     */
    protected void readyGoForResult(Class<?> clazz, int requestCode) {
        Intent intent = new Intent(this, clazz);
        startActivityForResult(intent, requestCode);
    }
    /**
     * startActivityForResult with bundle
     *
     * @param clazz       目标Activity
     * @param requestCode 发送判断值
     * @param bundle      数据
     */
    protected void readyGoForResult(Class<?> clazz, int requestCode, Bundle bundle) {
        Intent intent = new Intent(this, clazz);
        if (null != bundle) {
            intent.putExtras(bundle);
        }
        startActivityForResult(intent, requestCode);
    }

接下来的就是intent跳转传值方法,之前我说过,我们如有传值,强制使用bundle,就是在这里强制使用的。

以上就是我的baseac中的所有封装,有人会说这些代码也没有什么特别减少我们开发的东西呀,别急,baseac是服务于所有的ac我们只是把大家都能用到的东西放在里面,另外经常出现的代码我们会再次封装也就是我们接下来要讲的YzsBaseListActivity,YzsBaseHomeActivity。

YzsBaseListActivity:

列表类的经常出现,比如我们的消息列表,新闻列表,也就是这样的效果 这里写图片描述

这样的效果我们如果用普通方式写起来要写多少呢,来看看github上的一个开源项目androidFire新闻项目是怎么写的

/**
 * des:新闻fragment
 * Created by xsf
 * on 2016.09.17:30
 */
public class NewsFrament extends BaseFragment<NewsListPresenter, NewsListModel> implements NewsListContract.View, OnRefreshListener, OnLoadMoreListener {
    @Bind(R.id.irc)
    IRecyclerView irc;
    @Bind(R.id.loadedTip)
    LoadingTip loadedTip;
    private NewListAdapter newListAdapter;
    private List<NewsSummary> datas = new ArrayList<>();
    private String mNewsId;
    private String mNewsType;
    private int mStartPage=0;
    // 标志位,标志已经初始化完成。
    private boolean isPrepared;
    private boolean isVisible;
    @Override
    protected int getLayoutResource() {
        return R.layout.framents_news;
    }
    @Override
    public void initPresenter() {
        mPresenter.setVM(this, mModel);
    }
    @Override
    protected void initView() {
        if (getArguments() != null) {
            mNewsId = getArguments().getString(AppConstant.NEWS_ID);
            mNewsType = getArguments().getString(AppConstant.NEWS_TYPE);
        }
        irc.setLayoutManager(new LinearLayoutManager(getContext()));
        datas.clear();
        newListAdapter = new NewListAdapter(getContext(), datas);
        newListAdapter.openLoadAnimation(new ScaleInAnimation());
        irc.setAdapter(newListAdapter);
        irc.setOnRefreshListener(this);
        irc.setOnLoadMoreListener(this);
        //数据为空才重新发起请求
        if(newListAdapter.getSize()<=0) {
            mStartPage = 0;
            mPresenter.getNewsListDataRequest(mNewsType, mNewsId, mStartPage);
        }
    }
    @Override
    public void returnNewsListData(List<NewsSummary> newsSummaries) {
        if (newsSummaries != null) {
            mStartPage += 20;
            if (newListAdapter.getPageBean().isRefresh()) {
                irc.setRefreshing(false);
                newListAdapter.replaceAll(newsSummaries);
            } else {
                if (newsSummaries.size() > 0) {
                    irc.setLoadMoreStatus(LoadMoreFooterView.Status.GONE);
                    newListAdapter.addAll(newsSummaries);
                } else {
                    irc.setLoadMoreStatus(LoadMoreFooterView.Status.THE_END);
                }
            }
        }
    }
    /**
     * 返回顶部
     */
    @Override
    public void scrolltoTop() {
        irc.smoothScrollToPosition(0);
    }
    @Override
    public void onRefresh() {
        newListAdapter.getPageBean().setRefresh(true);
        mStartPage = 0;
        //发起请求
        irc.setRefreshing(true);
        mPresenter.getNewsListDataRequest(mNewsType, mNewsId, mStartPage);
    }
    @Override
    public void onLoadMore(View loadMoreView) {
        newListAdapter.getPageBean().setRefresh(false);
        //发起请求
        irc.setLoadMoreStatus(LoadMoreFooterView.Status.LOADING);
        mPresenter.getNewsListDataRequest(mNewsType, mNewsId, mStartPage);
    }
    @Override
    public void showLoading(String title) {
        if( newListAdapter.getPageBean().isRefresh()) {
            loadedTip.setLoadingTip(LoadingTip.LoadStatus.loading);
        }
    }
    @Override
    public void stopLoading() {
        loadedTip.setLoadingTip(LoadingTip.LoadStatus.finish);
    }
    @Override
    public void showErrorTip(String msg) {
        if( newListAdapter.getPageBean().isRefresh()) {
            loadedTip.setLoadingTip(LoadingTip.LoadStatus.error);
            loadedTip.setTips(msg);
            irc.setRefreshing(false);
        }else{
            irc.setLoadMoreStatus(LoadMoreFooterView.Status.ERROR);
        }
    }
}

这就是他在Activity中写的代码,有20行左右是获取api数据,我们不用去看,剩下的代码也不少呀,而且他的Adapter还是在另一个类中写的,是不是很多,如果我们使用YzsBaseListActivity呢,要写多少东西呢,看下面的代码

/**
 * Author: 姚智胜
 * Version: V1.0版本
 * Description:
 * Date: 2016/11/18
 */
public class DemoActivity extends YzsBaseListActivity<DemoBean> {
    @Override
    protected void initContentView(Bundle bundle) {
        setContentView(R.layout.ac_demo);
    }
    @Override
    protected void initItemLayout() {
        setLayoutResId(R.layout.item_demo);
    }
    @Override
    protected void initLogic() {
        setTitle("20行自己写的代码出现list界面");
        mRecyclerView.addOnItemTouchListener(new OnItemClickListener() {
            @Override
            public void SimpleOnItemClick(BaseQuickAdapter baseQuickAdapter, View view, int i) {
                Bundle bundle = new Bundle();
                bundle.putString("title", mAdapter.getItem(i).getName());
                readyGo(mAdapter.getItem(i).getClazz(), bundle);
            }
        });
        addData();
    }
    @Override
    protected void getBundleExtras(Bundle extras) {
    }
    @Override
    protected void onEventComing(EventCenter center) {
    }
    @Override
    protected void MyHolder(BaseViewHolder baseViewHolder, DemoBean demoBean) {
        baseViewHolder.setText(R.id.tv_item_demo, demoBean.getName());
    }
    private void addData() {
        List<DemoBean> list = new ArrayList<>();
        DemoBean bean = new DemoBean();
        bean.setClazz(FirstViewActivity.class);
        bean.setName("控件集合1");
        list.add(bean);
        bean = new DemoBean();
        bean.setClazz(AdaptiveWebViewActivity.class);
        bean.setName("自适应高度的webview+演示Loading");
        list.add(bean);
        bean = new DemoBean();
        bean.setClazz(StateButtonActivity.class);
        bean.setName("不用写selector的button+EventBus示范");
        list.add(bean);
        bean = new DemoBean();
        bean.setClazz(LikeIosDialogActivity.class);
        bean.setName("仿ios的dialog");
        list.add(bean);
        bean = new DemoBean();
        bean.setClazz(AndroidImageSliderActivity.class);
        bean.setName("AndroidImageSliderActivity");
        list.add(bean);
        bean = new DemoBean();
        bean.setClazz(UseAdapterActivity.class);
        bean.setName("UseAdapterActivity");
        list.add(bean);
        mAdapter.addData(list);
    }
}

除去加载假数据的代码,我们写了20行不到,而且这些代码中,全是YzsBaseListActivity强制要求重写的,也就是说,需要你写的代码也就是5到10行,这样的对比你们是否还满意呢,效果是一样的效果,代码量和清晰程度可不一样吧。是不是也想这样写代码,下面就来给大家讲下YzsBaseListActivity是怎么样写的,他的原理是什么。

public abstract class YzsBaseListActivity<T> extends YzsBaseActivity {
    private static final String TAG = "YzsBaseListActivity";
    /**
     * 普通list布局
     */
    protected static final int LINEAR_LAYOUT_MANAGER = 0;
    /**
     * grid布局
     */
    protected static final int GRID_LAYOUT_MANAGER = 1;
    /**
     * 瀑布流布局
     */
    protected static final int STAGGERED_GRID_LAYOUT_MANAGER = 2;
    /**
     * 默认为0单行布局
     */
    private int mListType = 0;
    /**
     * 排列方式默认垂直
     */
    private boolean mIsVertical = true;
    /**
     * grid布局与瀑布流布局默认行数
     */
    private int mSpanCount = 1;
    protected RecyclerView mRecyclerView;
    protected YzsListAdapter mAdapter;
    /**
     * 子布局id
     */
    private int layoutResId = -1;

首先我们在class前进行泛型声明,因为我们要写的是单一列表,所以他们的数据类肯定是一样的,使用泛型声明是为了让我们的adapter知道我们使用了什么数据类,下面的这几个常量就是为列表的布局方式设置的属性,我们在指定位置声明他,列表就是变成我们想要的布局。在这里我要说一下,我们使用的是recyclerview,因为他的功能比listview和gridview更加强大,一个控件就可以写出我们想要的这些布局,他的优化也比list和grid更加的优秀,更加详细的比较大家可以查阅下资料,我建议大家抛弃list和grid,使用最新的技术。

  @Override
    protected void initView() {
        initItemLayout();
        mRecyclerView = (RecyclerView) findViewById(R.id.yzs_base_list);
        chooseListType(mListType, mIsVertical);
        if (-1 == layoutResId) {
            throw new RuntimeException("layoutResId is null!");
        }
        mAdapter = new YzsListAdapter(layoutResId, new ArrayList<T>());
        mRecyclerView.setAdapter(mAdapter);
    }
     /**
     * 设置子布局layout
     *
     * @param layoutResId 子布局layout
     */
    public void setLayoutResId(@LayoutRes int layoutResId) {
        this.layoutResId = layoutResId;
    }
    /**
     * 初始化子布局
     */
    protected abstract void initItemLayout();

继续向下看,除了上面的常用属性,在initview里,第一个方法就是初始化子布局,这也是强制重写的方法,我们要在这里写出对于recyclerview的所有初始化,接下来,是recyclerview的初始化,同样和前面说的toolbar一样,在xml中使用这个id就可以第三个方法初始化recyclerview的加载形式,这个方法我们是调用不到的,只能用在initItemLayout中对其的值进行设定,放在这里是因为只有recyclerview初始化后才能对其进行操作,下面的判断,判断adapter的子布局是否设定,不舍得抛出异常。下面就是对recyclerview设置adapter了,这个adapter是我写的内部类,所以不需要你去处理,我们直接使用就可以了,下面是设置子布局和初始化子布局的方法,我就是要强制你在我的固定方法里写指定的代码,这样模块就能更加的清晰了。

 /**
     * 是否打开加载更多
     */
    protected void openLoadMoreSize(boolean loadMore) {
        mAdapter.loadMoreEnd(loadMore);
    }
    /**
     * @param type       布局管理type
     * @param isVertical 是否是垂直的布局 ,true垂直布局,false横向布局
     */
    protected void setListType(int type, boolean isVertical) {
        mListType = type;
        mIsVertical = isVertical;
    }
    protected void setSpanCount(int spanCount) {
        if (spanCount > 0)
            mSpanCount = spanCount;
    }

下面的代码,第一个是开启加载更多,这个是baserecyclerviewadapter框架,版本不是最新的,不过这个不影响我们,我会定期提供更新,几乎不需要你们在做神马处理,接下来这两个方法就是我刚刚提到的,我们在initItemLayout中提供的初始化方法,用我们这个类的静态常量,不要自己瞎写值呀。

/**
     * @param listType 选择布局种类
     */
    private void chooseListType(int listType, boolean isVertical) {
        switch (listType) {
            case LINEAR_LAYOUT_MANAGER:
                //设置布局管理器
                LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
                linearLayoutManager.setOrientation(isVertical ? LinearLayoutManager.VERTICAL : LinearLayoutManager.HORIZONTAL);
                mRecyclerView.setLayoutManager(linearLayoutManager);
                break;
            case GRID_LAYOUT_MANAGER:
                GridLayoutManager gridLayoutManager = new GridLayoutManager(this, mSpanCount);
                gridLayoutManager.setOrientation(isVertical ? GridLayoutManager.VERTICAL : GridLayoutManager.HORIZONTAL);
                mRecyclerView.setLayoutManager(gridLayoutManager);
                break;
            case STAGGERED_GRID_LAYOUT_MANAGER:
                //设置布局管理器
                StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager
                        (mSpanCount, isVertical ? StaggeredGridLayoutManager.VERTICAL : StaggeredGridLayoutManager.HORIZONTAL);
                mRecyclerView.setLayoutManager(staggeredGridLayoutManager);
                break;
            default:
                //设置布局管理器
                LinearLayoutManager layoutManager = new LinearLayoutManager(this);
                layoutManager.setOrientation(isVertical ? LinearLayoutManager.VERTICAL : LinearLayoutManager.HORIZONTAL);
                mRecyclerView.setLayoutManager(layoutManager);
                break;
        }
    }

这个大方法我们也是不需要重写的,这里是对recyclerview的布局形式进行初始化,让他知道我们是怎么设置他的布局形式的。

 /**
     * adapter内的处理
     *
     * @param baseViewHolder BaseViewHolder
     * @param t              泛型T
     */
    protected abstract void MyHolder(BaseViewHolder baseViewHolder, T t);
    public class YzsListAdapter extends BaseQuickAdapter<T, BaseViewHolder> {
        public YzsListAdapter(int layoutResId, List<T> data) {
            super(layoutResId, data);
        }
        @Override
        protected void convert(BaseViewHolder baseViewHolder, T t) {
            MyHolder(baseViewHolder, t);
        }
    }

最后这个Adapter和MyHolder方法,也就是我们缩减adapter的地方,我把他对子布局设置所需变量提取出来,变成强制方法,这样,我们就可以直接在这个方法中对adapter的控件进行控制了,而不用自己去声明出来一个adapter。 以上就是YzsBaseListActivity的所有源码,其实也不是很多,写这种Base其实不难,你只要掌握了这种封装思想,就可以写出比我更好的base了。

YzsBaseHomeActivity:

先给大家看下效果图: 这里写图片描述 这样的首页布局是不是很熟悉,回去点开你的微信,qq,今日头条,爱奇艺看看是不是都是这样的布局,只是有的有下方文字,有的没有,当然,我写的base这些方法必须提供,先不说别的,看看用普通的形式,我们要怎么样书写这种界面。

/**
 * des:主界面
 * Created by xsf
 * on 2016.09.15:32
 */
public class MainActivity extends BaseActivity {
    @Bind(R.id.tab_layout)
    CommonTabLayout tabLayout;
    private String[] mTitles = {"首页", "美女","视频","关注"};
    private int[] mIconUnselectIds = {
            R.mipmap.ic_home_normal,R.mipmap.ic_girl_normal,R.mipmap.ic_video_normal,R.mipmap.ic_care_normal};
    private int[] mIconSelectIds = {
            R.mipmap.ic_home_selected,R.mipmap.ic_girl_selected, R.mipmap.ic_video_selected,R.mipmap.ic_care_selected};
    private ArrayList<CustomTabEntity> mTabEntities = new ArrayList<>();
    private NewsMainFragment newsMainFragment;
    private PhotosMainFragment photosMainFragment;
    private VideoMainFragment videoMainFragment;
    private CareMainFragment careMainFragment;
    private static int tabLayoutHeight;
    /**
     * 入口
     * @param activity
     */
    public static void startAction(Activity activity){
        Intent intent = new Intent(activity, MainActivity.class);
        activity.startActivity(intent);
        activity.overridePendingTransition(R.anim.fade_in,
                com.jaydenxiao.common.R.anim.fade_out);
    }
    @Override
    public int getLayoutId() {
        return R.layout.act_main;
    }
    @Override
    public void initPresenter() {
    }
    @Override
    public void initView() {
        //此处填上在http://fir.im/注册账号后获得的API_TOKEN以及APP的应用ID
        UpdateKey.API_TOKEN = AppConfig.API_FIRE_TOKEN;
        UpdateKey.APP_ID = AppConfig.APP_FIRE_ID;
        //如果你想通过Dialog来进行下载,可以如下设置
//        UpdateKey.DialogOrNotification=UpdateKey.WITH_DIALOG;
        UpdateFunGO.init(this);
        //初始化菜单
        initTab();
    }
    @Override
    public void onCreate(Bundle savedInstanceState) {
        //切换daynight模式要立即变色的页面
        ChangeModeController.getInstance().init(this,R.attr.class);
        super.onCreate(savedInstanceState);
        //初始化frament
        initFragment(savedInstanceState);
        tabLayout.measure(0,0);
        tabLayoutHeight=tabLayout.getMeasuredHeight();
        //监听菜单显示或隐藏
        mRxManager.on(AppConstant.MENU_SHOW_HIDE, new Action1<Boolean>() {
            @Override
            public void call(Boolean hideOrShow) {
                startAnimation(hideOrShow);
            }
        });
    }
    /**
     * 初始化tab
     */
    private void initTab() {
        for (int i = 0; i < mTitles.length; i++) {
            mTabEntities.add(new TabEntity(mTitles[i], mIconSelectIds[i], mIconUnselectIds[i]));
        }
        tabLayout.setTabData(mTabEntities);
        //点击监听
        tabLayout.setOnTabSelectListener(new OnTabSelectListener() {
            @Override
            public void onTabSelect(int position) {
                SwitchTo(position);
            }
            @Override
            public void onTabReselect(int position) {
            }
        });
    }
    /**
     * 初始化碎片
     */
    private void initFragment(Bundle savedInstanceState) {
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        int currentTabPosition = 0;
        if (savedInstanceState != null) {
            newsMainFragment = (NewsMainFragment) getSupportFragmentManager().findFragmentByTag("newsMainFragment");
            photosMainFragment = (PhotosMainFragment) getSupportFragmentManager().findFragmentByTag("photosMainFragment");
            videoMainFragment = (VideoMainFragment) getSupportFragmentManager().findFragmentByTag("videoMainFragment");
            careMainFragment = (CareMainFragment) getSupportFragmentManager().findFragmentByTag("careMainFragment");
            currentTabPosition = savedInstanceState.getInt(AppConstant.HOME_CURRENT_TAB_POSITION);
        } else {
            newsMainFragment = new NewsMainFragment();
            photosMainFragment = new PhotosMainFragment();
            videoMainFragment = new VideoMainFragment();
            careMainFragment = new CareMainFragment();
            transaction.add(R.id.fl_body, newsMainFragment, "newsMainFragment");
            transaction.add(R.id.fl_body, photosMainFragment, "photosMainFragment");
            transaction.add(R.id.fl_body, videoMainFragment, "videoMainFragment");
            transaction.add(R.id.fl_body, careMainFragment, "careMainFragment");
        }
        transaction.commit();
        SwitchTo(currentTabPosition);
        tabLayout.setCurrentTab(currentTabPosition);
    }
    /**
     * 切换
     */
    private void SwitchTo(int position) {
        LogUtils.logd("主页菜单position" + position);
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        switch (position) {
            //首页
            case 0:
                transaction.hide(photosMainFragment);
                transaction.hide(videoMainFragment);
                transaction.hide(careMainFragment);
                transaction.show(newsMainFragment);
                transaction.commitAllowingStateLoss();
                break;
            //美女
            case 1:
                transaction.hide(newsMainFragment);
                transaction.hide(videoMainFragment);
                transaction.hide(careMainFragment);
                transaction.show(photosMainFragment);
                transaction.commitAllowingStateLoss();
                break;
            //视频
            case 2:
                transaction.hide(newsMainFragment);
                transaction.hide(photosMainFragment);
                transaction.hide(careMainFragment);
                transaction.show(videoMainFragment);
                transaction.commitAllowingStateLoss();
                break;
            //关注
            case 3:
                transaction.hide(newsMainFragment);
                transaction.hide(photosMainFragment);
                transaction.hide(videoMainFragment);
                transaction.show(careMainFragment);
                transaction.commitAllowingStateLoss();
                break;
            default:
                break;
        }
    }
    /**
     * 菜单显示隐藏动画
     * @param showOrHide
     */
    private void startAnimation(boolean showOrHide){
        final ViewGroup.LayoutParams layoutParams = tabLayout.getLayoutParams();
        ValueAnimator valueAnimator;
        ObjectAnimator alpha;
        if(!showOrHide){
             valueAnimator = ValueAnimator.ofInt(tabLayoutHeight, 0);
            alpha = ObjectAnimator.ofFloat(tabLayout, "alpha", 1, 0);
        }else{
             valueAnimator = ValueAnimator.ofInt(0, tabLayoutHeight);
            alpha = ObjectAnimator.ofFloat(tabLayout, "alpha", 0, 1);
        }
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                layoutParams.height= (int) valueAnimator.getAnimatedValue();
                tabLayout.setLayoutParams(layoutParams);
            }
        });
        AnimatorSet animatorSet=new AnimatorSet();
        animatorSet.setDuration(500);
        animatorSet.playTogether(valueAnimator,alpha);
        animatorSet.start();
    }
    /**
     * 监听全屏视频时返回键
     */
    @Override
    public void onBackPressed() {
        if (JCVideoPlayer.backPress()) {
            return;
        }
        super.onBackPressed();
    }
    /**
     * 监听返回键
     *
     * @param keyCode
     * @param event
     * @return
     */
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            moveTaskToBack(false);
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        //奔溃前保存位置
        LogUtils.loge("onSaveInstanceState进来了1");
        if (tabLayout != null) {
            LogUtils.loge("onSaveInstanceState进来了2");
            outState.putInt(AppConstant.HOME_CURRENT_TAB_POSITION, tabLayout.getCurrentTab());
        }
    }
    @Override
    protected void onResume() {
        super.onResume();
        UpdateFunGO.onResume(this);
    }
    @Override
    protected void onStop() {
        super.onStop();
        UpdateFunGO.onStop(this);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        ChangeModeController.onDestory();
    }
}

好了,就是这些代码,这个项目的是用mvp架构写的,我发出的这些截图只是在Activity里的代码,有几行代码与这个无关,我们先不用注意,但是你们有没有发现,很多代码重复的在显示隐藏控制着fragment。那我们可不可以省略他呢,当然是可以的,看看我是怎么写这个界面的。

/**
 * Author: 姚智胜
 * Version: V1.0版本
 * Description:  首页
 * Date: 2016/12/15.
 */
public class IndexActivity extends YzsBaseHomeActivity {
    private static final String TAG = "IndexActivity";
    private String[] mTitles = {"首页", "消息", "联系人", "更多"};
    private int[] mIconUnselectIds = {
            R.drawable.tab_home_unselect, R.drawable.tab_speech_unselect,
            R.drawable.tab_contact_unselect, R.drawable.tab_more_unselect};
    private int[] mIconSelectIds = {
            R.drawable.tab_home_select, R.drawable.tab_speech_select,
            R.drawable.tab_contact_select, R.drawable.tab_more_select};
    @Override
    protected void initContentView(Bundle bundle) {
        setContentView(R.layout.ac_index);
    }
    @Override
    protected void initTab() {
        setmFragments(new YzsBaseFragment[]{new HomeFragment(), new MsgFragment(), new PersonFragment(), new MoreFragment()});
        setmIconSelectIds(mIconSelectIds);
        setmIconUnSelectIds(mIconUnselectIds);
    }
    @Override
    protected void initLogic() {
        //一句话调用loading
        showLoadingDialog(LoadingDialog.YZS_LOADING, R.mipmap.icon);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                //取消loading
                cancelLoadingDialog();
            }
        }, 3000);
    }
    @Override
    protected void getBundleExtras(Bundle extras) {
    }
    @Override
    protected void onEventComing(EventCenter center) {
    }
}

没有了,就这些代码,其实我这样写都是写的多的,为了让你们能看清效果,如果追求最少行数的代码,没有文字的我算上设置Activity布局需要3行,有文字的需要4行,并且自动适配framelayout和viewpager,想想都觉得很激动呢。下面上源码解析。

  private static final String TAG = "YzsBaseHomeActivity";
    /**
     * title文字部分
     */
    private String[] mTitles;
    /**
     * 未选中图标数组
     */
    private int[] mIconUnSelectIds;
    /**
     * 选中图标数组
     */
    private int[] mIconSelectIds;
    /**
     * fragment集合
     */
    protected YzsBaseFragment[] mFragments;
    /**
     * 导航条
     */
    protected CommonTabLayout mTabLayout;
    /**
     * 图标信息对象
     */
    private ArrayList<CustomTabEntity> mTabEntities = new ArrayList<>();
    protected ViewPager mViewPager;
    protected FrameLayout mFrameLayout;
    private Bundle bundle;
     @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setBundle(savedInstanceState);
    }

首先我们要声明出我们所需要的变量,文字数组,图标数组,声明控件名等等,注释里写的很清楚。

 @Override
    protected void initView() {
        mTabLayout = (CommonTabLayout) findViewById(R.id.yzs_base_tabLayout);
        mViewPager = (ViewPager) findViewById(R.id.yzs_base_tabLayout_viewPager);
        mFrameLayout = (FrameLayout) findViewById(R.id.yzs_base_tabLayout_frameLayout);
        initTab();
        if (null == mFragments || mFragments.length == 0) {
            throw new RuntimeException("mFragments is null!");
        }
        initTabEntities();
        if (null == mTabLayout) {
            throw new RuntimeException("CommonTabLayout is null!");
        }
        if (null == mTitles || mTitles.length == 0) {
            mTabLayout.setTextsize(0);
        }
        if (null != mViewPager) {
            Log.e(TAG, "Choose_ViewPager");
            initViewpagerAdapter();
        } else {
            initFragments();
            Log.e(TAG, "Choose_frameLayout");
        }
        setTabSelect();
    }

接下来呢,我们要声明控件了,吧需要的都声明出来,和上面一样想使用直接在xml中使用它的id就可以了,放在那里随你心情,下面的方法就是初始化tab控件,初始化tab控件里的所需数据,相信看到这里的都应该明白我的套路了,我就是强制你重写这个方法,你能把我怎么样,省的你忘了。如果你不想设置文字,不用管title数据就可以,他就帮你干掉文字了,想使用就去给他赋值,暂时不支持一个有文字一个没文字这样的情况。最后一个if方法就是对viewpager和framelayout进行判断使用,你提供什么他就用什么,两个都提供了,就用viewpager,比较人性化,但是不建议这样写,因为这样写可能有bug。

 /**
     * 初始化图标图片文字fragment数据
     */
    private void initTabEntities() {
        if (null == mFragments || mFragments.length == 0 || mFragments.length != mIconSelectIds.length ||mFragments.length != mIconUnSelectIds.length) {
            throw new RuntimeException("mFragments is null!or Fragments and the number of ICONS do not meet");
        }
        for (int i = 0; i < mFragments.length; i++) {mTabEntities.add(new TabEntity(mTitles == null ? "" : mTitles[i], mIconSelectIds[i], mIconUnSelectIds[i]));
        }
        mTabLayout.setTabData(mTabEntities);
    }

下面就是初始化tab控件的对象属性了,把他需要的图标文字fragment放进去。

  /**
     * 初始化Fragments
     */
    private void initFragments() {
        //加载mFragments
        if (getBundle() == null) {
            //加载mFragments
            loadMultipleRootFragment(R.id.yzs_base_tabLayout_frameLayout, 1, mFragments);
        } else {
            // 这里库已经做了Fragment恢复,所有不需要额外的处理了, 不会出现重叠问题
            for (int i = 0; i < mFragments.length; i++) {
                Log.e(TAG, "initFragments" + i);
                mFragments[i] = findFragment(mFragments[i].getClass());
            }
        }
    }

这个方法注释写的很清楚,初始化fragment,我们在自己写首页切换fragment时,经常会出现重叠现象,但是用了这个base后就不会出现了,其实就是对fragment进行了一些处理,google的lib库在25之前的v4包fragment都有这个bug,但是如果你用25的就不会出现了,我也是用了一个开源库,专门封装了fragment,文章的最后把我用到的这几个开源库都告诉大家。


    /**
     * 初始化viewpager的adapter
     */
    private void initViewpagerAdapter() {
        mViewPager.setAdapter(new MyPagerAdapter(getSupportFragmentManager()));
        mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            }
            @Override
            public void onPageSelected(int position) {
                mTabLayout.setCurrentTab(position);
            }
            @Override
            public void onPageScrollStateChanged(int state) {
            }
        });
    }

同样,注释写的很清楚,初始化viewpager的adapter,用framelayout时不会使用这个方法,下面是对viewpager的监听去设置tab的位置。

    /**
     * 为mTabLayout设置监听
     */
    private void setTabSelect() {
        Log.e(TAG, "setTabSelect");
        mTabLayout.setOnTabSelectListener(new OnTabSelectListener() {
            @Override
            public void onTabSelect(int position) {
                if (null != mViewPager) {
                    mViewPager.setCurrentItem(position);
                } else {
                    int toDoHidden = -1;
                    for (int i = 0; i < mFragments.length; i++) {
                        if (!mFragments[i].isHidden()) {
                            toDoHidden = i;
                            Log.e(TAG, "查找显示中的fragment-------" + toDoHidden);
                        }
                    }
                    Log.e(TAG, "选中的fragment-------" + position);
                    Log.e(TAG, "确定显示中的fragment-------" + toDoHidden);
                    showHideFragment(mFragments[position], mFragments[toDoHidden]);
                }
            }
            @Override
            public void onTabReselect(int position) {
                if (position == 0) {
                    Log.e(TAG, "再次选中项" + position);
                }
            }
        });
    }

这两个方法是对导航条进行监听,分别是第一次点击和重复点击方法,如果有需要,自己在里面写你要的方法。

 /**
     * 设置TabLayout属性,所有关于TabLayout属性在这里设置
     */
    protected abstract void initTab();
    /**
     * 获取Fragment数组
     *
     * @return mFragments
     */
    public YzsBaseFragment[] getmFragments() {
        return mFragments;
    }
    /**
     * 放入Fragment数组(必须继承YzsBaseFragment)
     *
     * @param mFragments
     */
    public void setmFragments(YzsBaseFragment[] mFragments) {
        this.mFragments = mFragments;
    }
    /**
     * 获取选中图标数组
     *
     * @return mIconSelectIds
     */
    public int[] getmIconSelectIds() {
        return mIconSelectIds;
    }
    /**
     * 放入选中图标数组
     *
     * @param mIconSelectIds
     */
    public void setmIconSelectIds(int[] mIconSelectIds) {
        this.mIconSelectIds = mIconSelectIds;
    }
    /**
     * 获取未选中图标数组
     *
     * @return mIconUnSelectIds
     */
    public int[] getmIconUnSelectIds() {
        return mIconUnSelectIds;
    }
    /**
     * 放入未选中图标数组
     *
     * @param mIconUnSelectIds
     */
    public void setmIconUnSelectIds(int[] mIconUnSelectIds) {
        this.mIconUnSelectIds = mIconUnSelectIds;
    }
    /**
     * 获取mTitles数组
     *
     * @return mTitles
     */
    public String[] getmTitles() {
        return mTitles;
    }
    /**
     * 放入mTitles数组
     *
     * @param mTitles
     */
    public void setmTitles(String[] mTitles) {
        this.mTitles = mTitles;
    }

强制重写方法初始化tab,同样,初始化他的方法都写在这里,下面是提供给你的设置和获取fragment的方法,设置和获取图标文字的方法。

 public Bundle getBundle() {
        return bundle;
    }
    public void setBundle(Bundle bundle) {
        this.bundle = bundle;
    }
    private class MyPagerAdapter extends FragmentPagerAdapter {
        public MyPagerAdapter(FragmentManager fm) {
            super(fm);
        }
        @Override
        public int getCount() {
            return mFragments.length;
        }
        @Override
        public CharSequence getPageTitle(int position) {
            return mTitles == null ? "" : mTitles[position];
        }
        @Override
        public Fragment getItem(int position) {
            return mFragments[position];
        }
    }

这个是viewpager的adpter内部类,有了他,你也不用去关心viewpager的adapter了,现在还有一些设置导航条的方法没有写在里面,我会继续完善它,控件本来就支持的,我要做的就是把它封装起来,给你们初始化和一些情况的调用,比如导航条理出现小红点,提示数字,这些都是支持的。

以上就是目前YzsBaseHomeActivity的封装了,思想和YzsBaseListActivity一样,就是分模块的强制你在指定方法里做指定的事情,重复的事情交给我来处理,让代码来完成。这些就是我对baseActivity的理解,希望可以帮到大家,抛砖引玉,我相信你们会比我写的更好。

下面是我的项目中用到的第三方开源库

1.FlycoTabLayout——viewpager指示器 与 导航栏控件

2.fragmentation——为"单Activity + 多Fragment","多模块Activity + 多Fragment"架构而生,替代官方fragment

3.eventbus——事件总线

4.BaseRecyclerViewAdapterHelper——RecyclerView的强大的BaseAdapter

封装结束,继承我们写的base就可以几行代码写完一个界面了, 是不是很爽。当然,我写的这些可能不是特别完美,如果大家有更好的意见,可以提出来,我会改进,并且baseactivity会继续更新,把大家在app中发现大量出现的界面都会写进来,让你几行就搞定。这片博客就到这里结束了,希望大家可以让我这个抛砖人的功夫没白花,让你们有自己的思路。

最后再次安利下我的两个库

YzsLib

一个共享的开发框架

https://github.com/yaozs/YzsLib

YzsBaseActivity

BaseActivity的框架

https://github.com/yaozs/YzsBaseActivity

下次再见嘿嘿 (^o^)/YES!