亲手实现一个刷新控件_第1页
亲手实现一个刷新控件_第2页
亲手实现一个刷新控件_第3页
亲手实现一个刷新控件_第4页
亲手实现一个刷新控件_第5页
已阅读5页,还剩12页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、想要亲手实现一个刷新控件 你只需要掌握这些知识现在Android阵营里面的刷新控件很多,稂莠不齐。笔者试图从不一样的角度,在它的个性化和滚动上下一些功夫。笔者期望,这个刷新控件能像Google的SwipeRefreshLayout一样,支持大多数列表控件,有加载更多功能,最好是要很方便的支持个性化,滚动中能够越界是不是也会带来比普通的刷新控件更好的交互体验。作者:lcodecorex来源:segmentfault|2016-12-13 17:02 收藏   分享 现在Android阵营里面的刷新控件很多,稂莠不齐。笔者试图从不一样的角度,在它的个性化和滚动上下一些

2、功夫。笔者期望,这个刷新控件能像Google的SwipeRefreshLayout一样,支持大多数列表控件,有加载更多功能,最好是要很方便的支持个性化,滚动中能够越界是不是也会带来比普通的刷新控件更好的交互体验。开源库在这,TwinklingRefreshLayout,如果喜欢请star,笔者的文章也是围绕着这个控件的实现来说的。为了方便,笔者将TwinklingRefreshLayout直接继承自FrameLayout而不是ViewGroup,可以省去onMeasure、onLayout等一些麻烦,Header和Footer则是通过LayoutParams来设置View的Gravity属性来

3、做的。1. View的onAttachedToWindow()方法首先View没有明显的生命周期,我们又不能再构造函数里面addView()给控件添加头部和底部,因此这个操作比较合适的时机就是在onDraw()之前onAttachedToWindow()方法中。此时View被添加到了窗体上,View有了一个用于显示的Surface,将开始绘制。因此其保证了在onDraw()之前调用,但可能在调用 onDraw(Canvas) 之前的任何时刻,包括调用 onMeasure(int, int) 之前或之后。比较适合去执行一些初始化操作。(此外在屏蔽Home键的时候也会回调这个方法)· o

4、nDetachedFromWindow()与onAttachedToWindow()方法相对应。· ViewGroup先是调用自己的onAttachedToWindow()方法,再调用其每个child的onAttachedToWindow()方法,这样此方法就在整个view树中遍布开了,而visibility并不会对这个方法产生影响。· onAttachedToWindow方法是在Activity resume的时候被调用的,也就是act对应的window被添加的时候,且每个view只会被调用一次,父view的调用在前,不论view的visibility状态都会被调用,适合

5、做些view特定的初始化操作;· onDetachedFromWindow方法是在Activity destroy的时候被调用的,也就是act对应的window被删除的时候,且每个view只会被调用一次,父view的调用在后,也不论view的visibility状态都会被调用,适合做最后的清理操作;就TwinklingRefreshLayout来说,Header和Footer需要及时显示出来,View又没有明显的生命周期,因此在onAttachedToWindow()中进行设置可以保证在onDraw()之前添加了刷新控件。1. Override 2.   

6、;  protected void onAttachedToWindow()  3.         super.onAttachedToWindow(); 4.  5.         /添加头部 6.         FrameLayout headVie

7、wLayout = new FrameLayout(getContext(); 7.         LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0); 8.         layoutParams.gravity&#

8、160;= Gravity.TOP; 9.         headViewLayout.setLayoutParams(layoutParams); 10.  11.         mHeadLayout = headViewLayout; 12.         this.addVi

9、ew(mHeadLayout);/addView(view,-1)添加到-1的位置 13.  14.         /添加底部 15.         FrameLayout bottomViewLayout = new FrameLayout(getContext(); 16.       &#

10、160; LayoutParams layoutParams2 = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0); 17.         layoutParams2.gravity = Gravity.BOTTOM; 18.         bottomViewLayo

