android-Ultra-Pull-to-Refresh 深入理解及使用
下拉刷新,几乎是每个 Android 应用都会需要的功能。 android-Ultra-Pull-To-Refresh (以下简称 UltraPTR )便是一个强大的 Andriod 下拉刷新框架。
主要特点:
(1).继承于 ViewGroup, Content 可以包含任何 View。
(2).简洁完善的 Header 抽象,方便进行拓展,构建符合需求的头部。
项目地址:
https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh
竞品:
https://github.com/chrisbanes/Android-PullToRefresh
https://github.com/johannilsson/android-pulltorefresh
https://github.com/Demievil/SwipeRefreshLayout
参考文章:
android-Ultra-Pull-To-Refresh源码解析: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
公共技术点之 View 绘制流程:http://codekk.com/open-source-project-analysis/detail/Android/lightSky/%E5%85%AC%E5%85%B1%E6%8A%80%E6%9C%AF%E7%82%B9%E4%B9%8B%20View%20%E7%BB%98%E5%88%B6%E6%B5%81%E7%A8%8B
公共技术点之 View 事件传递:http://codekk.com/open-source-project-analysis/detail/Android/Trinea/%E5%85%AC%E5%85%B1%E6%8A%80%E6%9C%AF%E7%82%B9%E4%B9%8B%20View%20%E4%BA%8B%E4%BB%B6%E4%BC%A0%E9%80%92
Android事件分发机制完全解析,带你从源码的角度彻底理解(上、下):http://blog.csdn.net/guolin_blog/article/details/9097463
1.添加依赖
compile 'in.srain.cube:ultra-ptr:1.0.10'
2.XML界面配置
详细官方中文文档:https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh/blob/master/README-cn.md
<?xml version="1.0" encoding="utf-8"?>
<in.srain.cube.views.ptr.PtrClassicFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:cube_ptr="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#f1f1f1"
cube_ptr:ptr_duration_to_close="200"
cube_ptr:ptr_duration_to_close_header="1000"
cube_ptr:ptr_keep_header_when_refresh="true"
cube_ptr:ptr_pull_to_fresh="false"
cube_ptr:ptr_ratio_of_header_height_to_refresh="1.2"
cube_ptr:ptr_resistance="1.7">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</in.srain.cube.views.ptr.PtrClassicFrameLayout>
自定义属性实现
//1.在attr里定义属性
<declare-styleable name="PtrFrameLayout"?
//2.在构造方法里获取
TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.PtrFrameLayout);
arr.recycle();
如何获取header及content
1.重写view的onFinishInflate方法
2.判断childCount
childCount > 2 throw excetpion
childCount = 2 header+content
childCount = 1 content
childCount = 0 errorView
3.mHeaderView.bringToFront(),不懂什么意思
3.Java代码配置
// the following are default settings
//1.设置阻尼系数
mPtrFrame.setResistance(1.7f);
//2.下拉刷新头部的比率
mPtrFrame.setRatioOfHeaderHeightToRefresh(1.2f);
//3.设置从松手的位置到头部所要的时间
mPtrFrame.setDurationToClose(200);
//4.设置从头部到顶部所要的时间
mPtrFrame.setDurationToCloseHeader(1000);
//5.default is false,false下拉就进行刷新,true松手到超过指定高度才进行刷新
mPtrFrame.setPullToRefresh(false);
//6.default is true,true刷新时有消息头,flase刷新时没有消息头
mPtrFrame.setKeepHeaderWhenRefresh(true);
1.mPtrFrame.setResistance(1.7f);
//setp 1.在PtrIndicator.class里面设置resistance
mPtrIndicator.setResistance(arr.getFloat(R.styleable.PtrFrameLayout_ptr_resistance, mPtrIndicator.getResistance()));
private float mResistance = 1.7f;
//setp 2.在MotionEvent.ACTION_MOVE中计算,新的offsetY。(即offsetY / mResistance)
mPtrIndicator.onMove(e.getX(), e.getY());
public final void onMove(float x, float y) {
float offsetX = x - mPtLastMove.x;
float offsetY = (y - mPtLastMove.y);
processOnMove(x, y, offsetX, offsetY);
mPtLastMove.set(x, y);
}
protected void processOnMove(float currentX, float currentY, float offsetX, float offsetY) {
setOffset(offsetX, offsetY / mResistance);
}
protected void setOffset(float x, float y) {
mOffsetX = x;
mOffsetY = y;
}
private PointF mPtLastMove = new PointF();
//setp 3.在MotionEvent.ACTION_MOVE的时候使用新的offsetY
movePos(offsetY);
mContent.offsetTopAndBottom(change);
2.mPtrFrame.setRatioOfHeaderHeightToRefresh(1.2f);
//setp 1.在attr里设置header高度的比率
float ratio = mPtrIndicator.getRatioOfHeaderToHeightRefresh();
ratio = arr.getFloat(R.styleable.PtrFrameLayout_ptr_ratio_of_header_height_to_refresh, ratio);
mPtrIndicator.setRatioOfHeaderHeightToRefresh(ratio);
//setp 1'.在java代码里设置header高度的比率
public void setRatioOfHeaderHeightToRefresh(float ratio) {
mPtrIndicator.setRatioOfHeaderHeightToRefresh(ratio);
}
//以上两种方式都会调用PtrIndicator的方法
public void setRatioOfHeaderHeightToRefresh(float ratio) {
mRatioOfHeaderHeightToRefresh = ratio;
mOffsetToRefresh = (int) (mHeaderHeight * ratio);
}
//setp 2.在onMeasure的时候设置header的高度及栈值
mHeaderHeight = mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
mPtrIndicator.setHeaderHeight(mHeaderHeight);
public void setHeaderHeight(int height) {
mHeaderHeight = height;
updateHeight();
}
protected void updateHeight() {
mOffsetToRefresh = (int) (mRatioOfHeaderHeightToRefresh * mHeaderHeight);
}
public int getOffsetToRefresh() {
return mOffsetToRefresh;
}
//setp 3.在MotionEvent.ACTION_UP,使用这个栈值
//3.1
onRelease(false);
//3.2
tryToPerformRefresh();
//3.3
mPtrIndicator.isOverOffsetToRefresh()
public boolean isOverOffsetToRefresh() {
return mCurrentPos >= getOffsetToRefresh();
}
public int getOffsetToRefresh() {
return mOffsetToRefresh;
}
//疑问:setRatioOfHeaderHeightToRefresh与onMeasure的调用顺序
3.mPtrFrame.setDurationToClose(200);
//step 1.设置方式省略
//step 2.在MotionEvent.ACTION_UP中使用这个值
private void onRelease(boolean stayForLoading)
//step 3.要满足正在加载中&当刷新的时候保持头部&当前的pos要大于offsetToKeepHeader.
//这三个条件都要满足,才会执行回滚头部。(也就是从当前位置回滚到offsetToKeepHeader)
if (mStatus == PTR_STATUS_LOADING) {
// keep header for fresh
if (mKeepHeaderWhenRefresh) {
// scroll header back
if (mPtrIndicator.isOverOffsetToKeepHeaderWhileLoading() && !stayForLoading) {
mScrollChecker.tryToScrollTo(mPtrIndicator.getOffsetToKeepHeaderWhileLoading(), mDurationToClose);
}
}
}
//step 4.那什么是offsetToKeepHeader?可以看到它的默认值为mHeaderHeight,也有公开的方法可以设置,///但是,不知道应该在什么时候设置
private int mOffsetToKeepHeaderWhileLoading = -1;
public void setOffsetToKeepHeaderWhileLoading(int offset) {
mOffsetToKeepHeaderWhileLoading = offset;
}
public int getOffsetToKeepHeaderWhileLoading() {
return mOffsetToKeepHeaderWhileLoading >= 0 ? mOffsetToKeepHeaderWhileLoading : mHeaderHeight;
}
4.mPtrFrame.setDurationToCloseHeader(1000);
//step 1.当主动调用刷新完成的
private void performRefreshComplete() {
mStatus = PTR_STATUS_COMPLETE;
notifyUIRefreshComplete(false);
}
//step 2.通知UI刷新完成
private void notifyUIRefreshComplete(boolean ignoreHook) {
tryScrollBackToTopAfterComplete();
tryToNotifyReset();
}
private void tryScrollBackToTopAfterComplete() {
tryScrollBackToTop();
}
//step 3.回滚到起点(即顶部)
private void tryScrollBackToTop() {
if (!mPtrIndicator.isUnderTouch()) {
mScrollChecker.tryToScrollTo(PtrIndicator.POS_START, mDurationToCloseHeader);
}
}
//问题:mDurationToClose和mDurationToCloseHeader可能会有冲突?
//当网络请求的响应的时候.即小于mDurationToClose的时间。在UI就会有卡顿的问题
//根本原因:当执行mDurationToClose一半的时候,在调用refreshComplete()的,就会有一个回退的效果,然//后在执行mDurationToCloseHeader
5.mPtrFrame.setPullToRefresh(false);
//step 1.设置方法略
//step 2.在下拉的时候,调用movePos(即MotionEvent.ACTION_MOVE)
private void movePos(float deltaY) {
updatePos(change);
}
private void updatePos(int change) {
// Pull to Refresh
if (mStatus == PTR_STATUS_PREPARE) {
// reach fresh height while moving from top to bottom
if (isUnderTouch && !isAutoRefresh() && mPullToRefresh
&& mPtrIndicator.crossRefreshLineFromTopToBottom()) {
tryToPerformRefresh();
}
// reach header height while auto refresh
if (performAutoRefreshButLater() && mPtrIndicator.hasJustReachedHeaderHeightFromTopToBottom()) {
tryToPerformRefresh();
}
}
}
//step 3.执行下拉刷新(就是没有滑动超过指定高度的时候,就执行下拉刷新。相当于一个提前执行网络请求的一个效果)
private boolean tryToPerformRefresh() {
performRefresh();
}
private void performRefresh() {
if (mPtrUIHandlerHolder.hasHandler()) {
mPtrUIHandlerHolder.onUIRefreshBegin(this);
}
if (mPtrHandler != null) {
mPtrHandler.onRefreshBegin(this);
}
}
6.mPtrFrame.setKeepHeaderWhenRefresh(true);
//step 1.设置省略(xml,java两种方式)
//step 2.在MotionEvent.ACTION_UP时调用onRelease(false)
private void onRelease(boolean stayForLoading) {
tryToPerformRefresh();
if (mStatus == PTR_STATUS_LOADING) {
// keep header for fresh
if (mKeepHeaderWhenRefresh) {
// scroll header back
if (mPtrIndicator.isOverOffsetToKeepHeaderWhileLoading() && !stayForLoading) {
mScrollChecker.tryToScrollTo(mPtrIndicator.getOffsetToKeepHeaderWhileLoading(), mDurationToClose);
} else {
// do nothing
}
} else {
tryScrollBackToTopWhileLoading();
}
} else {
if (mStatus == PTR_STATUS_COMPLETE) {
notifyUIRefreshComplete(false);
} else {
tryScrollBackToTopAbortRefresh();
}
}
}
//step 3.与mDurationToClose相比,不再是回滚到mHeaderHeight(默认值),而是直接回滚到顶点(0)。
//这样做达到了。回滚的时候隐藏header的目的
4.设置下拉刷新监听
注意:如果有下拉问题,可以选择性重写checkCanDoRefresh
mPtrFrame.setPtrHandler(new PtrHandler() {
//检查能否下刷新
@Override
public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) {
return PtrDefaultHandler.checkContentCanBePulledDown(frame, mWebView, header);
}
//开始下拉刷新
@Override
public void onRefreshBegin(PtrFrameLayout frame) {
updateData();
}
});
需要自己实现checkContentCanBePulledDown
public static boolean canChildScrollUp(View view) {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (view instanceof AbsListView) {
final AbsListView absListView = (AbsListView) view;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return view.getScrollY() > 0;
}
} else {
return view.canScrollVertically(-1);
}
}
5.手动回调完成下拉刷新
mPtrFrame.refreshComplete();
6.自定义下拉刷新的头部
5.1 自定义view,并实现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);
}
5.2 用过xml布局或者setHeaderView,设置头部
5.3 设置PtrUIHandler
ptrHome.addPtrUIHandler(ptrUIHandler);
7.自动下拉刷新
ptrFrame.postDelayed(new Runnable() {
@Override
public void run() {
ptrFrame.autoRefresh(true);
}
}, 150);
8.其他设置
1. setPinContent(false)
解释:设置ContentView是否顶住.(设置为true即content不动,只有header随着移动)
//step 1.在MotionEvent.ACTION_MOVE中调用movePos(offsetY)
private void movePos(float deltaY) {
updatePos(change);
}
private void updatePos(int change) {
mHeaderView.offsetTopAndBottom(change);
if (!isPinContent()) {
mContent.offsetTopAndBottom(change);
}
}
2.setLoadingMinTime(500)
解释:设置网络加载时间最少为500毫秒
//step 1.在MotionEvent.ACTION_UP的时候调用
onRelease(false)
//step 2.执行下拉刷新,并保存mLoadingStartTime
private void onRelease(boolean stayForLoading) {
tryToPerformRefresh();
}
private boolean tryToPerformRefresh() {
if (mStatus != PTR_STATUS_PREPARE) {
return false;
}
//
if ((mPtrIndicator.isOverOffsetToKeepHeaderWhileLoading() && isAutoRefresh()) || mPtrIndicator.isOverOffsetToRefresh()) {
mStatus = PTR_STATUS_LOADING;
performRefresh();
}
return false;
}
private void performRefresh() {
mLoadingStartTime = System.currentTimeMillis();
if (mPtrUIHandlerHolder.hasHandler()) {
mPtrUIHandlerHolder.onUIRefreshBegin(this);
if (DEBUG) {
PtrCLog.i(LOG_TAG, "PtrUIHandler: onUIRefreshBegin");
}
}
if (mPtrHandler != null) {
mPtrHandler.onRefreshBegin(this);
}
}
//step 3.在调用refreshComplete()时计算delay。并且postDelayed
final public void refreshComplete() {
int delay = (int) (mLoadingMinTime - (System.currentTimeMillis() - mLoadingStartTime));
if (delay <= 0) {
performRefreshComplete();
} else {
postDelayed(new Runnable() {
@Override
public void run() {
performRefreshComplete();
}
}, delay);
}
}
//可处理mDurationToClose和mDurationToCloseHeader的冲突的问题
3.disableWhenHorizontalMove(false)
解释:为了解决与viewpager的手势冲突问题(即在x轴滑动的距离大于在y轴滑动的距离,则直接返回,UltraPtr不做处理)
//step 1.在MotionEvent.ACTION_MOVE
if (mDisableWhenHorizontalMove && !mPreventForHorizontal && (Math.abs(offsetX) > mPagingTouchSlop && Math.abs(offsetX) > Math.abs(offsetY))) {
if (mPtrIndicator.isInStartPosition()) {
mPreventForHorizontal = true;
}
}
if (mPreventForHorizontal) {
return dispatchTouchEventSupper(e);
}
9.其他问题
1.UltraPtr如何实现滑动动画
解答:通过Scroller与ScrollChecker。
1.Scroller为滚动的封装类,通过偏移来描述从一个点到移动另一个点,坐标的变化情况。(mScroller.startScroll(startX, startY, dx, dy, duration)
2.ScrollChecker实现Runnable接口的线程类,但是其run放在主线程执行。根据其变更的坐标,去设置header
和content的offsetTopAndBottom(int offset),最后在刷新界面
3.使用view.post(action),不断执行ScrollChecker的run方法。这样到header和content位置不断变化,然后产生动画
4.最后mScroller.computeScrollOffset()或者mScroller.isFinished()来判断是否到了指定的间隔
2.如何测量header和content?
3.为什么UltraPtr没有实现加载更多
对比 Android-PullToRefresh 项目,UltraPTR 没有实现 加载更多 的功能,但我认为 下拉刷新 和 加载更多 不是同一层次的功能, 下拉刷新 有更广泛的需求,可以适用于任何页面。而 加载更多 的功能应该交由具体的 Content 自己去实现。这应该是和 Google 官方推出 SwipeRefreshLayout 是相同的设计思路,但对比 SwipeRefreshLayout, UltraPTR 更灵活,更容易拓展。
4.事件处理流程是怎么样
5.类的关系图是怎样的
9.PtrIndicator
字段
- mIsUnderTouch-在ACTION_DOWN为true,ACTION_UP为false
- mPtLastMove-在ACTION_DOWN和ACTION_MOVE都要更新这个坐标
- mOffsetX,mOffsetY-偏移量,当前坐标减去mPtLastMove
- mOffsetToRefresh-(int) (mHeaderHeight * ratio)
- mCurrentPos-ACTION_MOVE的时候更新这个坐标
- mLastPos-ACTION_MOVE保存上一个mCurrentPos坐标
- mPressedPos-ACTION_DOWN保存mCurrentPos坐标
- mRefreshCompleteY-刷新完成的时候保存mCurrentPos坐标
方法
//滑动的位置与Header的高度的百分比
public float getCurrentPercent()
//在顶部上面
public boolean willOverTop(int to)
//在顶部
public boolean isAlreadyHere(int to)
//顶部有空余
public boolean hasLeftStartPosition()
//在开始点
public boolean isInStartPosition()
//‘刚刚’离开开始点
public boolean hasJustLeftStartPosition()
//‘刚刚’回到开始点
public boolean hasJustBackToStartPosition()
//超过设定的刷新高度
public boolean hasMovedAfterPressedDown()
public boolean goDownCrossFinishPosition()
public boolean crossRefreshLineFromTopToBottom()
public boolean hasJustReachedHeaderHeightFromTopToBottom()
可选
public void setOffsetToKeepHeaderWhileLoading(int offset) {
mOffsetToKeepHeaderWhileLoading = offset;
}
public int getOffsetToKeepHeaderWhileLoading() {
return mOffsetToKeepHeaderWhileLoading >= 0 ? mOffsetToKeepHeaderWhileLoading : mHeaderHeight;
}
public boolean isOverOffsetToKeepHeaderWhileLoading() {
return mCurrentPos > getOffsetToKeepHeaderWhileLoading();
}