啦啦啦,这是山寨UC浏览器的下拉刷新效果的第二篇,第一篇请移步Android 自定义View UC下拉刷新效果(一)
我们看图说话:
主要工作
1.下拉刷新的圆形向回首页的圆形的过度以及返回的效果。
2.View的事件分发等等。
3.相关接口回调。
对于第一块,就是这个切换是的效果,其实在Android drawPath实现QQ拖拽泡泡我的第一篇文章中就讲了,主要就是使用贝塞尔曲线来实现的。
只是这里我试着使用了四阶的贝塞尔曲线,因为控制点如果就一个的话,看起来有时候会觉得那个弧度拉得特别的尖,一点都不好看,而且我山寨的这个效果也没有UC的那个那么帅气,可能还需要做相关的改进,如果你有好的点子请记得给我留言,一起完善嘛!!
private void drawSecPath(Canvas canvas) {
path.reset();
path.moveTo((float) (secondRectf.centerX() + Math.cos(180 / Math.PI * 30) * (secondRectf.centerX() - secondRectf.left)), (float) (secondRectf.centerY() + Math.sin(180 / Math.PI * 30) * (secondRectf.centerY() - secondRectf.top)));
path.cubicTo(secondRectf.centerX() - 10*density, secondRectf.centerY() - backpaths, secondRectf.centerX() + 10*density, secondRectf.centerY() - backpaths, (float) (secondRectf.centerX() - Math.cos(180 / Math.PI * 30) * (secondRectf.centerX() - secondRectf.left)), (float) (secondRectf.centerY() + Math.sin(180 / Math.PI * 30) * (secondRectf.centerY() - secondRectf.top)));
//path.quadTo(secondRectf.centerX(), secondRectf.centerY() - backpaths, (float) (secondRectf.centerX() - Math.cos(180 / Math.PI * 30) * (secondRectf.centerX() - secondRectf.left)), (float) (secondRectf.centerY() + Math.sin(180 / Math.PI * 30) * (secondRectf.centerY() - secondRectf.top)));
canvas.drawArc(secondRectf, 0, 360, true, secPaint);
canvas.drawPath(path, secPaint);
//drawArc(canvas);
}
private void drawFirstPath(Canvas canvas) {
path.reset();
path.moveTo((float) (outRectF.centerX() - Math.cos(180 / Math.PI * 30) * (outRectF.centerX() - outRectF.left)), (float) (outRectF.centerY() - Math.sin(180 / Math.PI * 30) * (outRectF.centerY() - outRectF.top)));
//path.quadTo(outRectF.centerX(), outRectF.centerY() + paths, (float) (outRectF.centerX() + Math.cos(180 / Math.PI * 30) * (outRectF.centerX() - outRectF.left)), (float) (outRectF.centerY() - Math.sin(180 / Math.PI * 30) * (outRectF.centerY() - outRectF.top)));
path.cubicTo(outRectF.centerX() + 10 * density, outRectF.centerY() + paths, outRectF.centerX() - 10 * density, outRectF.centerY() + paths, (float) (outRectF.centerX() + Math.cos(180 / Math.PI * 30) * (outRectF.centerX() - outRectF.left)), (float) (outRectF.centerY() - Math.sin(180 / Math.PI * 30) * (outRectF.centerY() - outRectF.top)));
canvas.drawArc(outRectF, 0, 360, true, paint);
canvas.drawPath(path, paint);
drawArc(canvas);
}
这里两个控制点的偏移量是写死的,而不是根据圆形的size的百分比计算出来的,所以如果你修改了圆形的半径,那么这里可能会出现小小的问题,需要手动完善下!
下拉刷新
其实现在的下拉刷新也是烂大街的,就我现在理解的下拉刷新其实有两种模式了,一种是之前的写好一个头布局在那个HeaderLayout
中,然后margin将其隐藏掉,然后在下拉的时候拦截相关事件,决定是否应该让Header显示出来。拦截的条件就是子View(ListView ScrollView RecycleView等等是否在顶部了而且手势是向下拉(dy<0)!)
今天我们不说这种下拉,而是介绍Google在Android5.0(希望我没有记错 )提供的嵌套滑动的新机制
向下兼容的问题
从API21(就是5.0开始),ViewParent
的接口里面多了onStartNestedScroll()
、onStopNestedScroll()
等等的方法!当然,对应的ViewGroup
中也有了这些方法,目测是空实现,因为它实现了这个接口嘛。那么问题来了,如果你要向下兼容肿么办呢?!
这里有supportV4包来提供向下兼容,不会写不懂这玩意儿不着急,想想Android新的控件(RecycleView SwipeRefreshLayout NestedScrollView)这些都是支持嵌套滑动滴。。
相关接口方法
NestedScrollingParent
和NestedScrollingChild
这两个接口就是用来实现相关的向下兼容的方法滴。。
This interface should be implemented by
ViewGroup
subclasses that wish to support scrolling operations delegated by a nested child view.
Classes implementing this interface should create a final instance of aNestedScrollingParentHelper
as a field and delegate any View orViewGroup
methods to theNestedScrollingParentHelper
methods of the same signature.
Views invoking nested scrolling functionality should always do so from the relevant ViewCompat
, ViewGroupCompat
or ViewParentCompat
compatibility shim static methods. This ensures interoperability with nested scrolling views on Android 5.0 Lollipop and newer.
这个是NestedScrollingParent
自己的一番解释,可以明确知道,在5.0或者更新的,什么ViewCompat
等就提供了相关支持了(这个就是前面我说的那个嘛!),然后兼容的话,就要用这个,而且还要使用一个叫NestedScrollingParentHelper
的辅助类来统一处理一些东西。
然后是不是感觉要哔了狗了,这么多方法要实现?!其实我也是醉醉的,然后打算抄抄别人的就好了!
private final NestedScrollingParentHelper mNestedScrollingParentHelper;
private final NestedScrollingChildHelper mNestedScrollingChildHelper;
//初始化两个helper
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
然后各种实现的方法中:
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return isEnabled() && canChildScrollUp() && !mReturningToStart && !mRefreshing
&& (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
// Reset the counter of how much leftover scroll needs to be consumed.
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
// Dispatch up to the nested parent
startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL);
mTotalUnconsumed = 0;
mNestedScrollInProgress = true;
}
@Override
public boolean hasNestedScrollingParent() {
return mNestedScrollingChildHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean onNestedPreFling(View target, float velocityX,
float velocityY) {
return dispatchNestedPreFling(velocityX, velocityY);
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY,
boolean consumed) {
return dispatchNestedFling(velocityX, velocityY, consumed);
}
.......
新的嵌套滑动的分发机制:
子View parent
startNestedScroll ---> onStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScroll ---> onNestedPreScroll
dispatchNestedScroll ---> onNestedScroll
stopNestedScroll ---> onStopNestedScroll
所以说并不是很复杂,其实就是在以前的事件分发的基础上给父View提供了一个消费事件的机会,以前的话,谁接受了DOWN事件,那么之后所有的事件都会交给它处理,直到它不处理的时候才会又依次返回给父View,或者直到新的DOWN事件开始分发。
嵌套滑动的意思就是在子View处理相关事件的时候,可以根据情况反馈给父View,然后根据父View处理的结果再进行下一步的处理!
RecycleView
实现了NestedScrollingChild
,在TouchEvet()中有以下逻辑:
switch (action) {
case MotionEvent.ACTION_DOWN: {
.....
startNestedScroll(nestedScrollAxis);
} break;
case MotionEvent.ACTION_MOVE: {
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}
.....
} break;
case MotionEvent.ACTION_UP: {
resetTouch();
} break;
......
}
private void resetTouch() {
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
stopNestedScroll();
releaseGlows();
}
根据上面的代码可以看出onNestedPreScroll()
,这个就是在子View还没有滑动之前会先走的,如果父View有相关消费,那么子View会计算出父View消费的偏移量,继续消费剩余的偏移量。而在子View的消费的过程中,它会计算出过程中并没有消费的偏移量。
if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
然后回调dispatchNestedScroll
,父View就可以在onNestedScroll()
中进行处理了!
最后在UP或者CANCLE事件中,子View会stopNestedScroll()
,然后父View就走到了onStopNestedScroll()
。整个嵌套滑动到此结束!
具体实现
1.下拉的时候展现头布局
这里其实就是走onNestedScroll()
,因为这个时候子View已经在顶部了,向下拉的dy偏移量它肯定消费不了,所以在onNestedScroll()
中unconsumedY就是父View需要消费的。
2.下拉的过程中又开始向上滑动
这里就需要注意了,这个时候,父View和子View都可以响应和消费对应的事件的,因为他们现在都是可以向上滑动的,但是这里必须要父View优先消费事件,所以这里就要在onNestedPreScroll()中做相关的处理。
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
// if we're in a drag gesture and the user reverses up the we should take those events
if (!header.ismRunning() && dy > 0 && totalDrag > defaulTranslationY) {
Log.e(TAG, "onNestedPreScroll:消费 " + dy);
updateOffset(dy);
consumed[1] = dy;//通知子View我已经消费的偏移量
}
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
if (!header.ismRunning() && dyUnconsumed < 0) {
Log.e(TAG, "onNestedScroll:未消费:: " + dyUnconsumed);
updateOffset(dyUnconsumed);
}
}
OK,到这里,嵌套滑动就基本好了!接下来就是控制头布局的展现了!这里就是直接让子View向下移动,头布局自然就出现了!然后将相关偏移量传到之前的TouchCircleView
中,完成相关动画!
private void updateOffset(int dyUnconsumed) {
totalDrag -= dyUnconsumed * 0.5;
Log.i(TAG, "updateOffset: " + totalDrag);
if (totalDrag < 0) {
totalDrag = 0;
}
if (totalDrag > header.getHeight() * 1.5) {
totalDrag = header.getHeight() * 1.5f;
}
if (targetView != null) {
targetView.setTranslationY(totalDrag);
}
if (!header.ismRunning()) {
header.handleOffset((int) (totalDrag));
}
}
相关方法及回调
//设置为刷新的loading状态
public void setRefresh(boolean refresh) {
if (mRefresh == refresh) {
return;
}
mRefresh = refresh;
header.setRefresh(mRefresh);
}
//刷新失败状态
public void setRefreshError() {
header.setRefreshError();
}
//刷新成功状态
public void setRefreshSuccess() {
header.setRefreshSuccess();
}
mHeader.addLoadingListener(new TouchCircleView.OnLoadingListener() {
@Override
public void onProgressStateChange(int state, boolean hide) {
//状态改变
}
@Override
public void onProgressLoading() {
//正在loading 加载相关数据!
}
});
相关Demo请移步我的github。。。!