先上效果图吧,项目需求是这样一个效果,类似uc浏览器的下拉刷新,当然,我这个要差得多。。。
大概的效果就是:
1.下拉的时候画一个带点的圆弧,根据下拉距离渐进闭合;
2.当拉倒可以刷新的距离后,放手刷新,再画一个带点的圆弧旋转;
3.刷新完成后,画一个渐进闭合的圆;
4.最后,画一个钩,然后关闭header。
在实现的过程中有两个地方比较难办,一个是打钩的过程,另一个是header驻留一下才收起。
对于打钩的过程,我试过贝塞尔曲线,但是三阶的我没尝试过,二阶贝塞尔画出来的钩是带弧度的,就不是直角,我也尝试过path,可能是由于打钩的拐点没计算好,导致钩不是很平滑,后来才选择了用直线拐点的方式,这是借用的这个项目 android打钩画叉,非常感谢。有需要的童鞋请自行传送过去看看。
对于header驻留在关闭的问题,令我一度陷入僵局,因为我翻看了PtrFrameLayout的源码,发现即使自定义PtrHandler,PtrHandler收到onUIRefreshComplete的时候是在PtrFrameLayout已经调onRefreshComplete用后再notify的,所以要实现header关闭的动作在我打完钩之后在执行,就找不到入口了。看下PtrFrameLayout refreshComplete的代码
/**
* 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);
}
}
}
当我们数据加载完成后,大家都应该会调用这个方法吧,注释的很清楚,这里说到可能会延迟关闭,那取决于我们设置的 ptrRefresh.setLoadingMinTime(long delayTime),但是,我们怎么能保证数据加载的时间不会大于这个时间呢?所以想通过设置这个时间来延迟关闭header在打钩之后是不行的。
我们接着看performRefreshComplete()方法
/**
* Do refresh complete work when time elapsed is greater than{@link#mLoadingMinTime}
*/
private void performRefreshComplete() {
mStatus = PTR_STATUS_COMPLETE;
// if is auto refresh do nothing, wait scroller stop
if (mScrollChecker.mIsRunning && isAutoRefresh()) {
// do nothing
if (DEBUG) {
PtrCLog.d(LOG_TAG, "performRefreshComplete do nothing, scrolling: %s, auto refresh: %s",
mScrollChecker.mIsRunning, mFlag);
}
return;
}
notifyUIRefreshComplete(false);
}
这个时候, mStatus = PTR_STATUS_COMPLETE,PtrFrameLayout已经设为刷新完成的状态了,开始通知PtrHandler onUIRefreshComplete了,这也是我解决问题的转折点,我们看notifyUIRefreshComplete(boolean ignoreHook)方法
/**
* Do real refresh work. If there is a hook, execute the hook first.
* @param ignoreHook
*/
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();
}
看到mPtrUIHandlerHolder.onUIRefreshComplete(this),这个时候我们的PtrHandler就已经收到刷新完成的通知了,正常流程的话,header就开始关闭了。但是大家请看这句注释,After hook operation is done, {@link #notifyUIRefreshComplete} will be call in resume action to ignore hook,这里有一个关键的东西mRefreshCompleteHook,就是PtrUIHandlerHook,看这个名字你就知道他大概是干吗用的了,他就是截断header关闭的关键。他继承自Runnable 接口,我们看看他的代码
/**
* Run a hook runnable, the runnable will run only once.
* After the runnable is done, call resume to resume.
* Once run, call takeover will directory call the resume action
*/
public abstract class PtrUIHandlerHook implements Runnable {
private Runnable mResumeAction;
private static final byte STATUS_PREPARE = 0;
private static final byte STATUS_IN_HOOK = 1;
private static final byte STATUS_RESUMED = 2;
private byte mStatus = STATUS_PREPARE;
public void takeOver() {
takeOver(null);
}
public void takeOver(Runnable resumeAction) {
if (resumeAction != null) {
mResumeAction = resumeAction;
}
switch (mStatus) {
case STATUS_PREPARE:
mStatus = STATUS_IN_HOOK;
run();
break;
case STATUS_IN_HOOK:
break;
case STATUS_RESUMED:
resume();
break;
}
}
public void reset() {
mStatus = STATUS_PREPARE;
}
public void resume() {
if (mResumeAction != null) {
mResumeAction.run();
}
mStatus = STATUS_RESUMED;
}
/**
* Hook should always have a resume action, which is hooked by this hook.
*
* @param runnable
*/
public void setResumeAction(Runnable runnable) {
mResumeAction = runnable;
}
}
PtrUIHandlerHook允许外界设置一个Runnable在resume的时候执行,这就意味着我们可以在截断PtrFrameLayout关闭header后做一些自己的事情,这些事情可以放到PtrUIHandlerHook的run方法里,因为notifyUIRefreshComplete(boolean ignoreHook)有这样一句代码 –>mRefreshCompleteHook.takeOver(),这会触发PtrUIHandlerHook的run方法,比如我们把通知加载完成的事情放在这个润方法里。当我们的事情执行完了,再去调用PtrUIHandlerHook的resume方法,通知PtrFrameLayout关闭header。为什么可以通知PtrFrameLayout关闭header呢?因为在PtrFrameLayout里被有这样一个方法
/**
* please DO REMEMBER resume the hook
*
* @param hook
*/
public void setRefreshCompleteHook(PtrUIHandlerHook hook) {
mRefreshCompleteHook = hook;
hook.setResumeAction(new Runnable() {
@Override
public void run() {
if (DEBUG) {
PtrCLog.d(LOG_TAG, "mRefreshCompleteHook resume.");
}
notifyUIRefreshComplete(true);
}
});
}
我们把自定义的PtrUIHandlerHook放进去,PtrFrameLayout默认设置的ResumeAction的run方法执行的是啥?就是notifyUIRefreshComplete(boolean ignoreHook)方法!!!大家再回去看看notifyUIRefreshComplete(boolean ignoreHook)方法,这个时候传入的ignoreHook是true,那么会发生什么呢?对,就是PtrFrameLayout正常关闭header的流程,而这个流程发生在哪里?就是在我们手动调用PtrUIHandlerHook的resume方法的时候,至于大脚要在哪里去调PtrUIHandlerHook的resume方法,大家尽情发挥想象吧。
到了这个时候,整个流程就变得清晰了:
1.我们自定义一个PtrUIHandlerHook,在run方法里通知刷新完成,但这时候PtrFrameLayout还未执行关闭header等动作;
2.收到刷新的组件该干嘛干嘛,干完事情后,通知PtrUIHandlerHook,你可以resume了,这时候PtrUIHandlerHook执行PtrFrameLayout事先设入的Runnable的run方法,再次调用notifyUIRefreshComplete(boolean ignoreHook)方法,这个时候传入的ignoreHook是true,notifyUIRefreshComplete(boolean ignoreHook)方法就会正常执行关闭header等动作,刷新完成。
至此,延迟关闭header的原理就清楚了,剩下的就是代码实现的事情了,我是通过定义接口的方式来降低刷新组件、PtrUIHandler、PtrUIHandlerHook的耦合的,大家卡伊参考一下
自定义PtrUIHandler
package com.ykbjson.demo.customview.ptrheader;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import com.ykbjson.demo.R;
import in.srain.cube.views.ptr.PtrFrameLayout;
import in.srain.cube.views.ptr.PtrUIHandler;
import in.srain.cube.views.ptr.indicator.PtrIndicator;
/**
* 包名:com.ykbjson.demo.customview.ptrheader
* 描述:自定义下拉刷新头部
* 创建者:yankebin
* 日期:2016/12/7
*/
public class CustomerPtrHandler extends FrameLayout implements PtrUIHandler,
CustomerPtrUIHandlerHook.OnPtrUIHandlerHookCallback, PullLoadingView.OnRefreshCompleteCallback {
// 下拉图标
private PullLoadingView pullLoadingView;
private CustomerPtrUIHandlerHook handlerHook;
public CustomerPtrHandler(Context context) {
this(context, null);
}
public CustomerPtrHandler(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomerPtrHandler(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
/**
* 初始化
*
* @param context
*/
private void init(Context context) {
inflate(context, R.layout.item_refresh_header, this);
pullLoadingView = (PullLoadingView) findViewById(R.id.iv_rotate);
pullLoadingView.setCompleteCallback(this);
}
public void setLoadingColor(int loadingColor) {
if (checkNotNull(pullLoadingView)) {
pullLoadingView.setLoadingColor(loadingColor);
}
}
private boolean checkNotNull(Object o) {
return null != o;
}
@Override
public void onUIReset(PtrFrameLayout ptrFrameLayout) {
if (checkNotNull(pullLoadingView)) {
pullLoadingView.setMode(PullLoadingView.MODE_INIT);
}
}
@Override
public void onUIRefreshPrepare(PtrFrameLayout ptrFrameLayout) {
if (!checkNotNull(handlerHook)) {
handlerHook = new CustomerPtrUIHandlerHook(this);
ptrFrameLayout.setRefreshCompleteHook(handlerHook);
if (checkNotNull(pullLoadingView)) {
pullLoadingView.setMaxPullY(ptrFrameLayout.getOffsetToRefresh());
}
//修改loading最少驻留时间,不要一闪而过
ptrFrameLayout.setLoadingMinTime(2000);
//header关闭时间
ptrFrameLayout.setDurationToCloseHeader(1500);
}
}
@Override
public void onUIRefreshBegin(PtrFrameLayout ptrFrameLayout) {
if (checkNotNull(pullLoadingView)) {
pullLoadingView.setMode(PullLoadingView.MODE_LOADING);
}
}
@Override
public void onUIRefreshComplete(final PtrFrameLayout ptrFrameLayout) {
}
@Override
public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator) {
if (!isUnderTouch || status == PtrFrameLayout.PTR_STATUS_LOADING || status == PtrFrameLayout.PTR_STATUS_COMPLETE) {
return;
}
final int currentPos = ptrIndicator.getCurrentPosY();
if (checkNotNull(pullLoadingView)) {
pullLoadingView.onUIPositionChange(currentPos);
}
}
@Override
public void onPtrUIHandlerHookStart() {
if (checkNotNull(pullLoadingView)) {
pullLoadingView.setMode(PullLoadingView.MODE_LOADING_COMPLETE);
}
//如果没有pullLoadingView,强制hook resume,不然会导致刷新视图无法还原
else {
onRefreshComplete();
}
}
@Override
public void onRefreshComplete() {
if (checkNotNull(handlerHook)) {
handlerHook.resume();
}
}
}
自定义PtrUIHandlerHook
package com.ykbjson.demo.customview.ptrheader;
import in.srain.cube.views.ptr.PtrUIHandlerHook;
/**
* 包名:com.ykbjson.demo.customview.ptrheader
* 描述:下拉刷新关闭header的hook,可以再这里处理关闭header之前的事情
* 创建者:yankebin
* 日期:2016/12/7
*/
public class CustomerPtrUIHandlerHook extends PtrUIHandlerHook {
public interface OnPtrUIHandlerHookCallback{
void onPtrUIHandlerHookStart();
}
private OnPtrUIHandlerHookCallback handlerHookCallback;
public CustomerPtrUIHandlerHook(OnPtrUIHandlerHookCallback handlerHookCallback) {
this.handlerHookCallback = handlerHookCallback;
}
@Override
public void run() {
if (null != handlerHookCallback) {
handlerHookCallback.onPtrUIHandlerHookStart();
}
}
}
自定义PullLoadingView
package com.ykbjson.demo.customview.ptrheader;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.widget.ImageView;
/**
* 包名:com.ykbjson.demo.customview.ptrheader
* 描述:下拉刷新加载视图
* 创建者:yankebin
* 日期:2016/12/7
*/
public class PullLoadingView extends ImageView {
public static final int MODE_INIT = -1;
public static final int MODE_PULLING = MODE_INIT + 1;
public static final int MODE_LOADING = MODE_INIT + 2;
public static final int MODE_LOADING_COMPLETE = MODE_INIT + 3;
public static final int MODE_ALL_COMPLETE = MODE_INIT + 4;
private static final int CIRCLE_DEGREE = 360;
private static final String LOADING_TEXT = "loading...";
private int delayToCloseHeader = 1000;
private float maxPullY;
private float width;
private float height;
private Paint arcPaint;
private Paint textPaint;
private RectF rectF;
private int degree;
private int mode = MODE_INIT;
private float currentPosition;
private long invalidDelayTime = 30L;
private float loadingTextWidth;
private float line1X;
private float line1Y;
private float line2X;
private float line2Y;
private float radius;
private float center;
private float checkStartX;
private float step;
private float lineThick;
private boolean isNeedDrawSecondLine;
private OnRefreshCompleteCallback completeCallback;
private final Canvas mCanvas;//当布局本身或父布局被隐藏后,不会回调onDraw方法,此时手动传入此参数强制驱动onDraw方法执行,保证绘画不被阻断
public interface OnRefreshCompleteCallback {
void onRefreshComplete();
}
public PullLoadingView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PullLoadingView(Context context) {
this(context, null);
}
public PullLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mCanvas = new Canvas();
final float density = getResources().getDisplayMetrics().density;
step = density * 2;
lineThick = density * 2;
arcPaint = new Paint();
arcPaint.setColor(Color.RED);
arcPaint.setAntiAlias(true);
arcPaint.setStrokeWidth(lineThick);
arcPaint.setStyle(Paint.Style.STROKE);
textPaint = new Paint();
textPaint.setColor(Color.RED);
textPaint.setAntiAlias(true);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setTextSize(6 * density);
loadingTextWidth = textPaint.measureText(LOADING_TEXT);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
width = w;
height = h;
center = width / 2;
radius = width / 2 - lineThick;
checkStartX = center - width / 5;
rectF = new RectF(lineThick, lineThick, width - lineThick, height - lineThick);
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
//打钩
if (mode == MODE_ALL_COMPLETE) {
//画圆
canvas.drawArc(rectF, 0, CIRCLE_DEGREE, false, arcPaint);
if (line1X < radius / 3) {
line1X += step;
line1Y += step;
}
//画第一根线
canvas.drawLine(checkStartX, center, checkStartX + line1X, center + line1Y, arcPaint);
if (line1X >= radius / 3) {
if (!isNeedDrawSecondLine) {
line2X = line1X;
line2Y = line1Y;
isNeedDrawSecondLine = true;
}
line2X += step;
line2Y -= step;
//画第二根线
canvas.drawLine(checkStartX + line1X - lineThick / 2,
center + line1Y, checkStartX + line2X, center + line2Y, arcPaint);
}
if (line2X > radius) {
line1X = line1Y = line2X = line2Y = 0;
isNeedDrawSecondLine = false;
refreshComplete();
return;
}
uiChange();
}
//画圆
else if (mode == MODE_LOADING_COMPLETE) {
//画圆弧
canvas.drawArc(rectF, -CIRCLE_DEGREE / 4, degree, false, arcPaint);
//画圆和勾的过程做完后停止刷新界面
if (degree == CIRCLE_DEGREE) {
setMode(MODE_ALL_COMPLETE);
return;
}
//继续画圆
checkDegree();
uiChange();
}
//渐进闭合圆
else if (mode == MODE_PULLING) {
float sweepAngle = currentPosition / maxPullY * CIRCLE_DEGREE;
canvas.drawArc(rectF, -CIRCLE_DEGREE / 4, sweepAngle, false, arcPaint);
canvas.drawArc(rectF, sweepAngle - (CIRCLE_DEGREE / 4 - 5), 5, false, arcPaint);
}
//旋转圆弧
else if (mode == MODE_LOADING) {
canvas.drawText(LOADING_TEXT, (width - loadingTextWidth) / 2, height / 2, textPaint);
canvas.drawArc(rectF, degree, CIRCLE_DEGREE / 8, false, arcPaint);
canvas.drawArc(rectF, degree + CIRCLE_DEGREE / 8 + 2, 5, false, arcPaint);
degree += 10;
if (degree == CIRCLE_DEGREE) {
degree = 0;
}
uiChange();
}
//初始化
else {
super.draw(canvas);
}
}
public void setCompleteCallback(OnRefreshCompleteCallback completeCallback) {
this.completeCallback = completeCallback;
}
public void setDelayToCloseHeader(int delayToCloseHeader) {
this.delayToCloseHeader = delayToCloseHeader;
}
public void setMaxPullY(float maxPullY) {
this.maxPullY = maxPullY;
}
/**
* 设置动画的颜色,包括文字和圆圈以及打钩的线
*
* @param loadingColor
*/
public void setLoadingColor(int loadingColor) {
arcPaint.setColor(loadingColor);
textPaint.setColor(loadingColor);
}
public void setStep(float step) {
this.step = step;
}
/**
* 设置当前模式
*
* @param mode
*/
public void setMode(int mode) {
degree = 0;
if (mode == MODE_PULLING) {
this.mode = mode;
dispatchInvalidate();
} else {
if (this.mode != mode) {
this.mode = mode;
if (mode == MODE_LOADING_COMPLETE) {
invalidDelayTime = 50L;
} else if (mode == MODE_LOADING) {
invalidDelayTime = 15L;
} else if (mode == MODE_ALL_COMPLETE) {
invalidDelayTime = 5L;
} else {
invalidDelayTime = 30L;
}
uiChange();
}
}
}
/**
* 检测角度
*/
private void checkDegree() {
if (degree < CIRCLE_DEGREE) {
degree += 45;
if (degree > CIRCLE_DEGREE) {
degree = CIRCLE_DEGREE;
}
}
}
/**
* ui变化
*/
private synchronized void uiChange() {
removeCallbacks(invalidateRunner);
postDelayed(invalidateRunner, invalidDelayTime);
}
/**
* 刷新完成
*/
private synchronized void refreshComplete() {
removeCallbacks(closeHeaderRunner);
postDelayed(closeHeaderRunner, delayToCloseHeader);
}
/**
* 下拉距离变化
*
* @param currentPos
*/
public void onUIPositionChange(int currentPos) {
currentPosition = currentPos;
setMode(MODE_PULLING);
}
/**
* 根据view的可见性决定调postInvalidate方法或传入空canvas去驱动onDraw方法继续执行,保证绘画不被阻断
*/
private void dispatchInvalidate() {
if (isShown()) {
postInvalidate();
} else {
draw(mCanvas);
}
}
private Runnable invalidateRunner = new Runnable() {
@Override
public void run() {
dispatchInvalidate();
}
};
private Runnable closeHeaderRunner = new Runnable() {
@Override
public void run() {
if (null != completeCallback) {
completeCallback.onRefreshComplete();
}
}
};
}
再啰嗦几句,在实现这个效果的过程中还遇到了一个问题,当某个界面绘制还未完成时,打开了新页面,正在绘制的界面stop了,这个时候应该是android系统因为优化内存的原因,不可见的视图不会再调用draw相关方法的,即使你手动调用postInvalidate或是invalidate或是requestLayout方法,当我们的新页面关闭后,原来的页面resume了,view就会执行draw相关的方法,因为我的这个效果是纯绘制的,所有靠的是在draw方法里循环调用postInvalidate来驱动绘制,当界面stop后,收到了刷新结束的通知,但是因为draw方法没法继续驱动,所以状态就一直停留在了刷新结束的状态,还未闭合圆和打钩,当界面可见后,按照正常时间推算,这个界面的刷新早已结束,应该是看不见header的,但是由于刚才的那个原因,这个时候还会从界面刚刷新结束时的状态开始继续执行。所以这里才有dispatchInvalidate这个方法。
为了解决这个问题,我尝试了SurfaceView,子线程绘制,不受界面影响,但是最后放弃的原因是那个黑块块没办法去掉。唯一能去掉的办法就是每个有下拉刷新的页面都要给自定义PullLoadingView设置一个背景颜色,但是我们目前涉及的界面较多,色值严重不统一,所以我最后放弃了。最后还是继续用view来实现,只不过根据isShown方法来判断当前界面是否可见,不可见的时候给一个空画布手动驱动draw方法执行,驱动状态正常执行,由于是靠状态来绘制的,所以即使你很快的来回切页面也不会出现绘制错乱的问题。
最后就是要说一下isShown这个方法,大家可以去看看他的源码和注释,这和getVisibility方法还是有很大区别的,他的返回值是遍历整个view的树的可见性得到的结果,所以他的返回值是比较可靠的,这也是我解决我所遇到的问题的一个关键点。