SwipeRefreshLayout源码分析+自定义UC头条下拉刷新Demo
2017-07-27 17:37
239 查看
首先来看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 d 114fd yConsumed, 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 offset mSpinnerOffsetEnd = (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
@Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //mTarget:手势拖动的目标View if (mTarget == null) { //将不是头部刷新的View赋给mTarget ensureTarget(); } 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);
@Override protected 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子视图绘制的顺序,则需要重载这个方法.(我试了一下,不重写好像也没问题)
@Override protected int getChildDrawingOrder(int childCount, int i) { if (mCircleViewIndex < 0) { return i; } else if (i == childCount - 1) { // Draw the selected child last return mCircleViewIndex; } else if (i >= mCircleViewIndex) { // Move the children after the selected child earlier one return i + 1; } else { // Keep the children before the selected child the same return i; } }
然后就是触摸事件分发机制;onInterceptTouchEvent():onInterceptTouchEvent是在ViewGroup里面定义的,该方法决定了事件到底交给谁处理 。
当return true时,表示ViewGroup自己来处理onTouchEvent事件,子View接收不到onTouchEvent事件当return false时,表示ViewGroup不拦截事件,直接交给子View处理
onTouchEvent:
onTouchEvent只有当onInterceptTouchEvent返回true的时候才执行。它根据下拉的距离,动态的修改headerView的位置,通过调用setTargetOffsetTopAndBottom调用invalidate()方法进行重绘。@Override public boolean onInterceptTouchEvent(MotionEvent ev) { //一大堆根据当前状态判断是否拦截触摸事件的逻辑 //就是根据是否是最后一个条目或者是第一个条目进行事件拦截 .... }
@Override public 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 circle if (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 alpha startProgressAlphaStartAnimation(); } } else { if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) { // Animate the alpha startProgressAlphaMaxAnimation(); } } 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 refresh mRefreshing = false; mProgress.setStartEndTrim(0f, 0f); Animation.AnimationListener listener = null; if (!mScale) { listener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (!mScale) { startScaleDownAnimation(null); } } @Override public 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成长之路
相关文章推荐
- onMeasure()源码分析及自定义View对于wrap_content的支持
- jsp中自定义标签用法实例分析
- JUnit源码分析 - 扩展 - 自定义Rule
- JS自定义选项卡函数及用法实例分析
- 自定义圆形头像CircleImageView的使用和源码分析
- Android源码解析之新进程中启动自定义服务过程(startService)的原理分析
- Android系统在新进程中启动自定义服务过程(startService)的原理分析
- spring-security认证过程的分析及自定义登录
- 【JDK源码分析】04-使用Externalizable实现自定义序列化
- asp.net mvc源码分析-DefaultModelBinder 自定义的普通数据类型的绑定和验证
- 自定义圆形头像CircleImageView的使用和源码分析
- SOPC中自定义外设和自定义指令性能分析
- Spring Boot Actuator分析,自定义端点
- Android开发实践:自定义ViewGroup的onLayout()分析
- ArcGIS 网络分析[1] 利用自定义点线数据(shp或数据库)创建网络数据集【小白向】
- 自定义滚轮WheelView源码全方位分析
- Android简易实战教程--第二十七话《自定义View入门案例之开关按钮详细分析》
- 微软企业库5.0 学习之路——第七步、Cryptographer加密模块简单分析、自定义加密接口及使用—上篇
- Android自定义View(一)View绘制流程以及invalidate()等相关方法分析
- android-进阶(3)-自定义view(2)-Android中View绘制流程以及相关方法的分析