11、ut.setLayoutParams(layoutParams2); 19.  20.         mBottomLayout = bottomViewLayout; 21.         this.addView(mBottomLayout); 22.         /.其它步骤 

12、;23.       但是当TwinklingRefreshLayout应用在Activity或Fragment中时,可能会因为执行onResume重新触发了onAttachedToWindow()方法而导致重复创建Header和Footer挡住原先添加的View,因此需要加上判断:1. Override 2.    protected void onAttachedToWindow()  3.      

13、  super.onAttachedToWindow(); 4.        System.out.println("onAttachedToWindow绑定窗口"); 5.  6.        /添加头部 7.        if (mHeadLayout = null)&

14、#160; 8.            FrameLayout headViewLayout = new FrameLayout(getContext(); 9.            LayoutParams layoutParams = new LayoutParams(Vi

15、ewGroup.LayoutParams.MATCH_PARENT, 0); 10.            layoutParams.gravity = Gravity.TOP; 11.            headViewLayout.setLayoutParams(layoutParams); 12.  

16、;13.            mHeadLayout = headViewLayout; 14.  15.            this.addView(mHeadLayout);/addView(view,-1)添加到-1的位置 16.  17.      

17、60;     if (mHeadView = null) setHeaderView(new RoundDotView(getContext(); 18.         19.        /. 20.      2. View的事件分发机制事件的分发过程由dispatch

18、TouchEvent、onInterceptTouchEvent和onTouchEvent三个方法来共同完成的。由于事件的传递是自顶向下的,对于ViewGroup,笔者觉得最重要的就是onInterceptTouchEvent方法了,它关系到事件是否能够继续向下传递。看如下伪代码:1. public boolean dispatchTouchEvent(MotionEvenet ev) 2.     boolean consume = false; 3.  

19、0;  if (onInterceptTouchEvent(ev)  4.         consume = onTouchEvent(ev); 5.     else 6.         consume = child.dispatchTouchEvent(ev); 7.

20、     8.     return consume; 9.   如代码所示,如果ViewGroup拦截了(onInterceptTouchEvent返回true)事件,则事件会在ViewGroup的onTouchEvent方法中消费,而不会传到子View;否则事件将交给子View去分发。我们需要做的就是在子View滚动到顶部或者底部时及时的拦截事件,让ViewGroup的onTouchEvent来交接处理滑动事件。3. 判断子View滚动达到边界在什么时候对事件进

21、行拦截呢?对于Header,当手指向下滑动也就是 dy>0 且子View已经滚动到顶部(不能再向上滚动)时拦截;对于bottom则是 dy<0 且子View已经滚动到底部(不能再向下滚动)时拦截:1. Override 2.     public boolean onInterceptTouchEvent(MotionEvent ev)  3.         switch (ev.getAct

22、ion()  4.             case MotionEvent.ACTION_DOWN: 5.                 mTouchY = ev.getY(); 6.      

23、           break; 7.             case MotionEvent.ACTION_MOVE: 8.                 float

24、60;dy = ev.getY() - mTouchY; 9.  10.                 if (dy > 0 && !canChildScrollUp()  11.         

25、0;           state = PULL_DOWN_REFRESH; 12.                     return true; 13.       

26、60;          else if (dy < 0 && !canChildScrollDown() && enableLoadmore)  14.                  &

27、#160;  state = PULL_UP_LOAD; 15.                     return true; 16.                 

28、; 17.                 break; 18.          19.         return super.onInterceptTouchEvent(ev); 20.    

29、;   判断View能不能继续向上滚动,对于sdk14以上版本,v4包里提供了方法:1. public boolean canChildScrollUp()  2.     return ViewCompat.canScrollVertically(mChildView, -1); 3.   其它情况,直接交给子View了,ViewGroup这里也管不着。4. ViewGroup 的 onTouchEvent 方法走到这一步,子View

30、的滚动已经交给子View自己去搞了,ViewGroup需要处理的事件只有两个临界状态,也就是用户在下拉可能想要刷新的状态和用户在上拉可能想要加载更多的状态。也就是上面state记录的状态。接下来的事情就简单咯,监听一下ACTION_MOVE和ACTION_UP就好了。首先在ACTION_DOWN时需要记录下最原先的手指按下的位置 mTouchY,然后在一系列ACTION_MOVE过程中,获取当前位移(ev.getY()-mTouchY),然后通过 某种计算方式 不断计算当前的子View应该位移的距离offsetY,调用mChildView.setTranslationY(offsetY)来不断

31、设置子View的位移,同时需要给HeadLayout申请布局高度来完成顶部控件的显示。这其中笔者使用的计算方式就是插值器(Interpolator)。在ACTION_UP时,需要判断子View的位移有没有达到进入刷新或者是加载更多状态的要求,即mChildView.getTranslationY() >= mHeadHeight - mTouchSlop,mTouchSlop是为了防止发生抖动而存在。判断进入了刷新状态时,当前子View的位移在HeadHeight和maxHeadHeight之间,所以需要让子View的位移回到HeadHeight处,否则就直接回到0处。5. Interp

32、olator插值器Interpolator用于动画中的时间插值,其作用就是把0到1的浮点值变化映射到另一个浮点值变化。上面提到的计算方式如下:1. float offsetY = decelerateInterpolator.getInterpolation(dy / mWaveHeight / 2) * dy / 2; 其中(dy / mWaveHeight / 2)是一个01之间的浮点值,随着下拉高度的增加,这个值越来越大,通过decelerateInterpolator

33、获取到的插值也越来越大,只不过这些值的变化量是越来越小(decelerate效果)。dy表示的是手指移动的距离。这只是笔者为了滑动的柔和性使用的一种计算方式,头部位移的最大距离是mWaveHeight = dy/2,这样看的话可以发现 dy / mWaveHeight / 2 会从0到1变化。Interpolator继承自TimeInterpolator接口,源码如下:1. public interface TimeInterpolator  2.     /* 3.    

34、  * Maps a value representing the elapsed fraction of an animation to a value that represents 4.      * the interpolated fraction. This interpolated value&

35、#160;is then multiplied by the change in 5.      * value of an animation to derive the animated value at the current elapsed animation time. 6.   

36、0;  * 7.      * param input A value between 0 and 1.0 indicating our current point 8.      *        in the animation

37、0;where 0 represents the start and 1.0 represents 9.      *        the end 10.      * return The interpolation value. This value&#

38、160;can be more than 1.0 for 11.      *         interpolators which overshoot their targets, or less than 0 for 12.      *

39、60;        interpolators that undershoot their targets. 13.      */ 14.     float getInterpolation(float input); 15.   getInterpolation接收一个0.01.0之间的float参数,0.0代

40、表动画的开始,1.0代表动画的结束。返回值则可以超过1.0,也可以小于0.0,比如OvershotInterpolator。所以getInterpolation()是用来实现输入01返回01左右的函数值的一个函数。 6. 属性动画上面说到了手指抬起的时候,mChildView的位移要么回到mHeadHeight处,要么回到0处。直接setTranslationY()不免太不友好,所以我们这里使用属性动画来做。本来是直接可以用mChildView.animate()方法来完成属性动画的,因为需要兼容低版本并回调一些参数,所以这里使用ObjectAnimator:1. private&#

41、160;void animChildView(float endValue, long duration)  2.         ObjectAnimator oa = ObjectAnimator.ofFloat(mChildView, "translationY", mChildView.getTranslationY(), endValue); 3. 

42、0;       oa.setDuration(duration); 4.         oa.setInterpolator(new DecelerateInterpolator();/设置速率为递减 5.         oa.addUpdateListener(new ValueAnimator.AnimatorUpda

43、teListener()  6.             Override 7.             public void onAnimationUpdate(ValueAnimator animation)  8.     

44、0;           int height = (int) mChildView.getTranslationY();/获得mChildView当前y的位置 9.                 height = Math.abs(height);

45、0;10.  11.                 mHeadLayout.getLayoutParams().height = height; 12.                 mHeadLayout.requestLayout(

46、); 13.              14.         ); 15.     oa.start(); 16.   传统的补间动画只能够实现移动、缩放、旋转和淡入淡出这四种动画操作,而且它只是改变了View的显示效果,改变了画布绘制出来的样子,而不会真正去改变View的属性。比如用补间动画对一

47、个按钮进行了移动,只有在原位置点击按钮才会发生响应,而属性动画则可以真正的移动按钮。属性动画最简单的一种使用方式就是使用ValueAnimator:1. ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);   2. anim.start();  它可以传入多个参数,如ValueAnimator.ofFloat(0f, 5f, 3f, 10f),他会根据设置的插值器依次计算,比如想做一个心跳的效果,用ValueAnimator来控制心的当前缩放值大小就是个

48、不错的选择。除此之外,还可以调用setStartDelay()方法来设置动画延迟播放的时间,调用setRepeatCount()和setRepeatMode()方法来设置动画循环播放的次数以及循环播放的模式等。如果想要实现View的位移,ValueAnimator显然是比较麻烦的,我们可以使用ValueAnimator的子类ObjectAnimator,如下:1. ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f,&

49、#160;1f);   2. animator.setDuration(5000);   3. animator.start();    传入的第一个值是Object,不局限于View,传入的第二个参数为Object的一个属性,比如传入"abc",ObjectAnimator会去Object里面找有没有 getAbc() 和 setAbc(.) 这两个方法,如果没有,动画就没有效果,它内部应该是处理了相应的异常。另外还可以用AnimatorSet来实现多个属性动画同时播放,也

50、可以在xml中写属性动画。7. 个性化Header和Footer的接口要实现个性化的Header和Footer,最最重要的当然是把滑动过程中系数都回调出来啦。在ACTION_MOVE的时候,在ACTION_UP的时候,还有在mChildView在执行属性动画的时候,而且mChildView当前所处的状态都是很明确的,写个接口就好了。1. public interface IHeaderView  2.     View getView(); 3.  4.   

51、60; void onPullingDown(float fraction,float maxHeadHeight,float headHeight); 5.  6.     void onPullReleasing(float fraction,float maxHeadHeight,float headHeight); 7.  8.     void startAnim(f

52、loat maxHeadHeight,float headHeight); 9.  10.     void onFinish(); 11.   getView()方法保证在TwinklingRefreshLayout中可以取到在外部设置的View,onPullingDown()是下拉过程中ACTION_MOVE时的回调方法,onPullReleasing()是下拉状态中ACTION_UP时的回调方法,startAnim()则是正在刷新时回调的方法。其中fraction=mC

53、hildView.getTranslationY()/mHeadHeight,fraction=1 时,mChildView的位移恰好是HeadLayout的高度,fraction>1 时则超过了HeadLayout的高度,其最大高度可以到达 mWaveHeight/mHeadHeight。这样我们只需要写一个View来实现这个接口就可以实现个性化了,该有的参数都有了!8. 实现越界回弹不能在手指快速滚动到顶部时对越界做出反馈,这是一个继承及ViewGroup的刷新控件的通病。没有继承自具体的列表控件,它没办法获取到列表控件的Scroller,不能获取到列表控件的当前滚动速度,更是不能预

54、知列表控件什么时候能滚动到顶部;同时ViewGroup除了达到临界状态的事件被拦截了,其它事件全都交给了子View去处理。我们能获取到的有关于子View的操作,只有简简单单的手指的触摸事件。so, let's do it!1. mChildView.setOnTouchListener(new OnTouchListener()  2.     Override 3.     public boolean onTouch(View v,&

55、#160;MotionEvent event)  4.         return gestureDetector.onTouchEvent(event); 5.      6. );  我们把在mChildView上的触摸事件交给了一个工具类GestureDetector去处理,它可以辅助检测用户的单击、滑动、长按、双击、快速滑动等行为。我们这里只需要重写onFling()方法并获取到手指在Y

56、方向上的速度velocityY,要是再能及时的发现mChildView滚动到了顶部就可以解决问题了。1. GestureDetector gestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener()  2.  3.         Override 4.   

57、60;     public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)  5.             mVelocityY = velocityY; 6.  

58、;        7.     );  此外获取速度还可以用VelocityTracker,比较麻烦一些:1. VelocityTracker tracker = VelocityTracker.obtain(); 2. tracker.addMovement(ev); 3. /然后在恰当的位置使用如下方法获取速度 4. puteCurrentVelocity(1000); 5. mVe

59、locityY = (int)tracker.getYVelocity();  继续来实现越界回弹。对于RecyclerView、AbsListView,它们提供有OnScrollListener可以获取一下滚动状态:1. if (mChildView instanceof RecyclerView)  2.             (RecyclerView) mChildV

60、iew).addOnScrollListener(new RecyclerView.OnScrollListener()  3.                 Override 4.                 public 

61、;void onScrollStateChanged(RecyclerView recyclerView, int newState)  5.                     if (!isRefreshing && !isLoadingmore &&

62、0;newState = RecyclerView.SCROLL_STATE_IDLE)  6.                         if (mVelocityY >= 5000 && ScrollingUtil.isRecyclerV

63、iewToTop(RecyclerView) mChildView)  7.                             animOverScrollTop(); 8.         &

64、#160;                9.                         if (mVelocityY <= -5000 &

65、& ScrollingUtil.isRecyclerViewToBottom(RecyclerView) mChildView)  10.                             animOverScrollBottom(); 11.  

66、;                        12.                      13.    

67、60;                super.onScrollStateChanged(recyclerView, newState); 14.                  15.      

68、;       ); 16.           笔者选取了一个滚动速度的临界值,Y方向的滚动速度大于5000时才允许越界回弹,RecyclerView的OnScrollListener可以让我们获取到滚动状态的改变,滚动到顶部时滚动完成,状态变为SCROLL_STATE_IDLE,执行越界回弹动画。这样的策略也还有一些缺陷,不能获取到mChildView滚动到顶部时的滚动速度,也就不能根据不同的滚动速度来实现更加友

69、好的越界效果。现在的越界高度是固定的,还需要后面进行优化,比如采用加速度来计算,是否可行还待验证。9. 滚动的延时计算策略上面的方法对于RecyclerView和AbsListView都好用,对于ScrollView、WebView就头疼了,只能使用延时计算一段时间看有没有到达顶部的方式来判断的策略。延时策略的思想就是通过发送一系列的延时消息从而达到一种渐进式计算的效果,具体来说可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。另外提一点,需要不断循环计算一个数值,比如自定义View需要实现根据某个数值变化的动效,最好不要使用Thread + whi

70、le 循环的方式计算,使用ValueAnimator会是更好的选择。这里笔者选择了Handler的方式。1. Override 2. public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)  3.     mVelocityY = velocityY; 4.   &#

