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

啦啦啦,这是山寨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。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容