Android 自定义View UC下拉刷新效果(二)

0.014字数 1515阅读 2597

啦啦啦,这是山寨UC浏览器的下拉刷新效果的第二篇,第一篇请移步Android 自定义View UC下拉刷新效果(一)
我们看图说话:

pull_refresh2.gif

主要工作

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)这些都是支持嵌套滑动滴。。

相关接口方法

NestedScrollingParentNestedScrollingChild这两个接口就是用来实现相关的向下兼容的方法滴。。

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 a NestedScrollingParentHelper as a field and delegate any View or ViewGroup methods to the NestedScrollingParentHelper 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的辅助类来统一处理一些东西。

NestedScrollingParent相关的方法.png
NestedScrollingChild的相关方法.png

然后是不是感觉要哔了狗了,这么多方法要实现?!其实我也是醉醉的,然后打算抄抄别人的就好了!

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。。。

推荐阅读更多精彩内容