71、160; if (!(mChildView instanceof AbsListView | mChildView instanceof RecyclerView)  5.         /既不是AbsListView也不是RecyclerView,由于这些没有实现OnScrollListener接口,无法回调状态,只能采用延时策略 6.      

72、60;  if (Math.abs(mVelocityY) >= 5000)  7.             mHandler.sendEmptyMessage(MSG_START_COMPUTE_SCROLL); 8.          else  9.   &

73、#160;         cur_delay_times = ALL_DELAY_TIMES; 10.          11.      12.     return false; 13.   在滚动速度大于5000的时候发送一个重新计算的消息,Handl

74、er收到消息后,延时一段时间继续给自己发送消息,直到时间用完或者mChildView滚动到顶部或者用户又进行了一次Fling动作。1. private Handler mHandler = new Handler()  2.     Override 3.     public void handleMessage(Message msg)  4.    &

75、#160;    switch (msg.what)  5.             case MSG_START_COMPUTE_SCROLL: 6.                 cur_delay_times =

76、 -1; /这里没有break,写作-1方便计数 7.             case MSG_CONTINUE_COMPUTE_SCROLL: 8.                 cur_delay_times+; 9.  10.  

77、               if (!isRefreshing && !isLoadingmore && mVelocityY >= 5000 && childScrollToTop()  11.        &

78、#160;            animOverScrollTop(); 12.                     cur_delay_times = ALL_DELAY_TIMES; 13.    &

79、#160;             14.  15.                 if (!isRefreshing && !isLoadingmore && mVelocityY <= 

80、-5000 && childScrollToBottom()  16.                     animOverScrollBottom(); 17.               

81、;      cur_delay_times = ALL_DELAY_TIMES; 18.                  19.  20.                

82、60;if (cur_delay_times < ALL_DELAY_TIMES) 21.                     mHandler.sendEmptyMessageDelayed(MSG_CONTINUE_COMPUTE_SCROLL, 10); 22.      

83、           break; 23.             case MSG_STOP_COMPUTE_SCROLL: 24.                 cur_de

84、lay_times = ALL_DELAY_TIMES; 25.                 break; 26.          27.      28. ;  ALL_DELAY_TIMES是最多可以计算的次数,当Handler接

85、收到MSG_START_COMPUTE_SCROLL消息时,如果mChildView没有滚动到边界处,则会在10ms之后向自己发送一条MSG_CONTINUE_COMPUTE_SCROLL的消息,然后继续进行判断。然后在合适的时候越界回弹就好了。10. 实现个性化Header这里笔者来演示一下,怎么轻轻松松的做一个个性化的Header,比如新浪微博样式的刷新Header(如下面第1图)。1. 创建 SinaRefreshView 继承自 FrameLayout 并实现 IHeaderView 接口2. getView()方法中返回this3. 在onAttachedToWindow()方法中获

86、取一下需要用到的布局(笔者写到了xml中,也可以直接在代码里面写)1. Override 2. protected void onAttachedToWindow()  3.     super.onAttachedToWindow(); 4.  5.     if (rootView = null)  6.        &#

87、160;rootView = View.inflate(getContext(), R.layout.view_sinaheader, null); 7.         refreshArrow = (ImageView) rootView.findViewById(R.id.iv_arrow); 8.         refreshTextVi

88、ew = (TextView) rootView.findViewById(R.id.tv); 9.         loadingView = (ImageView) rootView.findViewById(R.id.iv_loading); 10.         addView(rootView); 11.   

89、60;  12.  4. 实现其它方法1. Override 2. public void onPullingDown(float fraction, float maxHeadHeight, float headHeight)  3.     if (fraction < 1f) refreshTextView.setText(pullDownStr); 4. 

90、0;   if (fraction > 1f) refreshTextView.setText(releaseRefreshStr); 5.     refreshArrow.setRotation(fraction * headHeight / maxHeadHeight * 180); 6.  7.  8. Override 9. public void&#

91、160;onPullReleasing(float fraction, float maxHeadHeight, float headHeight)  10.     if (fraction < 1f)  11.         refreshTextView.setText(pullDownStr); 12.   &#

92、160;     refreshArrow.setRotation(fraction * headHeight / maxHeadHeight * 180); 13.         if (refreshArrow.getVisibility() = GONE)  14.             refreshArrow.setVisibility(VISIBLE); 15. 

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论