打造自己的下拉刷新库(Ultra-Pull-To-Refresh)(二)
来源:互联网 发布:java数组转字符串 编辑:程序博客网 时间:2024/06/06 12:35
在开始本篇阅读前,建议大家先看下上一篇《 打造自己的下拉刷新库(Ultra-Pull-To-Refresh)(一)》,主要是由于本篇很多接口和设计都在上一篇提到,在这里不会做过多展开。
在本系列的上一篇文章中,我们为大家分析了整个下拉刷新库的结构,其中最关键的就是我们将Ultra-PTR封装到了PullToRefreshBaseView基类中,为我们给各种view实现下拉刷新提供了便利的接入。那么今天我们继续给大家呈上PullToRefreshRecyclerView的打造过程,继承PullToRefreshBaseView基类轻松地为RecyclerView实现下拉刷新的功能。
由于之前业务上的需求,PullToRefreshRecyclerView目前只支持LinearLayoutManager的布局方式,也就是说用RecyclerView实现ListView的模式。在后续有时间会考虑接入阿里前段时间开源的VLayout,也能非常轻松的实现各种样式的RecyclerView。
这章节的PullToRefreshRecyclerView,主要实现下拉刷新、上拉加载、数据自动装载刷新、封装统一的adapter、模拟ListView的简单分割线这几项功能。
开始
首先我们继承PullToRefreshBaseView基类创建 一个PullToRefreshRecyclerView,实现onInitContent方法,在其中返回我们要实现的内部容器RecyclerView。
@Override public View onInitContent() { mRecyclerView = new RecyclerView(getContext()); mRecyclerView.setLayoutParams(new RecyclerView.LayoutParams(-1, -1)); return mRecyclerView; }
其次,我们初始化刷新的默认头部、底部,给RecyclerView配置LaytouManager,这样就完成了基本的封装。
private void initView() { setDefaultLoadingHeaderView(); setDefaultLoadingFooterView(); setOnRefreshListener(this); mLinearLayoutManager = new LinearLayoutManager(getContext()); mRecyclerView.setLayoutManager(mLinearLayoutManager); mRecyclerView.setHasFixedSize(true); //确定每个item高度相同,提高性能 mRecyclerView.setAdapter(new EmptyRecyclerViewAdapter(getContext())); }
其中setOnRefreshListener设置的是要实现下拉刷新和上拉加载两个监听。
EmptyRecyclerViewAdapter是继承RecyclerView.Adapter实现的一个空的adapter,因为在实际调用过程中,会产生一个警告:
“Recycler View..No adapter attached: skipping layout”
在网上查到的资料中显示,是因为网络请求的数据还没回来就调用了notifyDataSetChanged()导致的,这里只需要加个EmptyRecyclerViewAdapter即可解决。
好啦,写到这里,实际上我们已经实现了RecyclerView下拉刷新、上拉加载的简单封装。现在的PullToRefreshRecyclerView已经具备刷新的功能,通过实现onPullDownToRefresh方法,可以捕获下拉事件;通过实现onPullUpToRefresh方法,可以捕获上拉事件。直接操作mRecyclerView即可实现数据填充、增加list头部底部等。
当然我们的追求远不止那么简单,直接操作mRecyclerView显然不是我们的风格,所以我们进一步对mRecyclerView进行封装处理。
分割线Divider
设置分割线,我们希望能像下面这么简单地调个方法,即可设置item之间间隔的宽度和颜色。
mPullRefreshRecyclerView.setDivider(R.dimen.dp_07, R.color.default_dividing_line);
当然也支持自定义RecyclerView.ItemDecoration。
mPullRefreshRecyclerView.setDivider(mItemDecoration);
首先RecyclerView提供了RecyclerView.ItemDecoration抽象类给我们自定义分割线,实现onDraw方法,利用Canvas绘画即可。
public class PTRRecyclerViewDecoration extends RecyclerView.ItemDecoration { private Drawable mDivider; private int dividerHeight; private int dividerWidth; private int mOrientation; public boolean isHadHeader = false; public boolean isHadFooter = false; public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL; public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL; public PTRRecyclerViewDecoration(Context context, int orientation, Drawable drawable) { this.mDivider = drawable; this.dividerHeight = mDivider != null ? mDivider.getIntrinsicHeight() : 0; this.dividerWidth = mDivider != null ? mDivider.getIntrinsicWidth() : 0; setOrientation(orientation); } public PTRRecyclerViewDecoration(Context context, int orientation, Drawable drawable, int dividerHeight) { this.mDivider = drawable; this.dividerHeight = dividerHeight; this.dividerWidth = mDivider != null ? mDivider.getIntrinsicWidth() : 0; setOrientation(orientation); } //设置屏幕方向 public void setOrientation(int orientation) { if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) { throw new IllegalArgumentException("invalid orientation"); } mOrientation = orientation; } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { if (parent.getChildCount() > 2) { if (mOrientation == HORIZONTAL_LIST) { drawVerticalLine(c, parent, state); } else { drawHorizontalLine(c, parent, state); } } } //横向 public void drawHorizontalLine(Canvas c, RecyclerView parent, RecyclerView.State state) { if (parent.getAdapter() == null) { return; } int left = parent.getPaddingLeft(); int right = parent.getWidth() - parent.getPaddingRight(); final int childCount = parent.getChildCount(); int dataEndPosition = parent.getAdapter().getItemCount(); for (int i = 1; i < childCount - 1; i++) { if (mDivider == null) { break; } View child = parent.getChildAt(i); int position = parent.getChildAdapterPosition(child); //获取child的布局信息 final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); final int top = child.getBottom() + params.bottomMargin; int bottom = top + dividerHeight; //处理第一个HeaderView、最后一个FooterView分割线 if ((isHadHeader && position <= 1) || (isHadFooter && position == dataEndPosition - 1)) { bottom = top; } mDivider.setBounds(left, top, right, bottom); mDivider.draw(c); } } //竖向 public void drawVerticalLine(Canvas c, RecyclerView parent, RecyclerView.State state) { if (parent.getAdapter() == null) { return; } int top = parent.getPaddingTop(); int bottom = parent.getHeight() - parent.getPaddingBottom(); final int childCount = parent.getChildCount(); int dataEndPosition = parent.getAdapter().getItemCount(); for (int i = 1; i < childCount - 1; i++) { if (mDivider == null) { break; } View child = parent.getChildAt(i); int position = parent.getChildAdapterPosition(child); //获取child的布局信息 final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); final int left = child.getRight() + params.rightMargin; int right = left + mDivider.getIntrinsicWidth(); //处理第一个HeaderView、最后一个FooterView分割线 if ((isHadHeader && position <= 1) || (isHadFooter && position == dataEndPosition - 1)) { right = left; } mDivider.setBounds(left, top, right, bottom); mDivider.draw(c); } } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { if (mOrientation == HORIZONTAL_LIST) { outRect.set(0, 0, dividerWidth, 0); } else { outRect.set(0, 0, 0, dividerHeight); } }}
核心主要还是绘图的代码,这里就不做展开,如果有一些复杂需求的分割线,可以网上搜搜,这方面的资料还是挺多的。
这里主要提一下两个关键的标记变量isHadHeader和isHadFooter。由于我们在下面要加入HeaderView和FooterView的支持,而我们在增加RecyclerView头部和底部的时候,都是不希望加入分割线的。所以这用两个标记变量isHadHeader和isHadFooter来判断,当前是否有加入头部/底部,从而“隐藏”分割线。
有了自定义的PTRRecyclerViewDecoration,我们就实现上面的setDivider方法了。
public void setDivider(int padding, int divider) { if (padding > 0 && divider >= 0) { Drawable _divider = divider != 0 ? getResources().getDrawable(divider) : null; myDecoration = new PTRRecyclerViewDecoration(getContext(), PTRRecyclerViewDecoration.VERTICAL_LIST, _divider, (int) getResources().getDimension(padding)); mRecyclerView.addItemDecoration(myDecoration); } }
优雅地添加HeaderView和FooterView
RcyclerView本身是不提供添加HeaderView和FooterView方法的,需要使用RecyclerView.Adapter来实现。而如果我们直接在我们已经实现的adapter上修改,增加头部和底部,这样我们需要为每个adapter加入同样的代码,显然不符合我们的封装思想。
这一节我们参考了鸿洋大神的下面这篇文章。
Android 优雅的为RecyclerView添加HeaderView和FooterView
采用装饰者模式的思想,给adapter包装一层,专门负责管理头部、底部的添加和删除。
/** * HeaderAndFooterWrapper .java */public class HeaderAndFooterWrapper extends RecyclerView.Adapter{ private static final int BASE_ITEM_TYPE_HEADER = 100000; private static final int BASE_ITEM_TYPE_FOOTER = 200000; private SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>(); private SparseArrayCompat<View> mFooterViews = new SparseArrayCompat<>(); private RecyclerView.Adapter mInnerAdapter; public HeaderAndFooterWrapper(RecyclerView.Adapter adapter) { mInnerAdapter = adapter; } private boolean isHeaderViewPos(int position) { return position < getHeadersCount(); } private boolean isFooterViewPos(int position) { return position >= getHeadersCount() + getRealItemCount(); } public void addHeaderView(View view) { mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view); } public void addFooterView(View view) { mFooterViews.put(mFooterViews.size() + BASE_ITEM_TYPE_FOOTER, view); } public void addHeaderView(List<View> view) { for (int i = 0; i < view.size(); i++) { addHeaderView(view.get(i)); } } public void addFooterView(List<View> view) { for (int i = 0; i < view.size(); i++) { addFooterView(view.get(i)); } } public void removeFooterView(View view) { int idx = mFooterViews.indexOfValue(view); if (idx != -1) { mFooterViews.removeAt(idx); } } public int getHeadersCount() { return mHeaderViews.size(); } public int getFootersCount() { return mFooterViews.size(); } private int getRealItemCount() { return mInnerAdapter.getItemCount(); } @Override public int getItemViewType(int position) { if (isHeaderViewPos(position)) { return mHeaderViews.keyAt(position); }else if (isFooterViewPos(position)) { return mFooterViews.keyAt(position - getHeadersCount() - getRealItemCount()); } return mInnerAdapter.getItemViewType(position - getHeadersCount()); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (mHeaderViews.get(viewType) != null) { View headerView = mHeaderViews.get(viewType); headerView.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)); CommonViewHolder myViewHolder = new CommonViewHolder(headerView); return myViewHolder; }else if (mFooterViews.get(viewType) != null) { View footerView = mFooterViews.get(viewType); footerView.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)); CommonViewHolder myViewHolder = new CommonViewHolder(footerView); return myViewHolder; } return mInnerAdapter.onCreateViewHolder(parent, viewType); } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (isHeaderViewPos(position)) { return; }else if (isFooterViewPos(position)) { return; } mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount()); } @Override public int getItemCount() { return getHeadersCount() + getFootersCount() + getRealItemCount(); }}
有了HeaderAndFooterWrapper这个包装类,我们就可以增加一些方法,来管理PullToRefreshRecyclerView拥有的HeaderView和FooterView。
private List<View> mHeaderViewList; private List<View> mFooterViewList; public void addHeaderView(View headerView) { if (mHeaderViewList == null){ mHeaderViewList = new ArrayList<>(); } mHeaderViewList.add(headerView); //指定分割线标记,当前拥有头部 if(myDecoration != null) { myDecoration.isHadHeader = true; } } /** * 增加FooterView */ public void addFooterView(View footerView) { if (mFooterViewList == null){ mFooterViewList = new ArrayList<>(); } mFooterViewList.add(footerView); //指定分割线标记,当前拥有底部 if(myDecoration != null) { myDecoration.isHadFooter = true; } RecyclerView.Adapter adapter = mRecyclerView.getAdapter(); if (adapter != null && !(adapter instanceof EmptyRecyclerViewAdapter)) { if (adapter instanceof HeaderAndFooterWrapper) { ((HeaderAndFooterWrapper) adapter).addFooterView(footerView); adapter.notifyDataSetChanged(); }else { HeaderAndFooterWrapper headerAndFooterWrapper = new HeaderAndFooterWrapper(adapter); headerAndFooterWrapper.addFooterView(footerView); mRecyclerView.setAdapter(headerAndFooterWrapper); } } } /** * 移除FooterView */ public void removeFooterView(View footerView) { RecyclerView.Adapter adapter = mRecyclerView.getAdapter(); if (adapter != null && (adapter instanceof HeaderAndFooterWrapper)) { ((HeaderAndFooterWrapper) adapter).removeFooterView(footerView); if (mFooterViewList != null && mFooterViewList.indexOf(footerView) != -1) { mFooterViewList.remove(footerView); } } } /** * 包装adapter,增加HeaderView,FooterView */ private RecyclerView.Adapter getWrappedListAdapter(RecyclerView.Adapter adapter) { if ((mHeaderViewList != null && mHeaderViewList.size() != 0) || (mFooterViewList != null && mFooterViewList.size() != 0)) { HeaderAndFooterWrapper headerAndFooterWrapper = new HeaderAndFooterWrapper(adapter); //增加HeaderView if (mHeaderViewList != null && mHeaderViewList.size() != 0) { headerAndFooterWrapper.addHeaderView(mHeaderViewList); } //增加FooterView if (mFooterViewList != null && mFooterViewList.size() != 0) { headerAndFooterWrapper.addFooterView(mFooterViewList); } return headerAndFooterWrapper; } return adapter; }
一键式数据装载与Item布局
上一篇文章中,我们提到了OnPullListActionListener接口,提供数据加载、item点击、item初始化、刷新完成的回调方法。主要用于在封装统一adapter的时候,使界面只需要关系接口请求和界面布局,实现一键式的数据装载与item布局指定。
/** * OnPullListActionListener .java */public interface OnPullListActionListener<T> { void loadData(int pageIndex, String tips); void clickItem(T item, int position); void createListItem(ViewHolder holder, T currentItem, List<T> list, int position); void onRefreshComplete();}
- loadData:发起获取数据请求
- clickItem:item点击事件
- createListItem:初始化item布局,其中ViewHolder是统一View控制器,避免要定义一系列view的变量;currentItem是当前item的数据
- onRefreshComplete:加载完成事件
/** * ViewHolder.java */public class ViewHolder { private final SparseArray<View> mViews; private View mConvertView; private OnClickListener mOnClickListener; public ViewHolder(View parent) { mConvertView = parent; mViews = new SparseArray<View>(); } public ViewHolder(View parent, OnClickListener clickListener) { mOnClickListener = clickListener; mConvertView = parent; mViews = new SparseArray<View>(); } private ViewHolder(Context context, ViewGroup parent, int layoutId) { this.mViews = new SparseArray<View>(); mConvertView = LayoutInflater.from(context).inflate(layoutId, parent, false); mConvertView.setTag(this); } public static ViewHolder get(Context context, View convertView, ViewGroup parent, int layoutId, int position) { if (convertView == null) { return new ViewHolder(context, parent, layoutId); } return (ViewHolder) convertView.getTag(); } public View getConvertView() { return mConvertView; } public <T extends View> T getView(int viewId) { View view = mViews.get(viewId); if (view == null) { view = mConvertView.findViewById(viewId); mViews.put(viewId, view); } return (T) view; } public View setOnClickListener(int viewId) { View view = getView(viewId); setClickListener(view); return view; } public TextView setText(int viewId, CharSequence text) { TextView view = getView(viewId); if (view != null) { view.setText(text); } return view; } public TextView setTextColor(int viewId, int Color) { TextView view = getView(viewId); if (view != null) { view.setTextColor(Color); } return view; } public ImageView setImageResource(int viewId, int drawableId) { ImageView view = getView(viewId); if (view != null) { view.setImageResource(drawableId); } return view; } public View setBackgroundResource(int viewId, int drawableId) { View view = getView(viewId); if (view != null) { view.setBackgroundResource(drawableId); } return view; } public View setVisibility(int viewId, int visibility) { View view = getView(viewId); view.setVisibility(visibility); return view; } public int getVisibility(int viewId) { View view = getView(viewId); return view.getVisibility(); } public void setClickListener(View view) { if (mOnClickListener != null && view != null) { view.setOnClickListener(mOnClickListener); } } public void setClickListener(OnClickListener clickListener) { mOnClickListener = clickListener; }}
我们有了ViewHolder对View的统一控制,就可以在adapter中使用起来,继承RecyclerView.Adapter我们可以创建一个公共的CommonBaseAdapter,封装CommonViewHolder用来包装上面的ViewHolder,即可实现一个通用的Adapter,而不需要自己每次单独实现RecyclerView.ViewHolder。
/** * CommonViewHolder.java * 这部分代码非常简单,就封装了一个上面的ViewHolder */public class CommonViewHolder extends RecyclerView.ViewHolder { public CommonViewHolder(View view) { super(view); } ViewHolder viewHolder;}
公共的CommonBaseAdapter。
/** * CommonBaseAdapter.java */public abstract class CommonBaseAdapter<T> extends RecyclerView.Adapter<CommonViewHolder>{ private Context mContext; private List<T> mData; protected final int mItemLayoutId; public CommonBaseAdapter(Context context, List<T> mData, int itemLayoutId) { this.mContext = context; this.mItemLayoutId = itemLayoutId; this.mData = mData; } @Override public CommonViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { ViewHolder viewHolder = getViewHolder(0, null, parent); CommonViewHolder myViewHolder = new CommonViewHolder(viewHolder.getConvertView()); myViewHolder.viewHolder = viewHolder; return myViewHolder; } @Override public void onBindViewHolder(final CommonViewHolder holder, final int position) { holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onItemClick(holder.itemView, position); } }); convert(holder.viewHolder, mData.get(position), mData, position); } private ViewHolder getViewHolder(int position, View convertView, ViewGroup parent) { return ViewHolder.get(mContext, convertView, parent, mItemLayoutId, position); } protected abstract void convert(ViewHolder holder, T item,List<T> list,int position); protected abstract void onItemClick(View itemView, int position); @Override public int getItemCount() { return mData.size(); }}
这样,我们可以在PullToRefreshRecyclerView创建一个内部类MyListAdapter,用来实现CommonBaseAdapter,从而将adapter内的创建、点击事件通过OnPullListActionListener传递到上层。
private class MyListAdapter extends CommonBaseAdapter<T> { public MyListAdapter(Context context, List<T> mData, int itemLayoutId) { super(context, mData, itemLayoutId); } @Override protected void onItemClick(View itemView, int position) { if (position >= 0 && mList.size() > 0) { T item = mList.get(position); if (mOnPullListActionListener != null && item != null) { int numHeaderView = mHeaderViewList != null ? mHeaderViewList.size() : 0; mOnPullListActionListener.clickItem(item, position + numHeaderView); } } } @Override protected void convert(ViewHolder holder, T item, List<T> list, int position) { if (mOnPullListActionListener != null && item != null) { mOnPullListActionListener.createListItem(holder, item, list, position); } } }
这里,我们先简单回顾一下,看看我们上面都实现了哪些功能。
- 添加分割线:setDivider(int padding, int divider)
- 添加HeaderView:addHeaderView(View headerView),
- 添加FooerView:addFooterView(View footerView)
- 移除FooterView:removeFooterView(View footerView)
- 初始化Item布局:通过接口OnPullListActionListener的createListItem,可以拿到ViewHolder,轻松实现布局和填充item的数据
- Item点击:同样通过接口OnPullListActionListener的clickItem,可以捕获Item点击事件
一切似乎都已经非常强大了,但貌似还漏了些什么。没错万事具备,只欠东风,我们还缺少了最关键的加载数据,和数据展示。不急,马上为您呈上!
OnPullListActionListener接口还有一个关键的loadData()方法,这当然就是用来为我们加载数据调用的。
/** * 下拉刷新加载数据 */ public void loadRefreshData(boolean isShowTops) { String tips = isShowTops ? TIPS_LOAD_DATA : ""; mPageIndex = 1; if (mOnPullListActionListener != null) { mOnPullListActionListener.loadData(mPageIndex, tips); } } /** * 上拉刷新加载更多数据 */ public void loadMoreData(int taskId, boolean isShowTops) { String tips = isShowTops ? TIPS_LOAD_DATA : ""; if (mOnPullListActionListener != null) { mOnPullListActionListener.loadData(mPageIndex, tips); } }
在上面我们已经实现了对adapter的一个包装类MyListAdapter,因此这里可以很简单的实现数据的装载与刷新。
/** * 显示数据 * 传入数据数组list,和指定的item布局itemLayoutId */ public void showAllData(List<T> list, int itemLayoutId) { if (commonBaseAdapter == null) { commonBaseAdapter = new MyListAdapter(getContext(), list, itemLayoutId); mRecyclerView.setAdapter(getWrappedListAdapter(commonBaseAdapter)); } else { getAdapter().notifyDataSetChanged(); } }
写在最后
到这里,我们就将PullToRefreshRecyclerView封装RecyclerView的打造过程,完整地分析给了大家。完整的源码这里就不再贴出来了,我们在上一篇已经贴给大家了。
其实封装PullToRefreshRecyclerView的时候,更多是从我们项目的需求出发,所以我们暂时只实现了LinearLayoutManager列表式布局,在后面如果有时间,我打算接入阿里的VLayout,这样就能实现各种样式的下拉刷新RecyclerView。这里大家如果有兴趣,也可以继承PullToRefreshBaseView尝试自己实现一下,欢迎一起交流,共同学习!
- 打造自己的下拉刷新库(Ultra-Pull-To-Refresh)(二)
- 打造自己的下拉刷新库(Ultra-Pull-To-Refresh)(一)
- 下拉刷新库android-Ultra-Pull-To-Refresh的使用
- android-Ultra-Pull-to-Refresh下拉刷新
- Ultra Pull To Refresh使用(自定义下拉刷新的头部)
- 下拉刷新框架Android-Ultra-Pull-To-Refresh的使用
- 使用 Ultra Pull To Refresh 定制自己的下拉刷新头部
- 万能刷新库(android-Ultra-Pull-To-Refresh )
- 常用的刷新技术(二)——Ultra-Pull-To-Refresh
- 4.2.5 Android 下拉刷新的几个方法:SwipeRefreshLayout,android-Ultra-Pull-To-Refresh(ptr),PullToRefreshListView
- 4.5.3 Go Android 下拉刷新的整理:SwipeRefreshLayout,android-Ultra-Pull-To-Refresh(ptr),PullToRefreshListView
- android下拉刷新android-Ultra-Pull-To-Refresh使用
- 下拉刷新框架android-Ultra-Pull-To-Refresh示例
- Ultra Pull To Refresh 下拉刷新 替代PullToRefresh
- Ultra-Pull-To-Refresh实现下拉刷新上拉加载
- android-Ultra-Pull-To-Refresh实现下拉刷新WebView
- Ultra-Pull-To-Refresh 自定义下拉刷新视差动画
- 使用 android-Ultra-Pull-To-Refresh 实现 WebView 下拉刷新
- 为什么Hibernate建议你的实体类实现hashCode和equals方法?
- 嵌入式Linux入门:概述
- 往同一张表中的不同字段分别添加图片路径(既多张图路径上传到不同的字段上)
- secureCRT 7.0远程CentOS6.5时setup命令乱码问题
- Moving Averages(2): What Are They?
- 打造自己的下拉刷新库(Ultra-Pull-To-Refresh)(二)
- Linux版SMB远程代码执行漏洞(CVE-2017-7494)-SambaCry 分析报告
- 1025: 最大字符
- 打印回环数字
- JS学习笔记(5)内置对象
- nginx 简单负载和反向代理
- 空壳邮件
- jdk-ConcurrentLinkedQueue(一)
- 1026: 字符类型判断