项目地址:https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh
该项目作者已经不维护了,由于公司开发使用的该框架,本着追求“知其然知其所以然”的心态,对这个框架的源码进行解读一下。
目录:
1.基于PtrClassicFrameLayout、PtrClassicDefaultHeader查看如何使用
2.onMeasure、onLayout的分析
3.事件分发的分析
这个项目的拓展性非常好,按照PtrClassicFrameLayout、PtrClassicDefaultHeader上的代码写法,就可以定制自己想要的下拉刷新的效果
如何使用
先来查看PtrClassicFrameLayout
public class PtrClassicFrameLayout extends PtrFrameLayout {
private PtrClassicDefaultHeader mPtrClassicHeader;
public PtrClassicFrameLayout(Context context) {
super(context);
initViews();
}
public PtrClassicFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initViews();
}
public PtrClassicFrameLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initViews();
}
private void initViews() {
mPtrClassicHeader = new PtrClassicDefaultHeader(getContext());
setHeaderView(mPtrClassicHeader);
addPtrUIHandler(mPtrClassicHeader);
}
代码比较简单,继承 PtrClassicFrameLayout ,创建一个PtrClassicDefaultHeader,然后添加进去,再来看PtrClassicDefaultHeader
public class PtrClassicDefaultHeader extends FrameLayout implements
PtrUIHandler{
...
...
...
public PtrClassicDefaultHeader(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initViews(attrs);
}
protected void initViews(AttributeSet attrs) {
TypedArray arr = getContext().obtainStyledAttributes(attrs, R.styleable.PtrClassicHeader, 0, 0);
if (arr != null) {
mRotateAniTime = arr.getInt(R.styleable.PtrClassicHeader_ptr_rotate_ani_time, mRotateAniTime);
}
buildAnimation();
View header = LayoutInflater.from(getContext()).inflate(R.layout.cube_ptr_classic_default_header, this);
mRotateView = header.findViewById(R.id.ptr_classic_header_rotate_view);
mTitleTextView = (TextView) header.findViewById(R.id.ptr_classic_header_rotate_view_header_title);
mLastUpdateTextView = (TextView) header.findViewById(R.id.ptr_classic_header_rotate_view_header_last_update);
mProgressBar = header.findViewById(R.id.ptr_classic_header_rotate_view_progressbar);
resetView();
}
@Override
public void onUIReset(PtrFrameLayout frame) {
resetView();
mShouldShowLastUpdate = true;
tryUpdateLastUpdateTime();
}
@Override
public void onUIRefreshPrepare(PtrFrameLayout frame) {
mShouldShowLastUpdate = true;
tryUpdateLastUpdateTime();
mLastUpdateTimeUpdater.start();
mProgressBar.setVisibility(INVISIBLE);
mRotateView.setVisibility(VISIBLE);
mTitleTextView.setVisibility(VISIBLE);
if (frame.isPullToRefresh()) {
mTitleTextView.setText(getResources().getString(R.string.cube_ptr_pull_down_to_refresh));
} else {
mTitleTextView.setText(getResources().getString(R.string.cube_ptr_pull_down));
}
}
@Override
public void onUIRefreshBegin(PtrFrameLayout frame) {
mShouldShowLastUpdate = false;
hideRotateView();
mProgressBar.setVisibility(VISIBLE);
mTitleTextView.setVisibility(VISIBLE);
mTitleTextView.setText(R.string.cube_ptr_refreshing);
tryUpdateLastUpdateTime();
mLastUpdateTimeUpdater.stop();
}
@Override
public void onUIRefreshComplete(PtrFrameLayout frame) {
hideRotateView();
mProgressBar.setVisibility(INVISIBLE);
mTitleTextView.setVisibility(VISIBLE);
mTitleTextView.setText(getResources().getString(R.string.cube_ptr_refresh_complete));
// update last update time
SharedPreferences sharedPreferences = getContext().getSharedPreferences(KEY_SharedPreferences, 0);
if (!TextUtils.isEmpty(mLastUpdateTimeKey)) {
mLastUpdateTime = new Date().getTime();
sharedPreferences.edit().putLong(mLastUpdateTimeKey, mLastUpdateTime).commit();
}
}
@Override
public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator) {
final int mOffsetToRefresh = frame.getOffsetToRefresh();
final int currentPos = ptrIndicator.getCurrentPosY();
final int lastPos = ptrIndicator.getLastPosY();
if (currentPos < mOffsetToRefresh && lastPos >= mOffsetToRefresh) {
if (isUnderTouch && status == PtrFrameLayout.PTR_STATUS_PREPARE) {
crossRotateLineFromBottomUnderTouch(frame);
if (mRotateView != null) {
mRotateView.clearAnimation();
mRotateView.startAnimation(mReverseFlipAnimation);
}
}
} else if (currentPos > mOffsetToRefresh && lastPos <= mOffsetToRefresh) {
if (isUnderTouch && status == PtrFrameLayout.PTR_STATUS_PREPARE) {
crossRotateLineFromTopUnderTouch(frame);
if (mRotateView != null) {
mRotateView.clearAnimation();
mRotateView.startAnimation(mFlipAnimation);
}
}
}
}
实现了PtrUIHandler,然后initViews()也很简单,加载header布局跟初始化动画,接着看PtrUIHandler,
public interface PtrUIHandler {
/**
* When the content view has reached top and refresh has been completed, view will be reset.
*
* @param frame
*/
public void onUIReset(PtrFrameLayout frame);
/**
* prepare for loading
*
* @param frame
*/
public void onUIRefreshPrepare(PtrFrameLayout frame);
/**
* perform refreshing UI
*/
public void onUIRefreshBegin(PtrFrameLayout frame);
/**
* perform UI after refresh
*/
public void onUIRefreshComplete(PtrFrameLayout frame);
public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator);
}
在PtrClassicDefaultHeader中主要就是实现了PtrUIHandler 接口,在onUIReset、onUIRefreshPrepare、onUIRefreshBegin、onUIRefreshComplete进行下拉刷新效果的编写,那么我们现在就清楚了,PtrUIHandler 这个接口就是提供给大家去定制自己的下拉刷新效果的。
mPtrFrame.setPtrHandler(new PtrHandler() {
@Override
public void onRefreshBegin(PtrFrameLayout frame) {
updateData();
}
@Override
public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) {
return PtrDefaultHandler.checkContentCanBePulledDown(frame, content, header);
}
});
接下来,我们在代码中设置PtrHandler,在onRefreshBegin中进行网络请求、在checkCanDoRefresh中进行自定义是否阻拦滑动
那么我们继续查看主类PtrFrameLayout 中的代码,这里主要贴有用的代码
public class PtrFrameLayout extends ViewGroup {
protected View mContent;
private View mHeaderView;
// optional config for define header and content in xml file
private int mHeaderId = 0;
private int mContainerId = 0;
// config
private int mDurationToClose = 200;
private int mDurationToCloseHeader = 1000;
private PtrUIHandlerHolder mPtrUIHandlerHolder = PtrUIHandlerHolder.create();
private PtrHandler mPtrHandler;
// working parameters
private ScrollChecker mScrollChecker;
private int mPagingTouchSlop;
private int mHeaderHeight;
private PtrIndicator mPtrIndicator;
public PtrFrameLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mPtrIndicator = new PtrIndicator();
TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.PtrFrameLayout, 0, 0);
if (arr != null) {
mHeaderId = arr.getResourceId(R.styleable.PtrFrameLayout_ptr_header, mHeaderId);
mContainerId = arr.getResourceId(R.styleable.PtrFrameLayout_ptr_content, mContainerId);
mPtrIndicator.setResistance(
arr.getFloat(R.styleable.PtrFrameLayout_ptr_resistance, mPtrIndicator.getResistance()));
mDurationToClose = arr.getInt(R.styleable.PtrFrameLayout_ptr_duration_to_close, mDurationToClose);
mDurationToCloseHeader = arr.getInt(R.styleable.PtrFrameLayout_ptr_duration_to_close_header, mDurationToCloseHeader);
float ratio = mPtrIndicator.getRatioOfHeaderToHeightRefresh();
ratio = arr.getFloat(R.styleable.PtrFrameLayout_ptr_ratio_of_header_height_to_refresh, ratio);
mPtrIndicator.setRatioOfHeaderHeightToRefresh(ratio);
mKeepHeaderWhenRefresh = arr.getBoolean(R.styleable.PtrFrameLayout_ptr_keep_header_when_refresh, mKeepHeaderWhenRefresh);
mPullToRefresh = arr.getBoolean(R.styleable.PtrFrameLayout_ptr_pull_to_fresh, mPullToRefresh);
arr.recycle();
}
mScrollChecker = new ScrollChecker();
final ViewConfiguration conf = ViewConfiguration.get(getContext());
mPagingTouchSlop = conf.getScaledTouchSlop() * 2;
}
@Override
protected void onFinishInflate() {
final int childCount = getChildCount();
if (childCount > 2) {
throw new IllegalStateException("PtrFrameLayout only can host 2 elements");
} else if (childCount == 2) {
if (mHeaderId != 0 && mHeaderView == null) {
mHeaderView = findViewById(mHeaderId);
}
if (mContainerId != 0 && mContent == null) {
mContent = findViewById(mContainerId);
}
.....
.....
}
在构造方法中,主要就是读取了一些自定义属性进行配置和初始化PtrIndicator 、ScrollChecker,然后onFinishflate中,如果没有初始化mHeaderView 和mContent,就进行初始化
onMeasure onLayout
继续查看onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mHeaderView != null) {
measureChildWithMargins(mHeaderView, widthMeasureSpec, 0, heightMeasureSpec, 0);
MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams();
mHeaderHeight = mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
mPtrIndicator.setHeaderHeight(mHeaderHeight);
}
if (mContent != null) {
measureContentView(mContent, widthMeasureSpec, heightMeasureSpec);
}
}
}
测量mHeaderView与mContent的大小,并把mHeaderHeight 赋值给mPtrIndicator
继续查看onLayout
@Override
protected void onLayout(boolean flag, int i, int j, int k, int l) {
layoutChildren();
}
private void layoutChildren() {
int offsetX = mPtrIndicator.getCurrentPosY();
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
if (mHeaderView != null) {
MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams();
final int left = paddingLeft + lp.leftMargin;
final int top = paddingTop + lp.topMargin + offsetX - mHeaderHeight;
final int right = left + mHeaderView.getMeasuredWidth();
final int bottom = top + mHeaderView.getMeasuredHeight();
mHeaderView.layout(left, top, right, bottom);
}
}
if (mContent != null) {
if (isPinContent()) {
offsetX = 0;
}
MarginLayoutParams lp = (MarginLayoutParams) mContent.getLayoutParams();
final int left = paddingLeft + lp.leftMargin;
final int top = paddingTop + lp.topMargin + offsetX;
final int right = left + mContent.getMeasuredWidth();
final int bottom = top + mContent.getMeasuredHeight();
}
mContent.layout(left, top, right, bottom);
}
}
主要查看mHeaderView.layout,top=paddingTop + lp.topMargin + offsetX - mHeaderHeight;,向上偏移的一个 mHeaderHeight的高,这样mHeaderView初始化时就会隐藏
以下是 http://a.codekk.com/detail/Android/Grumoon/android-Ultra-Pull-To-Refresh%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90的解析
ViewGroup 的事件处理,通常重写 onInterceptTouchEvent 方法或者 dispatchTouchEvent 方法,PtrFrameLayout 重写了 dispatchTouchEvent 方法。
事件处理流程图 如下:
以上有两点需要分析下
- ACTION_UP 或者 ACTION_CANCEL 时候执行的 onRelease 方法。
功能上,通过执行tryToPerformRefresh
方法,如果向下拉动的位移已经超过了触发下拉刷新的偏移量mOffsetToRefresh
,并且当前状态是 PTR_STATUS_PREPARE,执行刷新功能回调。
行为上,如果没有达到触发刷新的偏移量,或者当前状态为 PTR_STATUS_COMPLETE,或者刷新过程中不保持头部位置,则执行向上的位置回复动作。- ACTION_MOVE 中判断是否可以纵向 move。
ACTION_MOVE 的方向向下,如果mPtrHandler
不为空,并且mPtrHandler.checkCanDoRefresh
返回值为 true,则可以移动, Header 和 Content 向下移动,否则,事件交由父类处理。
ACTION_MOVE 的方向向上,如果当前位置大于起始位置,则可以移动,Header 和 Content 向上移动,否则,事件交由父类处理。