目录
3.4.PtrDefaultHandler实现了PtrHandler接口
7.数据加载完成通知PtrClassicFrameLayout刷新完成
PtrClassicFrameLayout开源git地址:https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh
支持最低版本API LEVEL >= 8
PtrClassicFrameLayout类关系图如下:
1.PtrClassicFrameLayout是什么?
下拉刷新,(android-Ultra-Pull-To-Refresh)是一个功能强大的下拉刷新控件,需要在自己的视图内部实现自己加载下一页的效果,简称(UltraPTR);
2.PtrClassicFrameLayout优点有哪些?
PtrClassicFrameLayout是旧的下拉刷新的替代方案,PtrClassicFrameLayout内部可以包含任意视图实现下拉刷新功能;PtrClassicFrameLayout是一个比Google提供SwipeRefreshLayout下拉刷新功能更加强大的控件;PtrClassicFrameLayout支持像ListView添加headview一样容易的自定义headview功能;
a.PtrClassicFrameLayout主要包含两部分视图mContent和mHeaderView
protected View mContent; //实际要显示的内容视图
private View mHeaderView; //下拉刷新的头部视图,可以自定义
b.可以自定义headView需要实现PtrUIHandler接口
public class CustomPtrHeader extends FrameLayout implements PtrUIHandler{}
PtrClassicFrameLayout主要实现了下拉刷新功能,未实现上拉加载更多效果,实际上上拉加载更多不是必要功能(例如 :支付宝的首页,航旅纵横首页等很多软件界面可能只需要下拉刷新,不需要上拉加载更多),下拉刷新几乎是每个APP必备功能;
3.PtrClassicFrameLayout关键类说明?
3.1.PtrFrameLayout
PtrFrameLayout继承ViewGroup,PtrFrameLayout完成视图的绘制(4.PtrClassicFrameLayout绘制流程?)和下拉刷新显示功能(5.触摸实现下拉刷新流程?);
3.2.PtrUIHandler
private PtrUIHandlerHolder mPtrUIHandlerHolder = PtrUIHandlerHolder.create();
private PtrHandler mPtrHandler;
PtrUIHandler是UI接口,自定headView时实现PtrUIHandler接口,根据下拉刷新状态执行相应状态到的回调改变headView显示状态,PtrUIHandler接口定义了四个方法;
//自定义headView实现的UI接口,根据headView状态调整headView视图显示或隐藏
public interface PtrUIHandler {
/**
*当视图已经到达顶部同时完成了刷新操作,可以做自定义headview视图恢复初始状态
* @param frame
*/
public void onUIReset(PtrFrameLayout frame);
/**
* 刷新准备,可以修改headview刷新视图样式
* @param frame
*/
public void onUIRefreshPrepare(PtrFrameLayout frame);
/**
* UI刷新开始,可以修改headview刷新视图样式
*/
public void onUIRefreshBegin(PtrFrameLayout frame);
/**
* 刷新完成以后,可以修改headview刷新视图样式
*/
public void onUIRefreshComplete(PtrFrameLayout frame);
//下拉时headView移动位置变化监听
public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator);
}
3.3.PtrUIHandler
PtrHandler功能接口
/**
* 检查是否可以开始刷新;例如当content内容视图里面是空的或者第一个视图显示的时候
* <p/>
* {@link in.srain.cube.views.ptr.PtrDefaultHandler#checkContentCanBePulledDown}
*/
public boolean checkCanDoRefresh(final PtrFrameLayout frame, final View content, final View header);
/**
* 刷新开始,回调提示执行刷新操作,在实现的回调方法执行刷新操作
*
* @param frame
*/
public void onRefreshBegin(final PtrFrameLayout frame);
3.4.PtrDefaultHandler实现了PtrHandler接口
实现了checkCanDoRefresh()方法,在checkCanDoRefresh()里面主要实现是否满足下拉刷新的条件;
需要自己执行设置监听刷新开始回调;
mPtrFrame.setPtrHandler(new PtrDefaultHandler() {
@Override
public void onRefreshBegin(PtrFrameLayout frame) {
//在此处执行刷新操作加载第一页数据
}
});
3.5.PtrUIHandlerHolder
PtrUIHandlerHolder包裹PtrUIHandler链表,优点是在PtrUIHandlerHolder包裹类在调用PtrUIHandler时做一些额外处理在执行回调方法;
class PtrUIHandlerHolder implements PtrUIHandler {
private PtrUIHandler mHandler;
private PtrUIHandlerHolder mNext;
}
当有刷新状态变化时会刷新链表下的所有回调,例如:
//循环链表执行回调
@Override
public void onUIRefreshBegin(PtrFrameLayout frame) {
PtrUIHandlerHolder current = this;
do {
final PtrUIHandler handler = current.getHandler();
if (null != handler) {
handler.onUIRefreshBegin(frame);
}
} while ((current = current.mNext) != null);
}
3.6.PtrUIHandlerHook
钩子任务类,实现了 Runnable 接口,可以理解为在原来的操作之间,插入了一段任务去执行。一个钩子任务只能执行一次,通过调用 takeOver
去执行。执行结束,用户需要调用 resume
方法,去恢复执行原来的操作。如果钩子任务已经执行过了,调用 takeOver
将会直接恢复执行原来的操作。可以通过 PtrFrameLayout 类的 setRefreshCompleteHook(PtrUIHandlerHook hook)
进行设置。当用户调用refreshComplete()
方法表示刷新结束以后,如果有 hook 存在,先执行 hook 的 takeOver
方法,执行结束,用户需要主动调用 hook 的 resume
方法,然后才会进行 Header 回弹到顶部的动作。
4.PtrClassicFrameLayout绘制流程?
PtrClassicFrameLayout视图绘制主要执行了三个回调方法onMeasure,onLayout,onFinishInflate;
4.1.onMeasure
测试视图尺寸,测量mContent和mHeaderView尺寸;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mHeaderView != null) {
//测试孩子视图的尺寸,measureChildWithMargins减去了ViewGroup的padding和子View的margin 保证child最大可用空间
measureChildWithMargins(mHeaderView, widthMeasureSpec, 0, heightMeasureSpec, 0);
MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams();
//获取headerView视图的高度包含上线margin
mHeaderHeight = mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
//记录headerView视图的高度
mPtrIndicator.setHeaderHeight(mHeaderHeight);
}
if (mContent != null) {
measureContentView(mContent, widthMeasureSpec, heightMeasureSpec);
}
}
//希望孩子视图的宽度和高度,父视图给子视图指定的宽度和高度
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
private void measureContentView(View child,
int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
getPaddingTop() + getPaddingBottom() + lp.topMargin, lp.height);
//测试孩子视图的尺寸,childHeightMeasureSpec和childWidthMeasureSpec减去了ViewGroup的padding和子View的margin 保证child最大可用空间
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
4.2.onLayout
完成视图布局,主要布局mContent和mHeaderView;
@Override
protected void onLayout(boolean flag, int i, int j, int k, int l) {
layoutChildren();
}
private void layoutChildren() {
int offsetX = mPtrIndicator.getCurrentPosY();//当前y轴headView下拉偏移座标
int paddingLeft = getPaddingLeft(); //ViewGroup左侧padding
int paddingTop = getPaddingTop(); //ViewGroup上侧padding
if (mHeaderView != null) {
MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams();
final int left = paddingLeft + lp.leftMargin;//headerView左侧座标
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); //布局headView视图,指定座标
if (DEBUG && DEBUG_LAYOUT) {
PtrCLog.d(LOG_TAG, "onLayout header: %s %s %s %s", 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();
if (DEBUG && DEBUG_LAYOUT) {
PtrCLog.d(LOG_TAG, "onLayout content: %s %s %s %s", left, top, right, bottom);
}
mContent.layout(left, top, right, bottom); //布局内容content视图,指定座标
}
}
4.3.onFinishInflate
布局完成以后获取内容视图和头部视图;
布局完成为mHeaderView和mContent变量赋值相应的视图;
5.触摸实现下拉刷新流程?
ViewGroup 的事件处理,通常重写 onInterceptTouchEvent 方法或者 dispatchTouchEvent 方法,PtrFrameLayout 重写了 dispatchTouchEvent 方法。事件处理流程图 如下:
刷新状态定义
// status enum
public final static byte PTR_STATUS_INIT = 1; //初始化
private byte mStatus = PTR_STATUS_INIT;
public final static byte PTR_STATUS_PREPARE = 2; //刷新准备
public final static byte PTR_STATUS_LOADING = 3; //刷新加载中
public final static byte PTR_STATUS_COMPLETE = 4; //刷新完成
5.1.按下事件
case MotionEvent.ACTION_DOWN:
mHasSendCancelEvent = false;
mPtrIndicator.onPressDown(e.getX(), e.getY());//记录按下x和y座标
mScrollChecker.abortIfWorking(); //终止其他操作
mPreventForHorizontal = false;
// The cancel event will be sent once the position is moved.
// So let the event pass to children.
// fix #93, #102
dispatchTouchEventSupper(e);
return true;
1)mPtrIndicator.onPressDown(e.getX(), e.getY());//记录按下x和y座标,当前位置等;
2)mScrollChecker.abortIfWorking(); //终止其他操作
5.2.滑动事件
case MotionEvent.ACTION_MOVE:
mLastMoveEvent = e; //上一次的事件
mPtrIndicator.onMove(e.getX(), e.getY());
float offsetX = mPtrIndicator.getOffsetX();
float offsetY = mPtrIndicator.getOffsetY();//计算X轴和Y轴移动的距离
//禁用水平移动时,并水平移动距离大于垂直距离则阻止水平滑动
if (mDisableWhenHorizontalMove && !mPreventForHorizontal && (Math.abs(offsetX) > mPagingTouchSlop && Math.abs(offsetX) > Math.abs(offsetY))) {
if (mPtrIndicator.isInStartPosition()) {
mPreventForHorizontal = true;
}
}
//阻止水平移动,事件向下传递,当前视图不处理触摸事件向下传递
if (mPreventForHorizontal) {
return dispatchTouchEventSupper(e);
}
boolean moveDown = offsetY > 0; //判断是否向下移动
boolean moveUp = !moveDown;
boolean canMoveUp = mPtrIndicator.hasLeftStartPosition();
if (DEBUG) {
boolean canMoveDown = mPtrHandler != null && mPtrHandler.checkCanDoRefresh(this, mContent, mHeaderView);
PtrCLog.v(LOG_TAG, "ACTION_MOVE: offsetY:%s, currentPos: %s, moveUp: %s, canMoveUp: %s, moveDown: %s: canMoveDown: %s", offsetY, mPtrIndicator.getCurrentPosY(), moveUp, canMoveUp, moveDown, canMoveDown);
}
// 当视图没有到达顶端时滑动事件向下传递,当前视图不处理
if (moveDown && mPtrHandler != null && !mPtrHandler.checkCanDoRefresh(this, mContent, mHeaderView)) {
return dispatchTouchEventSupper(e);
}
//向下滑动或者向上滑动同时能够向上滑动
if ((moveUp && canMoveUp) || moveDown) {
movePos(offsetY); //滑动时更新新的偏移位置
return true;
}
1)检测是否阻止水平移动继续向下传递触摸事件
mDisableWhenHorizontalMove(true)变量为包裹(例如ViewPager)视图禁用PtrClassicFrameLayout水平移动,让事件继续传递,传递给子视图(例如ViewPager)
mPreventForHorizontal(true)检测水平移动距离大于垂直距离为水平距离,设置mPreventForHorizontal为true,变量阻止PtrClassicFrameLayout水平移动,让事件继续传递,传递给子视图(例如ViewPager)
if (mDisableWhenHorizontalMove && !mPreventForHorizontal && (Math.abs(offsetX) > mPagingTouchSlop && Math.abs(offsetX) > Math.abs(offsetY))) {
if (mPtrIndicator.isInStartPosition()) {
mPreventForHorizontal = true;
}
}
if (mPreventForHorizontal) {
return dispatchTouchEventSupper(e);
}
2) 获取视图移动方向
moveDown:向下移动
moveUp:!moveDown
canMoveUp:判断是否可以向上滑动(return mCurrentPos > POS_START;)
boolean moveDown = offsetY > 0;
boolean moveUp = !moveDown;
boolean canMoveUp = mPtrIndicator.hasLeftStartPosition();
3)视图向下滑动通过checkCanDoRefresh()方法是否滚动到内容视图顶部,没滚动顶部则继续向下传递事件,不拦截处理
if (moveDown && mPtrHandler != null && !mPtrHandler.checkCanDoRefresh(this, mContent, mHeaderView)) {
return dispatchTouchEventSupper(e);
}
4)判断向下滑动或者向上滑动同时下拉刷新显示视图显示(canMoveUp)时调整mHeaderView和mContent的偏移位置
if ((moveUp && canMoveUp) || moveDown) {
movePos(offsetY); //滑动时更新新的偏移位置
return true;
}
5)判断视图是否满足开始执行刷新条件
mPtrHandler.checkCanDoRefresh(this, mContent, mHeaderView)
public static boolean canChildScrollUp(View view) {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (view instanceof AbsListView) {
final AbsListView absListView = (AbsListView) view;
//第一条可显示内容大于0,可以下拉或者第一个孩子的相对父视图顶部的位置小于父视图paddingTop
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return view.getScrollY() > 0;//>0表示可以下拉,<=0不可以下拉了
}
} else {
//canScrollVertically(-1); 滑到最顶部时,返回false,意思是不能下拉了
return view.canScrollVertically(-1);
}
}
/**
* Default implement for check can perform pull to refresh
*
* @param frame
* @param content
* @param header
* @return
*/
public static boolean checkContentCanBePulledDown(PtrFrameLayout frame, View content, View header) {
return !canChildScrollUp(content);
}
@Override
public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) {
return checkContentCanBePulledDown(frame, content, header);
}
当然以上给出的是针对通用 View 的判断方式。如果遇到特殊需求的 View,或者自定义 View。使用者还是要自己实现符合需求的判断。
6) movePos(offsetY); //滑动时更新新的偏移位置(判断可以滑动或者向上滑动同时headView显示出来了,则可以更新滑动位置)
/**
* if deltaY > 0, 向下滑动
*
* @param deltaY
*/
private void movePos(float deltaY) {
// 已经到达了顶部(内容视图的顶部),则不需要更新滑动位置
if ((deltaY < 0 && mPtrIndicator.isInStartPosition())) {
if (DEBUG) {
PtrCLog.e(LOG_TAG, String.format("has reached the top"));
}
return;
}
int to = mPtrIndicator.getCurrentPosY() + (int) deltaY;//已经滑动距离+当前滑动到的距离
// over top
if (mPtrIndicator.willOverTop(to)) {//已经超过顶部
if (DEBUG) {
PtrCLog.e(LOG_TAG, String.format("over top"));
}
//设置目标位置为0
to = PtrIndicator.POS_START;
}
mPtrIndicator.setCurrentPos(to); //更新当前headView偏移位置,在onLayout布局时会用到,控制显示位置
int change = to - mPtrIndicator.getLastPosY();//计算变化距离
updatePos(change);
}
updatePos(change);更新headView和内容视图位置;
mHeaderView.offsetTopAndBottom(change);
if (!isPinContent()) {
mContent.offsetTopAndBottom(change);
}
invalidate();
5.3.擡起事件
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mPtrIndicator.onRelease();//设置触摸变量为false,表示触摸事件结束
if (mPtrIndicator.hasLeftStartPosition()) {//判断headView是否显示,需要刷新
if (DEBUG) {
PtrCLog.d(LOG_TAG, "call onRelease when user release");
}
onRelease(false); //检测是否需要执行刷新操作
if (mPtrIndicator.hasMovedAfterPressedDown()) {
sendCancelEvent();
return true;
}
return dispatchTouchEventSupper(e);
} else {
return dispatchTouchEventSupper(e);
}
1)设置触摸变量为false,表示触摸事件结束
mPtrIndicator.onRelease();
public void onRelease() {
mIsUnderTouch = false;
}
2)检测是否需要执行刷新操作
private void onRelease(boolean stayForLoading)
private void onRelease(boolean stayForLoading) {
tryToPerformRefresh();//执行去刷新
if (mStatus == PTR_STATUS_LOADING) {
// 在刷新状态时保持头部显示
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();
}
}
}
设置刷新保留头部时,若下拉滚动距离过大时则滚动恢复到设置的偏移位置;
mScrollChecker.tryToScrollTo(mPtrIndicator.getOffsetToKeepHeaderWhileLoading(), mDurationToClose);
3)尝试去执行刷新操作
判断是否满足刷新条件,自动刷新表示为true同时头部偏移的位置大于刷新时保持头部的偏移位置或者偏移位置大于设置的头部偏移位置,则更新状态值为刷新状态,同时执行刷新操作;
private boolean tryToPerformRefresh() {
if (mStatus != PTR_STATUS_PREPARE) {//非准备状态,不继续处理
return false;
}
//判断是否满足刷新条件
if ((mPtrIndicator.isOverOffsetToKeepHeaderWhileLoading() && isAutoRefresh()) || mPtrIndicator.isOverOffsetToRefresh()) {
mStatus = PTR_STATUS_LOADING; //满足则更新为刷新状态中
performRefresh(); //执行开始刷新回调
}
return false;
}
4)performRefresh()执行刷新回调
实现PtrUIHandler接口的头部视图可以修改头部视图样式;
实现PtrHandler接口的类,接收到刷新开始通知,执行网络数据刷新操作;
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);
}
}
6.自动刷新实现
设置页面打开时自动刷新
public void autoRefresh(boolean atOnce) {
autoRefresh(atOnce, mDurationToCloseHeader);
}
执行视图刷新状态显示
public void autoRefresh(boolean atOnce, int duration) {
if (mStatus != PTR_STATUS_INIT) {
return;
}
mFlag |= atOnce ? FLAG_AUTO_REFRESH_AT_ONCE : FLAG_AUTO_REFRESH_BUT_LATER;
mStatus = PTR_STATUS_PREPARE;
if (mPtrUIHandlerHolder.hasHandler()) {
mPtrUIHandlerHolder.onUIRefreshPrepare(this);
if (DEBUG) {
PtrCLog.i(LOG_TAG, "PtrUIHandler: onUIRefreshPrepare, mFlag %s", mFlag);
}
}
mScrollChecker.tryToScrollTo(mPtrIndicator.getOffsetToRefresh(), duration);
if (atOnce) {
mStatus = PTR_STATUS_LOADING;
performRefresh();
}
}
a. mPtrUIHandlerHolder.onUIRefreshPrepare(this);通知headView准备刷新,可在该方法实现headView样式修改;
b.mScrollChecker.tryToScrollTo(mPtrIndicator.getOffsetToRefresh(), duration);滚动视图设定的偏移位置;
c.更新状态为加载中PTR_STATUS_LOADING;
d.performRefresh();执行刷新回调通知headView和相应界面准备加载内容;
7.数据加载完成通知PtrClassicFrameLayout刷新完成
调用refreshComplete()方法通知PtrClassicFrameLayout数据加载完成,恢复视图显示状态;
/**
* Call this when data is loaded.
* The UI will perform complete at once or after a delay, depends on the time elapsed is greater then {@link #mLoadingMinTime} or not.
*/
final public void refreshComplete() {
if (DEBUG) {
PtrCLog.i(LOG_TAG, "refreshComplete");
}
if (mRefreshCompleteHook != null) {
mRefreshCompleteHook.reset();
}
int delay = (int) (mLoadingMinTime - (System.currentTimeMillis() - mLoadingStartTime));
if (delay <= 0) {
if (DEBUG) {
PtrCLog.d(LOG_TAG, "performRefreshComplete at once");
}
performRefreshComplete();
} else {
postDelayed(mPerformRefreshCompleteDelay, delay);
if (DEBUG) {
PtrCLog.d(LOG_TAG, "performRefreshComplete after delay: %s", delay);
}
}
}
1)mRefreshCompleteHook.reset();设置钩子任务为准备状态;
2)performRefreshComplete();执行刷新完成操作;
3)区分自动刷新和手动刷新处理UI完成
/**
* Do refresh complete work when time elapsed is greater than {@link #mLoadingMinTime}
*/
private void performRefreshComplete() {
mStatus = PTR_STATUS_COMPLETE;
// 如果是自动刷新,等待scroller停止
if (mScrollChecker.mIsRunning && isAutoRefresh()) {
// do nothing
if (DEBUG) {
PtrCLog.d(LOG_TAG, "performRefreshComplete do nothing, scrolling: %s, auto refresh: %s",
mScrollChecker.mIsRunning, mFlag);
}
return;
}
//通知UI刷新完成
notifyUIRefreshComplete(false);
}
4)判断是否有钩子任务,若有先执行钩子任务
private void notifyUIRefreshComplete(boolean ignoreHook) {
/**
* After hook operation is done, {@link #notifyUIRefreshComplete} will be call in resume action to ignore hook.
*/
if (mPtrIndicator.hasLeftStartPosition() && !ignoreHook && mRefreshCompleteHook != null) {
if (DEBUG) {
PtrCLog.d(LOG_TAG, "notifyUIRefreshComplete mRefreshCompleteHook run.");
}
mRefreshCompleteHook.takeOver();
return;
}
if (mPtrUIHandlerHolder.hasHandler()) {
if (DEBUG) {
PtrCLog.i(LOG_TAG, "PtrUIHandler: onUIRefreshComplete");
}
mPtrUIHandlerHolder.onUIRefreshComplete(this);
}
mPtrIndicator.onUIRefreshComplete();
tryScrollBackToTopAfterComplete();
tryToNotifyReset();
}
a.先判断是否有钩子任务,有先执行;
b.通知headView刷新完成,在回调方法中修改headView样式;
c.恢复内容显示到顶部;
8.PtrClassicFrameLayout相关参数说明
UltraPTR 的核心类,自定义控件类。作为自定义控件, UltraPTR 有 8 个自定义属性。ptr_header
,设置头部 id,ptr_content
,设置内容 id,ptr_resistance
,阻尼系数,默认: 1.7f
,越大,感觉下拉时越吃力
,ptr_ratio_of_header_height_to_refresh
,触发刷新时移动的位置比例,默认,1.2f
,移动达到头部高度 1.2 倍时可触发刷新操作
,ptr_duration_to_close
,回弹延时,默认 200ms
,回弹到刷新高度所用时间
,ptr_duration_to_close_header
,头部回弹时间,默认 1000ms
,ptr_pull_to_fresh
,刷新是否保持头部,默认值 true
,ptr_keep_header_when_refresh
,下拉刷新 / 释放刷新,默认为释放刷新。
9.自定义HeadView
package com.github.baby.owspace.view.widget;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.github.baby.owspace.R;
import com.github.baby.owspace.app.GlideApp;
import com.orhanobut.logger.Logger;
import java.text.SimpleDateFormat;
import java.util.Date;
import in.srain.cube.views.ptr.PtrFrameLayout;
import in.srain.cube.views.ptr.PtrUIHandler;
import in.srain.cube.views.ptr.indicator.PtrIndicator;
/**
* Created by Mr.Yangxiufeng
* DATE 2016/10/10
* owspace
*/
public class CustomPtrHeader extends FrameLayout implements PtrUIHandler{
private final static String KEY_SharedPreferences="CustomPtrHeader_last_update";
private long mLastUpdateTime = -1;
private TextView mLastUpdateTextView;
private String mLastUpdateTimeKey;
private ImageView refreshImage;
private Context context;
private static SimpleDateFormat sDataFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private boolean mShouldShowLastUpdate;
private LastUpdateTimeUpdater mLastUpdateTimeUpdater = new LastUpdateTimeUpdater();
public CustomPtrHeader(Context context,int mode) {
super(context);
this.context = context;
mLastUpdateTimeKey = "CustomPtrHeader_last_update_mode"+mode;
init(context);
}
@Override
public void onUIReset(PtrFrameLayout frame) {
Logger.d("onUIReset...............");
refreshImage.setVisibility(GONE);
}
@Override
public void onUIRefreshPrepare(PtrFrameLayout frame) {
Logger.d("onUIRefreshPrepare...............");
mShouldShowLastUpdate = true;
tryUpdateLastUpdateTime();
mLastUpdateTimeUpdater.start();
refreshImage.setVisibility(VISIBLE);
}
@Override
public void onUIRefreshBegin(PtrFrameLayout frame) {
Logger.d("onUIRefreshBegin...............");
tryUpdateLastUpdateTime();
mLastUpdateTimeUpdater.stop();
}
@Override
public void onUIRefreshComplete(PtrFrameLayout frame) {
Logger.d("onUIRefreshComplete...............");
// 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) {
}
/**
* Specify the last update time by this key string
*
* @param key
*/
public void setLastUpdateTimeKey(String key) {
if (TextUtils.isEmpty(key)) {
return;
}
mLastUpdateTimeKey = key;
}
private void tryUpdateLastUpdateTime() {
if (TextUtils.isEmpty(mLastUpdateTimeKey) || !mShouldShowLastUpdate) {
mLastUpdateTextView.setVisibility(GONE);
} else {
String time = getLastUpdateTime();
if (TextUtils.isEmpty(time)) {
mLastUpdateTextView.setVisibility(GONE);
} else {
mLastUpdateTextView.setVisibility(VISIBLE);
mLastUpdateTextView.setText(time);
}
}
}
private String getLastUpdateTime() {
if (mLastUpdateTime == -1 && !TextUtils.isEmpty(mLastUpdateTimeKey)) {
mLastUpdateTime = getContext().getSharedPreferences(KEY_SharedPreferences, 0).getLong(mLastUpdateTimeKey, -1);
}
if (mLastUpdateTime == -1) {
return null;
}
long diffTime = new Date().getTime() - mLastUpdateTime;
int seconds = (int) (diffTime / 1000);
if (diffTime < 0) {
return null;
}
if (seconds <= 0) {
return null;
}
StringBuilder sb = new StringBuilder();
sb.append("上次更新:");
if (seconds < 60) {
sb.append(seconds + "秒前");
} else {
int minutes = (seconds / 60);
if (minutes > 60) {
int hours = minutes / 60;
if (hours > 24) {
Date date = new Date(mLastUpdateTime);
sb.append(sDataFormat.format(date));
} else {
sb.append(hours + "小时前");
}
} else {
sb.append(minutes + "分钟前");
}
}
return sb.toString();
}
private void init(Context context){
View view = LayoutInflater.from(context).inflate(
R.layout.refresh_header, this);
refreshImage = (ImageView)view.findViewById(R.id.refresh_loading);
mLastUpdateTextView = (TextView)view.findViewById(R.id.latest_fresh_time);
GlideApp.with(context).load(R.drawable.refresh_loading).into(refreshImage);
mShouldShowLastUpdate=true;
tryUpdateLastUpdateTime();
}
private class LastUpdateTimeUpdater implements Runnable {
private boolean mRunning = false;
private void start() {
if (TextUtils.isEmpty(mLastUpdateTimeKey)) {
return;
}
mRunning = true;
run();
}
private void stop() {
mRunning = false;
removeCallbacks(this);
}
@Override
public void run() {
tryUpdateLastUpdateTime();
if (mRunning) {
postDelayed(this, 1000);
}
}
}
}
参考:
https://www.jianshu.com/p/1602d7207492
https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh