首先来看SwipeRefreshLayout(以下简称SR)的继承关系

NestedScrollingParent:嵌套滑动父接口

NestedScrollingChild :嵌套滑动子接口

Android 就是通过这两个接口, 来实现 子View 与父View 之间的嵌套滑动

  • NestedScrollingChild:源码
public interface NestedScrollingChild {/*** Enable or disable nested scrolling for this view* 为这个视图启用或禁用嵌套滚动*/public void setNestedScrollingEnabled(boolean enabled);/*** Returns true if nested scrolling is enabled for this view.* 若启动嵌套滑动,则返回True*/public boolean isNestedScrollingEnabled();/*** Begin a nestable scroll operation along the given axes.* 在给定的轴上开始一个新的滚动操作。* ViewCompat.SCROLL_AXIS_HORIZONTAL 横向* ViewCompat.SCROLL_AXIS_VERTICAL 纵向*/public boolean startNestedScroll(int axes);/*** Stop a nested scroll in progress.* 停止嵌套的滚动*/public void stopNestedScroll();/*** Returns true if this view has a nested scrolling parent.* 如果该视图有一个嵌套滚动的父视图,则返回true。*/public boolean hasNestedScrollingParent();/*** Dispatch one step of a nested scroll in progress.** 在处理滑动之后 调用 * @param dxConsumed x轴上 被消费的距离 * @param dyConsumed y轴上 被消费的距离 * @param dxUnconsumed x轴上 未被消费的距离 * @param dyUnconsumed y轴上 未被消费的距离 * @param offsetInWindow view 的移动距离* 如果事件被发送,则返回true,如果该事件不能被发送,则为false*/public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);/*** Dispatch one step of a nested scroll in progress before this view consumes any portion of it.**一般在滑动之前调用, 在ontouch 中计算出滑动距离, 然后调用该方法, 就给支持的嵌套的父View 处理滑动事件 * @param dx x 轴上滑动的距离, 相对于上一次事件, 不是相对于 down事件的 那个距离 * @param dy y 轴上滑动的距离 * @param consumed 一个数组, 可以传 一个空的 数组,  表示 x 方向 或 y 方向的事件 是否有被消费 * @param offsetInWindow   支持嵌套滑动到额父View 消费 滑动事件后 导致 本 View 的移动距离 * @return 支持的嵌套的父View 是否处理了 滑动事件*/public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);/*** Dispatch a fling to a nested scrolling parent.* @param velocityX x 轴上的滑动速度 * @param velocityY y 轴上的滑动速度 * @param consumed 是否被消费 * @return */public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);/*** Dispatch a fling to a nested scrolling parent before it is processed by this view.**@param velocityX x 轴上的滑动速度 * @param velocityY y 轴上的滑动速度 * @return* @param velocityX Horizontal fling velocity in pixels per second* @param velocityY Vertical fling velocity in pixels per second* @return true if a nested scrolling parent consumed the fling*/public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
  • NestedScrollingParent源码:

