RecyclerView下拉刷新和加载更多
来源:互联网 发布:威客网络兼职网站 编辑:程序博客网 时间:2024/06/05 08:59
之前一直写的是ListVIew下拉刷新,但是好多朋友都说要RecycleView的下拉刷新和滑动加载,其实,这个原理都是差不多。抽空,我就写了下RecycleView的下拉刷新和滑动加载更多。因此,这才写到博客里,记录一下。
在大家阅读这篇博客前,大家需要了解的知识
1.Scroller。实现弹性滑动的类,这个是经常用到的,不懂的请自觉先学习Scroller的知识。
2.事件分发机制。事件是以ACTION_DOWN开始到ACTION_UP货ACTION_CANCEL结束的一个序列,期间事件分发,能够通过onInterceptTouchEvent方法和dispatchTouchEvent进行事件的阻止和消费。
3.RecyclerView的基本使用。比如如何添加一个Decoration.
4.onSizeChange的触发时机。onSizeChange()在View的layout中触发,它执行在所有控件的onMeasure()之后,因此可以直接获取到控件的测量长和宽。
整体的思路:采用的是LinearLayout+RecyclerView的组合,在LinearLayout中添加HeaderView和FooterView。当RecyclerView滑动到了最顶部,则可以触发下拉事件;当RecyclerView滑动到了底部,则可以触发滑动加载更多的事件。然后在通过事件分发,进行滑动事件的处理。
先看一下效果:
下面是自定义View的代码,后面会逐一分析代码块:
package com.mjc.recyclerviewdemo.refresh;import android.content.Context;import android.graphics.Color;import android.support.v7.widget.LinearLayoutManager;import android.support.v7.widget.RecyclerView;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.ViewConfiguration;import android.widget.LinearLayout;import android.widget.Scroller;import com.mjc.recyclerviewdemo.CustomItemDecoration;/** * Created by mjc on 2016/3/11. */public class PullToRefreshRecycleView extends LinearLayout { //头部 private IHeaderView mHeaderView; private int mHeaderHeight; //尾部 private CustomFooterView mFooterView; private int mFooterHeight; //阻尼系数,越大,阻力越大 public static final float RATIO = 0.5f; //当前是否阻止事件 private boolean isIntercept; //是否正在刷新 private boolean isRefreshing; //滑动类 private Scroller mScroller; //刷新的View private RecyclerView mRefreshView; private Context mContext; private int mMaxScrollHeight; private boolean isFirst = true; public static final int NORMAL = 0; public static final int PULL_TO_REFRESH = 1; public static final int RELEASE_TO_REFRESH = 2; public static final int REFRESING = 3; private int mCurrentState; private int mTouchSlop; private OnRefreshListener listener; private boolean isPullDownMotion; private int lastVisible; public PullToRefreshRecycleView(Context context) { super(context); init(context); } public PullToRefreshRecycleView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } private void init(Context context) { mContext = context; this.setOrientation(VERTICAL); mRefreshView = getRefreshView(); mRefreshView.setBackgroundColor(Color.WHITE); LayoutParams listParams = new LayoutParams(-1, -1); mRefreshView.setLayoutParams(listParams); addView(mRefreshView); //添加HeaderView mHeaderView = new CustomHeaderView(context); LayoutParams params = new LayoutParams(-1, -2); mHeaderView.setLayoutParams(params); addView(mHeaderView, 0); //添加FooterView mFooterView = new CustomFooterView(context); LayoutParams fParams = new LayoutParams(-1, 200); mFooterView.setLayoutParams(fParams); addView(mFooterView, -1); //弹性滑动实现 mScroller = new Scroller(context); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); //第一次获取相关参数,并隐藏HeaderView,FooterView if (isFirst) { mHeaderHeight = mHeaderView.getMeasuredHeight(); mMaxScrollHeight = mHeaderHeight * 3; resetHeaderLayout(-mHeaderHeight); mFooterHeight = mFooterView.getMeasuredHeight(); resetFooterLayout(-mFooterHeight); Log.v("@mHeaderHeight", mHeaderHeight + ""); Log.v("@mFooterHeight", mFooterHeight + ""); isFirst = false; } } private void resetHeaderLayout(int offset) { LayoutParams params = (LayoutParams) mHeaderView.getLayoutParams(); params.topMargin = offset; mHeaderView.requestLayout(); } private void resetFooterLayout(int offset) { LayoutParams params = (LayoutParams) mFooterView.getLayoutParams(); params.bottomMargin = offset; mFooterView.requestLayout(); } //按下时的位置,当事件被阻止时,第一次ActionDown事件,onTouchEvent无法获取这个位置 //需要在onInterceptTouchEvent获取 private float downY; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { //如果当前是正在刷新并且是下拉状态,则当前视图处理事件 if (isRefreshing && mScrollY < 0) { return true; } //如果当前是刷新状态,并且处于上拉状态,则视图不可进入下拉状态 if (mScrollY >= 0 && isRefreshing) return false; boolean isIntercept = false; int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: downY = ev.getY(); break; case MotionEvent.ACTION_MOVE: //如果达到了滑动条件 if (Math.abs(ev.getY() - downY) >= mTouchSlop) { if (ev.getY() - downY > 0) {//下拉 isIntercept = isEnablePullDown(); if (isIntercept)//设置下拉还是上滑的状态,true表示下拉动作 isPullDownMotion = true; } else {//上滑 isIntercept = isEnableLoadMore(); if (isIntercept)//false表示上滑状态 isPullDownMotion = false; } } else { isIntercept = false; } break; case MotionEvent.ACTION_CANCEL: //如果返回true,子视图如果包含点击事件,则无法进行处理 isIntercept = false; break; case MotionEvent.ACTION_UP: isIntercept = false; break; } return isIntercept; } //记录当前滑动的位置 private int mScrollY; @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: //第一次判断时,downY只能从intercept中获取,之后从这里获取 downY = event.getY(); break; case MotionEvent.ACTION_MOVE: float dY = event.getY() - downY; if (isPullDownMotion)//下拉 doPullDownMoveEvent(dY); else {//自动加载更多 doLoadMoreEvent(dY); } break; case MotionEvent.ACTION_UP: if (isPullDownMotion) { //处理下拉结果 doPullDownResult(); } else { //处理滑动加载更多结果 doLoadMoreResult(); } break; case MotionEvent.ACTION_CANCEL: //同ACTION_UP if (isPullDownMotion) { doPullDownResult(); } else { doLoadMoreResult(); } break; } return true; } /** * 处理加载更多 */ private void doLoadMoreResult() { //手指松开时,如果FooterView,没有完全滑动出来,自动滑动出来 scrollTo(0, mFooterHeight); mScrollY = getScrollY(); if (!isRefreshing) { isRefreshing = true; if (listener != null) listener.onLoadMore(); } } /** * 加载更多完成后调用 */ public void completeLoadMore() { scrollTo(0, 0); mScrollY = 0; isRefreshing = false; LinearLayoutManager manager = (LinearLayoutManager) mRefreshView.getLayoutManager(); int count = manager.getItemCount(); if (count > lastVisible + 1)//加载了更多数据 mRefreshView.scrollToPosition(lastVisible + 1); } //处理加载更多 private void doLoadMoreEvent(float y) { int scrollY = (int) (mScrollY - y); if (scrollY < 0) { scrollY = 0; } if (scrollY > mFooterHeight) { scrollY = mFooterHeight; } Log.v("@scrollY", scrollY + ""); scrollTo(0, scrollY); } /** * 处理释放后的操作 */ private void doPullDownResult() { //先获取现在滑动到的位置 mScrollY = getScrollY(); switch (mCurrentState) { case PULL_TO_REFRESH: mCurrentState = NORMAL; mHeaderView.onNormal(); smoothScrollTo(0); break; case RELEASE_TO_REFRESH: //松开时,如果是释放刷新,则开始进行刷新动作 if (!isRefreshing) { //滑动到指定位置 smoothScrollTo(-mHeaderHeight); mHeaderView.onRefreshing(); isRefreshing = true; if (listener != null) { //执行刷新回调 listener.onPullDownRefresh(); } //如果当前滑动位置太靠下,则滑动到指定刷新位置 } else if (mScrollY < -mHeaderHeight) { smoothScrollTo(-mHeaderHeight); } break; } } /** * 获取到数据后,调用 */ public void completeRefresh() { isRefreshing = false; mCurrentState = NORMAL; smoothScrollTo(0); } private void doPullDownMoveEvent(float y) { int scrollY = (int) (mScrollY - y * RATIO); if (scrollY > 0) { scrollY = 0; } if (scrollY < -mMaxScrollHeight) { scrollY = -mMaxScrollHeight; } scrollTo(0, scrollY); if (isRefreshing) return; //设置相应的状态 if (scrollY == 0) { mCurrentState = NORMAL; mHeaderView.onNormal(); } else if (scrollY <= 0 && scrollY > -mHeaderHeight) { mCurrentState = PULL_TO_REFRESH; mHeaderView.onPullToRefresh(Math.abs(scrollY)); } else if (scrollY <= -mHeaderHeight && scrollY >= -mMaxScrollHeight) { mCurrentState = RELEASE_TO_REFRESH; mHeaderView.onReleaseToRefresh(Math.abs(scrollY)); } } /** * 从当前位置滑动到指定位置 * @param y 滑动到的位置 */ private void smoothScrollTo(int y) { int dY = y - mScrollY; mScroller.startScroll(0, mScrollY, 0, dY, 500); invalidate(); } private RecyclerView getRefreshView() { mRefreshView = new RecyclerView(mContext); mRefreshView.setLayoutManager(new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false)); mRefreshView.addItemDecoration(new CustomItemDecoration(mContext, CustomItemDecoration.VERTICAL_LIST)); return mRefreshView; } public void setAdapter(RecyclerView.Adapter adapter) { mRefreshView.setAdapter(adapter); } /** * 判断列表是否在最顶端 * @return */ private boolean isEnablePullDown() { LinearLayoutManager manager = (LinearLayoutManager) mRefreshView.getLayoutManager(); int firstVisible = manager.findFirstVisibleItemPosition(); //当前还没有数据,可以进行下拉 if(manager.getItemCount()==0) return true; return firstVisible == 0 && manager.getChildAt(0).getTop() == 0; } /** * 判断列表是否滑动到了最底部 * @return */ private boolean isEnableLoadMore() { LinearLayoutManager manager = (LinearLayoutManager) mRefreshView.getLayoutManager(); lastVisible = manager.findLastVisibleItemPosition(); int totalCount = manager.getItemCount(); //如果没有数据,只能下拉刷新 if (totalCount == 0) return false; int bottom = manager.findViewByPosition(lastVisible).getBottom(); int decorHeight = manager.getBottomDecorationHeight(mRefreshView.getChildAt(0)); //最后一个child的底部位置在当前视图的上面 return totalCount == lastVisible + 1 && bottom + decorHeight <= getMeasuredHeight(); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(0, mScroller.getCurrY()); mScrollY = mScroller.getCurrY(); invalidate(); } } /** * 设置Footer的内容 */ public void setFooterViewState(boolean hasMoreData){ if(hasMoreData){ mFooterView.onRefreshing(); }else{ mFooterView.onNoData(); } } public interface OnRefreshListener { void onPullDownRefresh(); void onLoadMore(); } public void setOnRefreshListener(OnRefreshListener listener) { this.listener = listener; }}
接下来一步一步的进行分析。
首先,我们在构造方法中,调用了init(Context)方法,如下:
private void init(Context context) { mContext = context; this.setOrientation(VERTICAL); mRefreshView = getRefreshView(); mRefreshView.setBackgroundColor(Color.WHITE); LayoutParams listParams = new LayoutParams(-1, -1); mRefreshView.setLayoutParams(listParams); addView(mRefreshView); //添加HeaderView mHeaderView = new CustomHeaderView(context); LayoutParams params = new LayoutParams(-1, -2); mHeaderView.setLayoutParams(params); addView(mHeaderView, 0); //添加FooterView mFooterView = new CustomFooterView(context); LayoutParams fParams = new LayoutParams(-1, 200); mFooterView.setLayoutParams(fParams); addView(mFooterView, -1); //弹性滑动实现 mScroller = new Scroller(context); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); }
方法中,我们构造了HeaderView,RecyclerView以及FooterView。HeaderView和FooterView是简单的自定义View,RecyclerView是直接构造的。并且在init()方法中,构造了Scroller,用于后面的弹性滑动需要。
接着,后面会执行onSizeChange方法:
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); //第一次获取相关参数,并隐藏HeaderView,FooterView if (isFirst) { mHeaderHeight = mHeaderView.getMeasuredHeight(); mMaxScrollHeight = mHeaderHeight * 3; resetHeaderLayout(-mHeaderHeight); mFooterHeight = mFooterView.getMeasuredHeight(); resetFooterLayout(-mFooterHeight); Log.v("@mHeaderHeight", mHeaderHeight + ""); Log.v("@mFooterHeight", mFooterHeight + ""); isFirst = false; } }
设置了一个isFirst变量,防止重复设置里面的代码。在这个方法里面,我们获取了HeaderView,FooterView的测量高。并且,我们设置了HeaderView,FooterView的margin值,隐藏了头部和尾部。
再接着,就是与用户的交互过程,即用户的触摸事件。这个实现过程,分成两块,一块是下拉刷新,一块是滑动到底部自动加载。这里我们一起分析。
//按下时的位置,当事件被阻止时,第一次ActionDown事件,onTouchEvent无法获取这个位置 //需要在onInterceptTouchEvent获取 private float downY; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { //如果当前是正在刷新并且是下拉状态,则当前视图处理事件 if (isRefreshing && mScrollY < 0) { return true; } //如果当前是刷新状态,并且处于上拉状态,则视图不可进入下拉状态 if (mScrollY >= 0 && isRefreshing) return false; boolean isIntercept = false; int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: downY = ev.getY(); break; case MotionEvent.ACTION_MOVE: //如果达到了滑动条件 if (Math.abs(ev.getY() - downY) >= mTouchSlop) { if (ev.getY() - downY > 0) {//下拉 isIntercept = isEnablePullDown(); if (isIntercept)//设置下拉还是上滑的状态,true表示下拉动作 isPullDownMotion = true; } else {//上滑 isIntercept = isEnableLoadMore(); if (isIntercept)//false表示上滑状态 isPullDownMotion = false; } } else { isIntercept = false; } break; case MotionEvent.ACTION_CANCEL: //如果返回true,子视图如果包含点击事件,则无法进行处理 isIntercept = false; break; case MotionEvent.ACTION_UP: isIntercept = false; break; } return isIntercept; }
onInterceptTouchEvent的作用,如果返回值为true,表示拦截事件,则事件交个当前控件进行处理,子View无法接收到事件;否则事件交给子View处理。 我们要知道,一般,一个事件序列,只能由一个控件处理,也就是说,如果这个控件消费了ACTION_DOWN事件,那么,后面的ACTION_MOVE等都会交给他处理。但是,如果他的parentView在ACTION_MOVE中,拦截了事件,事件将会转交给ParentView的onTouchEvent处理。
然后,开始分析代码,
//如果当前是正在刷新并且是下拉状态,则当前视图处理事件 if (isRefreshing && mScrollY < 0) { return true; } //如果当前是刷新状态,并且处于上拉状态,则视图不可进入下拉状态 if (mScrollY >= 0 && isRefreshing) return false;
如果当前为下拉并且在刷新状态,则返回true,表示拦截事件,RecyclerView不可滑动。如果当前是滑动加载更多,并且刷新状态,则不拦截,因为后面我想在滑动加载更多时,RecyclerView可以滑动。 截止后面,在ACTION_DOWN事件中,我们记录下按下的y轴位置。然后是ACTION_MOVE;
//如果达到了滑动条件 if (Math.abs(ev.getY() - downY) >= mTouchSlop) { if (ev.getY() - downY > 0) {//下拉 isIntercept = isEnablePullDown(); if (isIntercept)//设置下拉还是上滑的状态,true表示下拉动作 isPullDownMotion = true; } else {//上滑 isIntercept = isEnableLoadMore(); if (isIntercept)//false表示上滑状态 isPullDownMotion = false; } } else { isIntercept = false; }
mTouchSlop是滑动的最小值,如果小于这个值,我们认为没有滑动,大于这个值才算滑动。如果当前滑动,大于这个值,继续走里面的if判断,如果当前是下拉状态,并且是可以下拉,那么拦截事件,否则进行滑动加载更多,如果满足滑动加载更多的条件,那么可以向上滑动。并且整个过程,用isPullDownMotion记录下了是向上还是向下的动作。后面在onTouchEvent中需要使用。最后,ACTION_UP和ACTION_CANCEL不拦截,如果拦截,会影响到子View的点击事件。
最后是onTouchEvent
//记录当前滑动的位置 private int mScrollY; @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: //第一次判断时,downY只能从intercept中获取,之后从这里获取 downY = event.getY(); break; case MotionEvent.ACTION_MOVE: float dY = event.getY() - downY; if (isPullDownMotion)//下拉 doPullDownMoveEvent(dY); else {//自动加载更多 doLoadMoreEvent(dY); } break; case MotionEvent.ACTION_UP: if (isPullDownMotion) { //处理下拉结果 doPullDownResult(); } else { //处理滑动加载更多结果 doLoadMoreResult(); } break; case MotionEvent.ACTION_CANCEL: //同ACTION_UP if (isPullDownMotion) { doPullDownResult(); } else { doLoadMoreResult(); } break; } return true; }
看下拉环节(滑动加载更多类似,不再介绍),下拉过程ACTION_MOVE中先调用doPullDownMoveEvent,然后在ACTION_UP中调用了doPullDownResult。先看duPullDownMoveEvent
private void doPullDownMoveEvent(float y) { int scrollY = (int) (mScrollY - y * RATIO); if (scrollY > 0) { scrollY = 0; } if (scrollY < -mMaxScrollHeight) { scrollY = -mMaxScrollHeight; } scrollTo(0, scrollY); if (isRefreshing) return; //设置相应的状态 if (scrollY == 0) { mCurrentState = NORMAL; mHeaderView.onNormal(); } else if (scrollY <= 0 && scrollY > -mHeaderHeight) { mCurrentState = PULL_TO_REFRESH; mHeaderView.onPullToRefresh(Math.abs(scrollY)); } else if (scrollY <= -mHeaderHeight && scrollY >= -mMaxScrollHeight) { mCurrentState = RELEASE_TO_REFRESH; mHeaderView.onReleaseToRefresh(Math.abs(scrollY)); } }
先计算滑动的位置,把滑动的位置限制在-mMaxScrollHeight和0之间,这样就不会滑动到其他地方,然后调用View的scrollTo方法,滑动到对应位置。这样就完成了触摸滑动。 后面,我们在通过滑动的位置,设置相应的状态。并回调HeaderView的各个状态的方法。
然后再看doPullDownResult
/** * 处理释放后的操作 */ private void doPullDownResult() { //先获取现在滑动到的位置 mScrollY = getScrollY(); switch (mCurrentState) { case PULL_TO_REFRESH: mCurrentState = NORMAL; mHeaderView.onNormal(); smoothScrollTo(0); break; case RELEASE_TO_REFRESH: //松开时,如果是释放刷新,则开始进行刷新动作 if (!isRefreshing) { //滑动到指定位置 smoothScrollTo(-mHeaderHeight); mHeaderView.onRefreshing(); isRefreshing = true; if (listener != null) { //执行刷新回调 listener.onPullDownRefresh(); } //如果当前滑动位置太靠下,则滑动到指定刷新位置 } else if (mScrollY < -mHeaderHeight) { smoothScrollTo(-mHeaderHeight); } break; } }
这个方法,就是手指松开屏幕时触发。然后判断移动过程中的状态,如果是下拉刷新状态,则重新恢复到下拉之前,调用smoothScrollTo(后面分析具体实现),弹性滑动到初始位置,并设置状态为NORMAL状态。 如果松开时,是释放刷新状态,那么,先弹性滑动到刷新位置,并执行回调方法。
现在分析,弹性滑动 smoothScrollTo
/** * 从当前位置滑动到指定位置 * @param y 滑动到的位置 */ private void smoothScrollTo(int y) { int dY = y - mScrollY; mScroller.startScroll(0, mScrollY, 0, dY, 500); invalidate(); }
这个方法,必须要配合computeScroll使用,不然是没有效果的。具体的原因,需要查看View的绘制流程,这里我就不具体分析。
@Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(0, mScroller.getCurrY()); mScrollY = mScroller.getCurrY(); invalidate(); } }
这个过程,是从Scroller的startScroll方法开始的,这个方法,调用后,Scroller的computeScrollOffset只要动作没有执行完,就会一直返回true。调用了startScroll方法,需要调用invalide()来引起computeScroll方法的调用,而里面scrollTo方法,才是真正实现位移的原因。里面再调用invalidate又重新引起了computeScroll方法,直到Scroller的computeOffset方法返回false。 这样,每次都移动一小段位置,就实现了平滑滑动的效果。
使用方法,布局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.mjc.recyclerviewdemo.refresh.PullToRefreshRecycleView android:id="@+id/prrv" android:layout_width="match_parent" android:layout_height="match_parent"/></LinearLayout>
Activity中
mPRRV = (PullToRefreshRecycleView) findViewById(R.id.prrv); mPRRV.setOnRefreshListener(new PullToRefreshRecycleView.OnRefreshListener() { @Override public void onPullDownRefresh() { mHandler.postDelayed(new Runnable() { @Override public void run() { datas.add(0, "add"); mAdapter.notifyDataSetChanged(); mPRRV.completeRefresh(); } }, 2000); } @Override public void onLoadMore() { mHandler.postDelayed(new Runnable() { @Override public void run() { datas.add("李四"); datas.add("王五"); datas.add("张三"); datas.add("李四"); datas.add("王五"); datas.add("张三"); mAdapter.notifyDataSetChanged(); mPRRV.completeLoadMore(); } }, 1000); } });
附:源码
0 2
- RecyclerView下拉刷新和加载更多
- recyclerview下拉刷新和加载更多
- RecyclerView下拉刷新和加载更多
- RecyclerView实现下拉刷新和上拉加载更多
- RecyclerView下拉刷新和上拉加载更多
- SwipeRefreshLayout + RecyclerView 实现 上拉刷新 和 下拉加载更多
- RecyclerView 添加下拉刷新和上拉加载更多
- Android之RecyclerView轻松实现下拉刷新和加载更多
- RecyclerView实现下拉刷新和上拉加载更多
- RecyclerView的下拉刷新和加载更多 动画
- RecyclerView添加下拉刷新和上拉加载更多
- RecyclerView封装--添加下拉刷新和上拉加载更多
- Android RecyclerView下拉刷新和上拉加载更多
- Android RecyclerView下拉刷新和上拉加载更多
- RecyclerView 下拉刷新上拉加载更多
- RecyclerView 下拉刷新上拉加载更多
- 自定义RecyclerView实现下拉刷新,加载更多
- RecyclerView 下拉刷新、上拉加载更多
- 【HDU4085】Peach Blossom Spring【斯坦纳树】【状态压缩】
- 找jar包配置的网站
- 天朝特色的飞控开发前期准备工作的基础环境搭建
- hiho 1269(二分)
- 计算日期模板
- RecyclerView下拉刷新和加载更多
- QT父子与QT对象delete
- python开发工具
- iOS 图片编辑——涂鸦——随手指移动随意画线
- 初涉bootstrap:bootstrap css
- 比较分析 Spring AOP 和 AspectJ 之间的差别
- JVM崩溃日志分析1
- 百度定位定位到非洲的方案
- iOS NSDateFormatter 转换 出现 8小时偏差