




已阅读5页,还剩12页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
想要亲手实现一个刷新控件 你只需要掌握这些知识现在Android阵营里面的刷新控件很多,稂莠不齐。笔者试图从不一样的角度,在它的个性化和滚动上下一些功夫。笔者期望,这个刷新控件能像Google的SwipeRefreshLayout一样,支持大多数列表控件,有加载更多功能,最好是要很方便的支持个性化,滚动中能够越界是不是也会带来比普通的刷新控件更好的交互体验。作者:lcodecorex来源:segmentfault|2016-12-13 17:02收藏 分享 现在Android阵营里面的刷新控件很多,稂莠不齐。笔者试图从不一样的角度,在它的个性化和滚动上下一些功夫。笔者期望,这个刷新控件能像Google的SwipeRefreshLayout一样,支持大多数列表控件,有加载更多功能,最好是要很方便的支持个性化,滚动中能够越界是不是也会带来比普通的刷新控件更好的交互体验。开源库在这,TwinklingRefreshLayout,如果喜欢请star,笔者的文章也是围绕着这个控件的实现来说的。为了方便,笔者将TwinklingRefreshLayout直接继承自FrameLayout而不是ViewGroup,可以省去onMeasure、onLayout等一些麻烦,Header和Footer则是通过LayoutParams来设置View的Gravity属性来做的。1. View的onAttachedToWindow()方法首先View没有明显的生命周期,我们又不能再构造函数里面addView()给控件添加头部和底部,因此这个操作比较合适的时机就是在onDraw()之前onAttachedToWindow()方法中。此时View被添加到了窗体上,View有了一个用于显示的Surface,将开始绘制。因此其保证了在onDraw()之前调用,但可能在调用 onDraw(Canvas) 之前的任何时刻,包括调用 onMeasure(int, int) 之前或之后。比较适合去执行一些初始化操作。(此外在屏蔽Home键的时候也会回调这个方法) onDetachedFromWindow()与onAttachedToWindow()方法相对应。 ViewGroup先是调用自己的onAttachedToWindow()方法,再调用其每个child的onAttachedToWindow()方法,这样此方法就在整个view树中遍布开了,而visibility并不会对这个方法产生影响。 onAttachedToWindow方法是在Activity resume的时候被调用的,也就是act对应的window被添加的时候,且每个view只会被调用一次,父view的调用在前,不论view的visibility状态都会被调用,适合做些view特定的初始化操作; onDetachedFromWindow方法是在Activity destroy的时候被调用的,也就是act对应的window被删除的时候,且每个view只会被调用一次,父view的调用在后,也不论view的visibility状态都会被调用,适合做最后的清理操作;就TwinklingRefreshLayout来说,Header和Footer需要及时显示出来,View又没有明显的生命周期,因此在onAttachedToWindow()中进行设置可以保证在onDraw()之前添加了刷新控件。1. Override2. protectedvoidonAttachedToWindow()3. super.onAttachedToWindow();4. 5. /添加头部6. FrameLayoutheadViewLayout=newFrameLayout(getContext();7. LayoutParamslayoutParams=newLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,0);8. layoutParams.gravity=Gravity.TOP;9. headViewLayout.setLayoutParams(layoutParams);10. 11. mHeadLayout=headViewLayout;12. this.addView(mHeadLayout);/addView(view,-1)添加到-1的位置13. 14. /添加底部15. FrameLayoutbottomViewLayout=newFrameLayout(getContext();16. LayoutParamslayoutParams2=newLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,0);17. layoutParams2.gravity=Gravity.BOTTOM;18. bottomViewLayout.setLayoutParams(layoutParams2);19. 20. mBottomLayout=bottomViewLayout;21. this.addView(mBottomLayout);22. /.其它步骤23. 但是当TwinklingRefreshLayout应用在Activity或Fragment中时,可能会因为执行onResume重新触发了onAttachedToWindow()方法而导致重复创建Header和Footer挡住原先添加的View,因此需要加上判断:1. Override2. protectedvoidonAttachedToWindow()3. super.onAttachedToWindow();4. System.out.println(onAttachedToWindow绑定窗口);5. 6. /添加头部7. if(mHeadLayout=null)8. FrameLayoutheadViewLayout=newFrameLayout(getContext();9. LayoutParamslayoutParams=newLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,0);10. layoutParams.gravity=Gravity.TOP;11. headViewLayout.setLayoutParams(layoutParams);12. 13. mHeadLayout=headViewLayout;14. 15. this.addView(mHeadLayout);/addView(view,-1)添加到-1的位置16. 17. if(mHeadView=null)setHeaderView(newRoundDotView(getContext();18. 19. /.20. 2. View的事件分发机制事件的分发过程由dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三个方法来共同完成的。由于事件的传递是自顶向下的,对于ViewGroup,笔者觉得最重要的就是onInterceptTouchEvent方法了,它关系到事件是否能够继续向下传递。看如下伪代码:1. publicbooleandispatchTouchEvent(MotionEvenetev)2. booleanconsume=false;3. if(onInterceptTouchEvent(ev)4. consume=onTouchEvent(ev);5. else6. consume=child.dispatchTouchEvent(ev);7. 8. returnconsume;9. 如代码所示,如果ViewGroup拦截了(onInterceptTouchEvent返回true)事件,则事件会在ViewGroup的onTouchEvent方法中消费,而不会传到子View;否则事件将交给子View去分发。我们需要做的就是在子View滚动到顶部或者底部时及时的拦截事件,让ViewGroup的onTouchEvent来交接处理滑动事件。3. 判断子View滚动达到边界在什么时候对事件进行拦截呢?对于Header,当手指向下滑动也就是 dy0 且子View已经滚动到顶部(不能再向上滚动)时拦截;对于bottom则是 dy0&!canChildScrollUp()11. state=PULL_DOWN_REFRESH;12. returntrue;13. elseif(dy= mHeadHeight - mTouchSlop,mTouchSlop是为了防止发生抖动而存在。判断进入了刷新状态时,当前子View的位移在HeadHeight和maxHeadHeight之间,所以需要让子View的位移回到HeadHeight处,否则就直接回到0处。5. Interpolator插值器Interpolator用于动画中的时间插值,其作用就是把0到1的浮点值变化映射到另一个浮点值变化。上面提到的计算方式如下:1. floatoffsetY=decelerateInterpolator.getInterpolation(dy/mWaveHeight/2)*dy/2;其中(dy / mWaveHeight / 2)是一个01之间的浮点值,随着下拉高度的增加,这个值越来越大,通过decelerateInterpolator获取到的插值也越来越大,只不过这些值的变化量是越来越小(decelerate效果)。dy表示的是手指移动的距离。这只是笔者为了滑动的柔和性使用的一种计算方式,头部位移的最大距离是mWaveHeight = dy/2,这样看的话可以发现 dy / mWaveHeight / 2 会从0到1变化。Interpolator继承自TimeInterpolator接口,源码如下:1. publicinterfaceTimeInterpolator2. /*3. *Mapsavaluerepresentingtheelapsedfractionofananimationtoavaluethatrepresents4. *theinterpolatedfraction.Thisinterpolatedvalueisthenmultipliedbythechangein5. *valueofananimationtoderivetheanimatedvalueatthecurrentelapsedanimationtime.6. *7. *paraminputAvaluebetween0and1.0indicatingourcurrentpoint8. *intheanimationwhere0representsthestartand1.0represents9. *theend10. *returnTheinterpolationvalue.Thisvaluecanbemorethan1.0for11. *interpolatorswhichovershoottheirtargets,orlessthan0for12. *interpolatorsthatundershoottheirtargets.13. */14. floatgetInterpolation(floatinput);15. getInterpolation接收一个0.01.0之间的float参数,0.0代表动画的开始,1.0代表动画的结束。返回值则可以超过1.0,也可以小于0.0,比如OvershotInterpolator。所以getInterpolation()是用来实现输入01返回01左右的函数值的一个函数。6. 属性动画上面说到了手指抬起的时候,mChildView的位移要么回到mHeadHeight处,要么回到0处。直接setTranslationY()不免太不友好,所以我们这里使用属性动画来做。本来是直接可以用mChildView.animate()方法来完成属性动画的,因为需要兼容低版本并回调一些参数,所以这里使用ObjectAnimator:1. privatevoidanimChildView(floatendValue,longduration)2. ObjectAnimatoroa=ObjectAnimator.ofFloat(mChildView,translationY,mChildView.getTranslationY(),endValue);3. oa.setDuration(duration);4. oa.setInterpolator(newDecelerateInterpolator();/设置速率为递减5. oa.addUpdateListener(newValueAnimator.AnimatorUpdateListener()6. Override7. publicvoidonAnimationUpdate(ValueAnimatoranimation)8. intheight=(int)mChildView.getTranslationY();/获得mChildView当前y的位置9. height=Math.abs(height);10. 11. mHeadLayout.getLayoutParams().height=height;12. mHeadLayout.requestLayout();13. 14. );15. oa.start();16. 传统的补间动画只能够实现移动、缩放、旋转和淡入淡出这四种动画操作,而且它只是改变了View的显示效果,改变了画布绘制出来的样子,而不会真正去改变View的属性。比如用补间动画对一个按钮进行了移动,只有在原位置点击按钮才会发生响应,而属性动画则可以真正的移动按钮。属性动画最简单的一种使用方式就是使用ValueAnimator:1. ValueAnimatoranim=ValueAnimator.ofFloat(0f,1f);2. anim.start();它可以传入多个参数,如ValueAnimator.ofFloat(0f, 5f, 3f, 10f),他会根据设置的插值器依次计算,比如想做一个心跳的效果,用ValueAnimator来控制心的当前缩放值大小就是个不错的选择。除此之外,还可以调用setStartDelay()方法来设置动画延迟播放的时间,调用setRepeatCount()和setRepeatMode()方法来设置动画循环播放的次数以及循环播放的模式等。如果想要实现View的位移,ValueAnimator显然是比较麻烦的,我们可以使用ValueAnimator的子类ObjectAnimator,如下:1. ObjectAnimatoranimator=ObjectAnimator.ofFloat(textview,alpha,1f,0f,1f);2. animator.setDuration(5000);3. animator.start();传入的第一个值是Object,不局限于View,传入的第二个参数为Object的一个属性,比如传入abc,ObjectAnimator会去Object里面找有没有 getAbc() 和 setAbc(.) 这两个方法,如果没有,动画就没有效果,它内部应该是处理了相应的异常。另外还可以用AnimatorSet来实现多个属性动画同时播放,也可以在xml中写属性动画。7. 个性化Header和Footer的接口要实现个性化的Header和Footer,最最重要的当然是把滑动过程中系数都回调出来啦。在ACTION_MOVE的时候,在ACTION_UP的时候,还有在mChildView在执行属性动画的时候,而且mChildView当前所处的状态都是很明确的,写个接口就好了。1. publicinterfaceIHeaderView2. ViewgetView();3. 4. voidonPullingDown(floatfraction,floatmaxHeadHeight,floatheadHeight);5. 6. voidonPullReleasing(floatfraction,floatmaxHeadHeight,floatheadHeight);7. 8. voidstartAnim(floatmaxHeadHeight,floatheadHeight);9. 10. voidonFinish();11. getView()方法保证在TwinklingRefreshLayout中可以取到在外部设置的View,onPullingDown()是下拉过程中ACTION_MOVE时的回调方法,onPullReleasing()是下拉状态中ACTION_UP时的回调方法,startAnim()则是正在刷新时回调的方法。其中fraction=mChildView.getTranslationY()/mHeadHeight,fraction=1 时,mChildView的位移恰好是HeadLayout的高度,fraction1 时则超过了HeadLayout的高度,其最大高度可以到达 mWaveHeight/mHeadHeight。这样我们只需要写一个View来实现这个接口就可以实现个性化了,该有的参数都有了!8. 实现越界回弹不能在手指快速滚动到顶部时对越界做出反馈,这是一个继承及ViewGroup的刷新控件的通病。没有继承自具体的列表控件,它没办法获取到列表控件的Scroller,不能获取到列表控件的当前滚动速度,更是不能预知列表控件什么时候能滚动到顶部;同时ViewGroup除了达到临界状态的事件被拦截了,其它事件全都交给了子View去处理。我们能获取到的有关于子View的操作,只有简简单单的手指的触摸事件。so, lets do it!1. mChildView.setOnTouchListener(newOnTouchListener()2. Override3. publicbooleanonTouch(Viewv,MotionEventevent)4. returngestureDetector.onTouchEvent(event);5. 6. );我们把在mChildView上的触摸事件交给了一个工具类GestureDetector去处理,它可以辅助检测用户的单击、滑动、长按、双击、快速滑动等行为。我们这里只需要重写onFling()方法并获取到手指在Y方向上的速度velocityY,要是再能及时的发现mChildView滚动到了顶部就可以解决问题了。1. GestureDetectorgestureDetector=newGestureDetector(getContext(),newGestureDetector.SimpleOnGestureListener()2. 3. Override4. publicbooleanonFling(MotionEvente1,MotionEvente2,floatvelocityX,floatvelocityY)5. mVelocityY=velocityY;6. 7. );此外获取速度还可以用VelocityTracker,比较麻烦一些:1. VelocityTrackertracker=VelocityTracker.obtain();2. tracker.addMovement(ev);3. /然后在恰当的位置使用如下方法获取速度4. puteCurrentVelocity(1000);5. mVelocityY=(int)tracker.getYVelocity();继续来实现越界回弹。对于RecyclerView、AbsListView,它们提供有OnScrollListener可以获取一下滚动状态:1. if(mChildViewinstanceofRecyclerView)2. (RecyclerView)mChildView).addOnScrollListener(newRecyclerView.OnScrollListener()3. Override4. publicvoidonScrollStateChanged(RecyclerViewrecyclerView,intnewState)5. if(!isRefreshing&!isLoadingmore&newState=RecyclerView.SCROLL_STATE_IDLE)6. if(mVelocityY=5000&ScrollingUtil.isRecyclerViewToTop(RecyclerView)mChildView)7. animOverScrollTop();8. 9. if(mVelocityY=5000)7. mHandler.sendEmptyMessage(MSG_START_COMPUTE_SCROLL);8. else9. cur_delay_times=ALL_DELAY_TIMES;10. 11. 12. returnfalse;13. 在滚动速度大于5000的时候发送一个重新计算的消息,Handler收到消息后,延时一段时间继续给自己发送消息,直到时间用完或者mChildView滚动到顶部或者用户又进行了一次Fling动作。1. privateHandlermHandler=newHandler()2. Override3. publicvoidhandleMessage(Messagemsg)4. switch(msg.what)5. caseMSG_START_COMPUTE_SCROLL:6. cur_delay_times=-1;/这里没有break,写作-1方便计数7. caseMSG_CONTINUE_COMPUTE_SCROLL:8. cur_delay_times+;9. 10. if(!isRefreshing&!isLoadingmore&mVelocityY=5000&childScrollToTop()11. animOverScrollTop();12. cur_delay_times=ALL_DELAY_TIMES;13. 14. 15. if(!isRefreshing&!isLoadingmore&mVelocityY=-5000&childScrollToBottom()16. animOverScrollBottom();17. cur_delay_times=ALL_DELAY_TIMES;18. 19. 20. if(cur_delay_timesALL_DELAY_TIMES)21. mHandler.sendEmptyMessageDelayed(MSG_CONTINUE_COMPUTE_SCROLL,10);22. break;23. caseMSG_STOP_COMPUTE_SCROLL:24. cur_delay_times=ALL_DELAY_TIMES;25. break;26. 27. 28. ;ALL_DELAY_TIMES是最多可以计算的次数,当Handler接收到MSG_START_COMPUTE_SCROLL消息时,如果mChildView没有滚动到边界处,则会在10ms之后向自己发送一条MSG_CONTINUE_COMPUTE_SCROLL的消息,然后继续进行判断。然后在合适的时候越界回弹就好了。10. 实现个性化Header这里笔者来演示一下,怎么轻轻松松的做一个个性化的Header,比如新浪微博样式的刷新Header(如下面第1图)。1. 创建 SinaRefreshView 继承自 FrameLayout 并实现 IHeaderView 接口2. getView()方法中返回this3. 在onAttachedToWindow()方法中获取一下需要用到的布局(笔者写到了xml中,也可以直接在代码里面写)1. Override2. protectedvoidonAttachedToWindow()3. super.onAttachedToWindow();4. 5. if(rootView=null)6. rootView=View.inflate(getContext(),R.layout.view_sinaheader,null);7. refreshArrow=(ImageView)rootView.findViewById(R.id.iv_arrow);8. refreshTextView=(TextView)rootView.findViewById(R.id.tv);9. loadingView=(ImageView)rootView.findViewById(R.id.iv_loading);10. addView(rootView);11. 12. 4. 实现其它方法1. Override2. publicvoidonPullingDown(floatfraction,floatmaxHeadHeight,floathea
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 导游服务礼仪培训
- 课件模板治愈系文案
- 硬件设计培训课件
- 非凡中国:成就与展望
- 遗憾的过客课件
- 儿童行为密码解析课件大纲
- 美术连环漫画课件
- 课件最短路径
- 课件最后照片
- 广东婚姻法自考试题及答案
- 营造清朗空间+课件-2025-2026学年(统编版2024)道德与法治八年级上册
- saas货运管理办法
- excel操作考试题及答案
- 2025新疆生产建设兵团草湖项目区公安局面向社会招聘警务辅助人员考试参考试题及答案解析
- 喷雾干燥课件
- 《网页设计与制作Dreamweaver-cs6》教学课件(全)
- DBJ51T 196-2022 四川省智慧工地建设技术标准
- 审核检查表(ISO13485、GMP、体考指南、QSR820)
- 宿舍教室报修维修登记表
- 剪映入门教程PPT
- 律师事务所合同纠纷法律诉讼服务方案
评论
0/150
提交评论