public interface NestedScrollingParent {/*** React to a descendant view initiating a nestable scroll operation, claiming thenested scroll operation if appropriate.* 对嵌套滚动的子View进行响应* ** @param child ViewParent包含触发嵌套滚动的view的对象* @param target触发嵌套滚动的view(在这里如果不涉及多层嵌套的话,child和ta   rget)是相同的* @param nestedScrollAxes 方向  ViewCompat.SCROLL_AXIS_HORIZONTAL*                         ViewCompat.SCROLL_AXIS_VERTICAL* @return true 如果ViewParent接受嵌套滚动操作,则返回true*/public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);/*** React to the successful claiming of a nested scroll operation.* 对成功的使用嵌套滚动操作作出反应* @param child ViewParent包含触发嵌套滚动的view的对象* @param target触发嵌套滚动的view(在这里如果不涉及多层嵌套的话,child和ta   rget)是相同的* @param nestedScrollAxes 滑动的方向          ViewCompat#SCROLL_AXIS_HORIZONTAL},*  ViewCompat#SCROLL_AXIS_VERTICAL*/public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);/*** React to a nested scroll operation ending.* 对一个嵌套滚动操作的结果进行响应* @param target 启动滚动的View*/public void onStopNestedScroll(View target);/*** React to a nested scroll in progress.* 对正在进行的嵌套滚动进行响应* @param target 控制滚动的子View* @param dxConsumed x轴消费的距离* @param dyConsumed y轴消费的距离* @param dxUnconsumed x轴未消费的距离* @param dyUnconsumed y轴未消费的距离*/public void onNestedScroll(View target, int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed);/*** React to a nested scroll in progress before the target view consumes a portion of the scroll.** @param target 控制滚动的子View* @param dx x轴消费总距离* @param dy y轴消费总距离* @param consumed Output. 父布局分别在x,y轴消费的总距离:consumed[0],consumed[1]*/public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);/*** Request a fling from a nested scroll.* 嵌套滑动的速度* @param target 控制滚动的子View* @param velocityX velocityX x 轴上的滑动速度 * @param velocityY y 轴上的滑动速度* @param consumed 子view是否消费* @return true if this parent consumed or otherwise reacted to the fling*/public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);/*** React to a nested fling before the target view consumes it.** @param target 控制滚动的子View* @param velocityX x 轴上的滑动速度 * @param velocityY y 轴上的滑动速度* @return 如果父布局在这之前消费了该事件则返回True*/public boolean onNestedPreFling(View target, float velocityX, float velocityY);/*** Return the current axes of nested scrolling for this NestedScrollingParent.返回一个当前滑动轴,以下3种情况* @return Flags indicating the current axes of nested scrolling* @see ViewCompat#SCROLL_AXIS_HORIZONTAL* @see ViewCompat#SCROLL_AXIS_VERTICAL* @see ViewCompat#SCROLL_AXIS_NONE*/public int getNestedScrollAxes();
}

这两个接口的作用在上面的注释中有详细的解释,下面就是最关键的SR源码的分析;因为SR继承的是ViewGroup,我们平常都会自定义View,而自定义View通常都少不了:onMeasure(测量),onDraw(绘画);而自定义ViewGroup会涉及到对子View的排版问题,所以在自定义ViewGroup中多了一个onLayout()方法需要我们处理,这些基本的问题解决后,若自定义控件涉及到触摸事件,也会需要我们对触摸事件的分发机制有一定的了解;然后就让我们根据SR源码来一步一步分析下拉刷新控件是怎样实现的!(对源码的分析都是以代码的注释的形式来进行的)

  • SR构造:
public SwipeRefreshLayout(Context context, AttributeSet attrs) {super(context, attrs);//ViewConfiguration定义UI中用于超时、大小和距离的标准常量和获取他们的值的方法mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();//获取动画时间mMediumAnimationDuration = getResources().getInteger(android.R.integer.config_mediumAnimTime);//若没有任何绘图,则设置此方法(没有重写onDrow方法)setWillNotDraw(false);//设置减速插值器mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);// 描述一个显示的一般信息的结构,例如它的大小、密度和字体大小。final DisplayMetrics metrics = getResources().getDisplayMetrics();mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density);//创建头部刷新控件createProgressView();//告诉ViewGroup是否按照该方法定义的顺序绘制它的孩子ViewCompat.setChildrenDrawingOrderEnabled(this, true);// the absolute offset has to take into account that the circle starts at an offsetmSpinnerOffsetEnd = (int) (DEFAULT_CIRCLE_TARGET * metrics.density);mTotalDragDistance = mSpinnerOffsetEnd;mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);setNestedScrollingEnabled(true);mOriginalOffsetTop = mCurrentTargetOffsetTop = -mCircleDiameter;//头部刷新控件起始位置moveToStart(1.0f);final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);setEnabled(a.getBoolean(0, true));a.recycle();}

在构造方法中主要做了一下几件事情:

  • 对一些常量(列如动画时间,圆的直径,圆的偏移量等)的设置
  • 将一个头部刷新控件加入进来
  • 创建mNestedScrollingParentHelper,mNestedScrollingChildHelper等对象,为这个视图启用嵌套滚动

onMeasure 方法:三件事

  • 找出目标View
  • 测量子控件的大小
  • 得到下拉刷新View的Index
 @Overridepublic void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);//mTarget:手势拖动的目标Viewif (mTarget == null) {//将不是头部刷新的View赋给mTargetensureTarget();}if (mTarget == null) {return;}//根据测量规格测出目标View的大小mTarget.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));//同上mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY),MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY));mCircleViewIndex = -1;// Get the index of the circleview.for (int index = 0; index < getChildCount(); index++) {if (getChildAt(index) == mCircleView) {mCircleViewIndex = index;break;}}}

在得到各个子控件的大小后,就是对各个控件的排版问题,也就是 onLayout()方法

  • 确定目标View的位置:child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
  • 确定刷新控件的位置:mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
    (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
 @Overrideprotected void onLayout(boolean changed, int left, int top, int right, int bottom) {final int width = getMeasuredWidth();final int height = getMeasuredHeight();if (getChildCount() == 0) {return;}if (mTarget == null) {ensureTarget();}if (mTarget == null) {return;}final View child = mTarget;final int childLeft = getPaddingLeft();final int childTop = getPaddingTop();final int childWidth = width - getPaddingLeft() - getPaddingRight();final int childHeight = height - getPaddingTop() - getPaddingBottom();child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);int circleWidth = mCircleView.getMeasuredWidth();int circleHeight = mCircleView.getMeasuredHeight();mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,(width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);}

因为下拉刷新控件是在一开始的时候是不显示的,所以就要考虑各个子控件的绘制顺序,将下拉刷新控件放在最后绘制,getChildDrawingOrder用于返回当前迭代子视图的索引.就是说获取当前正在绘制的视图索引. 如果需要改变ViewGroup子视图绘制的顺序,则需要重载这个方法.(我试了一下,不重写好像也没问题)

 @Overrideprotected int getChildDrawingOrder(int childCount, int i) {if (mCircleViewIndex < 0) {return i;} else if (i == childCount - 1) {// Draw the selected child lastreturn mCircleViewIndex;} else if (i >= mCircleViewIndex) {// Move the children after the selected child earlier onereturn i + 1;} else {// Keep the children before the selected child the samereturn i;}}

然后就是触摸事件分发机制;onInterceptTouchEvent():onInterceptTouchEvent是在ViewGroup里面定义的,该方法决定了事件到底交给谁处理 。

  • 当return true时,表示ViewGroup自己来处理onTouchEvent事件,子View接收不到onTouchEvent事件
  • 当return false时,表示ViewGroup不拦截事件,直接交给子View处理

onTouchEvent:

  • onTouchEvent只有当onInterceptTouchEvent返回true的时候才执行。它根据下拉的距离,动态的修改headerView的位置,通过调用setTargetOffsetTopAndBottom调用invalidate()方法进行重绘。
 @Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {//一大堆根据当前状态判断是否拦截触摸事件的逻辑//就是根据是否是最后一个条目或者是第一个条目进行事件拦截....}
@Overridepublic boolean onTouchEvent(MotionEvent ev) {...case MotionEvent.ACTION_MOVE: {pointerIndex = ev.findPointerIndex(mActivePointerId);if (pointerIndex < 0) {Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");return false;}final float y = ev.getY(pointerIndex);startDragging(y);if (mIsBeingDragged) {final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;if (overscrollTop > 0) {moveSpinner(overscrollTop);} else {return false;}}break;}...  case MotionEvent.ACTION_UP: {pointerIndex = ev.findPointerIndex(mActivePointerId);if (pointerIndex < 0) {Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");return false;}if (mIsBeingDragged) {final float y = ev.getY(pointerIndex);final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;mIsBeingDragged = false;finishSpinner(overscrollTop);}mActivePointerId = INVALID_POINTER;return false;} }

onTouchEvent方法中最重要的便是moveSpinner(overscrollTop)和finishSpinner(overscrollTop)方法的调用

  • 获取拖拽百分比和高度差并修正
  • 开启动画
  • 动态修正下拉刷新控件的位置
  • 设置监听

  • moveSpinner

 @SuppressLint("NewApi")private void moveSpinner(float overscrollTop) {mProgress.showArrow(true);//原始拖动距离百分比float originalDragPercent = overscrollTop / mTotalDragDistance;//原谅我的数学太差,我不知道我为什么用下面的公式计算下拉偏移量,float dragPercent = Math.min(1f, Math.abs(originalDragPercent));float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3;float extraOS = Math.abs(overscrollTop) - mTotalDragDistance;float slingshotDist = mUsingCustomStart ? mSpinnerOffsetEnd - mOriginalOffsetTop: mSpinnerOffsetEnd;float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2)/ slingshotDist);float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow((tensionSlingshotPercent / 4), 2)) * 2f;float extraMove = (slingshotDist) * tensionPercent * 2;int targetY = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove);// where 1.0f is a full circleif (mCircleView.getVisibility() != View.VISIBLE) {mCircleView.setVisibility(View.VISIBLE);}if (!mScale) {ViewCompat.setScaleX(mCircleView, 1f);ViewCompat.setScaleY(mCircleView, 1f);}if (mScale) {setAnimationProgress(Math.min(1f, overscrollTop / mTotalDragDistance));}if (overscrollTop < mTotalDragDistance) {if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA&& !isAnimationRunning(mAlphaStartAnimation)) {// Animate the alphastartProgressAlphaStartAnimation();}} else {if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) {// Animate the alphastartProgressAlphaMaxAnimation();}}float strokeStart = adjustedPercent * .8f;mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart));mProgress.setArrowScale(Math.min(1f, adjustedPercent));float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f;mProgress.setProgressRotation(rotation);setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);}
  • finishSpinner
private void finishSpinner(float overscrollTop) {if (overscrollTop > mTotalDragDistance) {//下拉刷新状态的设置setRefreshing(true, true /* notify */);} else {// cancel refreshmRefreshing = false;mProgress.setStartEndTrim(0f, 0f);Animation.AnimationListener listener = null;if (!mScale) {listener = new Animation.AnimationListener() {@Overridepublic void onAnimationStart(Animation animation) {}@Overridepublic void onAnimationEnd(Animation animation) {if (!mScale) {startScaleDownAnimation(null);}}@Overridepublic void onAnimationRepeat(Animation animation) {}};}//这个方法是进行下拉刷新的回复在ANIMATE_TO_START_DURATION=200毫秒内animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);mProgress.showArrow(false);}}

总结

到此我们的分析基本结束了,让我们一起来看看做了多少事情才写出一个下拉刷新控件

  • addView() 加入下拉刷新控件
  • 测量各个子view的大小
  • onLayout()对子view的位置进行确定以及确定各子view的绘制顺序
  • 触摸事件的分发机制
  • 嵌套滑动实现
  • 设置回调接口

下拉刷新控件的机制我们了解的差不多了,下面就是我们定制自己的下拉刷新,上拉加载控件了——仿UC头条下拉刷新布局

ps:上面也说了楼主数学太差,那个圆的角度变化和下拉距离偏移量的关系式对楼主来说太难了,所以就搞了个假的!不多说了,来看下效果图:

这只是加深对自定义ViewGroup的理解而做的一个小Demo,下面是代码地址

Github>>>,大家随便看看就行,推荐一个很酷炫的下拉刷新第三方,楼主就是看了这位大神写的下拉刷新控件才想看看原理是怎样的!
酷炫的下拉刷新上拉加载控件》》》

  • 若有错误,敬请指正!!!

拼搏在技术道路上的一只小白And成长之路

SwipeRefreshLayout源码分析+自定义UC头条下拉刷新Demo相关推荐

  1. 探索SwipeRefreshLayout配合自定义ListView完成下拉刷新、滑到底部自动加载更多

    在Android开发过程中经常需要实现上下拉刷新功能,Google推出的下拉刷新控件SwipeRefreshLayout(彩虹条),由于官方版本只有下拉刷新而没有上拉加载更多的功能,很多人也尝试在这个 ...

  2. 修改源码自定义SwipeRefreshLayout样式——高仿微信朋友圈下拉刷新

    上一篇文章里把SwipeRefreshLayout的原理简单过了一下,大致了解了其工作原理,不熟悉的可以去看一下:http://blog.csdn.net/u011443509/article/det ...

  3. [Vue源码分析]自定义事件原理及事件总线的实现

    最近小组有个关于vue源码分析的分享会,提前准备一下- 前言: 我们都知道Vue中父组件可以通过 props 向下传数据给子组件:子组件可以通过向$emit触发一个事件,在父组件中执行回调函数,从而实 ...

  4. Android -- ViewGroup源码分析+自定义

    1,我们前三篇博客了解了一下自定义View的基本方法和流程 从源码的角度一步步打造自己的TextView 深入了解自定义属性 onMeasure()源码分析 之前,我们只是学习过自定义View,其实自 ...

  5. android SwipeRefreshLayout 源码分析之 弹力计算分析。

    打开源码发现 swipe 继承viewgruop ,通过NestedScrollingParent  和NestedScrollingChild辅助类来帮助头部的 下拉刷新滑动, 关于这个网上很多资料 ...

  6. 使用SwipeRefreshLayout和RecyclerView实现仿“简书”下拉刷新和上拉加载更多

    原文地址: http://blog.csdn.net/leoleohan/article/details/50989549/ 一.概述 我们公司目前开发的所有Android APP都是遵循iOS风格设 ...

  7. html仿今日头条下拉刷新,小程序 仿今日头条 带滑动切换的文章列表

    小程序 仿今日头条 带滑动切换的文章列表 发布时间:2018-07-19 09:41, 浏览次数:353 拿别人仿今日头条的代码做的改版, 首先感谢前辈.其次,这个代码虽然能用,但是js里还是存在一些 ...

  8. 微信小程序自定义navigationbar与下拉刷新思考

    第一次开发小程序,产品提出要求导航栏字体样式,然后系统的未提供修改的接口. 那么只能自定义导航栏才行呀. 迅速的自定义了一个导航栏 app.json中添加 "navigationStyle& ...

  9. Android自定义View实现下拉刷新控件

    路过的老铁同志可以微信搜索"Android小菜",不定期更新Android技术文章.比CSDN更快一步阅读. 本文实现的功能如下: 1.支持下拉刷新: 2.支持上拉加载更多 3.刷 ...

最新文章

  1. shell expect的简单用法
  2. 青龙羊毛——狸猫十堰
  3. Java 中的字符串(String)与C# 中字符串(string)的异同
  4. 图形基础 GPU架构(5)GPU vs CPU
  5. SSO单点登录之跨域问题
  6. java selenium 日志_java - 支持selenium日志_java_酷徒编程知识库
  7. python国际象棋ai程序_用Python编写一个国际象棋AI程序
  8. Linux操作寄存器前为什么要ioremap
  9. redis高级-------2
  10. 更改应用程序图标_在 Windows 10 version 1903 中查看应用程序是否支持 DPI 感知
  11. Java二十三设计模式之------迭代子模式
  12. [vb] Set 语句
  13. 黑客可利用超声波秘密控制语音助手设备
  14. Ubuntu下Hadoop的安装和配置
  15. HTML在手机上能编写吗,手机版使用开发
  16. 基于onvif协议的嵌入式设备(摄像头)开发(客户端)
  17. ubuntu2004使用Renesas upd720202 扩展卡
  18. 2021年全球及中国酒店行业发展现状及竞争格局分析,全球酒店行业景气度大幅回暖「图」
  19. matlab计算六面体的体积,六面体单元体积坐标方法-工程力学-清华大学.PDF
  20. JDK下载应该选择哪个版本?教你选择最好的JDK版本

热门文章

  1. 可调稳压电源lm317实验报告_LM317可调稳压电源制作报告
  2. volatility3安装报错
  3. 打印冻结窗格怎么保证每页都有_《excel如何打印标题行》 EXCEL 如何让冻结的窗口 在打印的每页上面显示...
  4. PHP_MINIT_FUNCTION
  5. 计算机视觉的上游任务和下游任务
  6. stame进去显示服务器,steam显示更新服务器
  7. 北京2018年计算机技校,2020年北京的技校有哪些
  8. 无线网卡加密方式wep wpa/wpa2 介绍
  9. 反编译apk修改v7包_微信Android SDK反编译还原源码 进行修改重新编译
  10. 外汇交易策略如何制定