Android弹性滑动的三种实现方式
引言
上一篇文章我们介绍了实现弹性滑动的三种方式,但仅仅是给出了代码片段和方法理论。今天我们结合一个具体的例子来谈一下如何使用这三种方法来实现弹性滑动。今天我们的例子是仿IOS的下拉操作,我们知道Android系统ListView之类的控件的是不存在下拉操作的,IOS系统大多数界面都可以下拉,然后缓缓恢复,今天我们的例子就是简单的仿IOS的这种效果。
一些准备工作
我们自定义了一个View,让一个LinearLayout填充这个View,模拟占满全屏的效果。XML代码如下:
1 <com.research.gong.android_view_research.view.PullView 2 android:layout_width="match_parent" 3 android:layout_height="wrap_content"> 4 5 <LinearLayout 6 android:layout_width="match_parent" 7 android:layout_height="1000dp" 8 android:orientation="vertical" 9 android:background="#4097e6" 10 android:id="@+id/main"> 11 12 </LinearLayout> 13 14 </com.research.gong.android_view_research.view.PullView>
Scroller实现弹性滑动
我们想实现弹性滑动,第一步需要实现的就是View需要能够跟随手指滑动,这当然让我们想到了OnTouchEvent来检测用户的触摸事件。先看核心代码:
1 @Override 2 public boolean onTouchEvent(MotionEvent event) { 3 int y=(int)event.getY(); 4 switch (event.getAction()){ 5 //手指按下时,初始化按下位置的X,Y位置值 6 case MotionEvent.ACTION_DOWN: 7 mLastY=y; 8 break; 9 //计算滑动的偏移量,产生滑动效果 10 case MotionEvent.ACTION_MOVE: 11 //手指向下滑动delayY>0,向上滑动delayY<0 12 int delayY=y-mLastY; 13 delayY=delayY*-1; 14 scrollBy(0,delayY); 15 break; 16 case MotionEvent.ACTION_UP: 17 /** 18 * scrollY是指:View的上边缘和View内容的上边缘(其实就是第一个ChildView的上边缘)的距离 19 * scrollY=上边缘-View内容上边缘,scrollTo/By方法滑动的知识View的内容 20 * 往下滑动scrollY是负值 21 */ 22 int scrollY=getScrollY(); 23 smoothScrollByScroller(scrollY); 24 //smoothScrollByAnim(scrollY); 25 //smoothScrollByHandler(scrollY); 26 break; 27 } 28 mLastY=y; 29 return true; 30 }
在代码中,我们看到当手指按下时,记录按下的位置mLastY,然后我们在ACTION_MOVE事件中不断的计算滑动的偏移量delayY然后使用scrollBy来实现View的滑动,这样我们就可以实现View跟随手指滑动而滑动。细心的朋友可能发现我手指向下滑动,delayY应该是正值,拿View向下滑动为什么需要将delayY*-1变成负数?这是因为Android系统是通过移动可视区域来实现改变View内容位置的,我们自觉上View向下滑动,对于可视区域来说是向上滑动,所以scrollBy需要使用负值,这样感官上和我们向下滑动效果是一样的,这一点需要注意。
下面我们开始分析手指放开的那一段代码逻辑,看代码的22-23行,我们先获取mScrollY的值,这个值我们在上一篇文章中已经介绍过了,是指View的上边缘和View内容上边缘的距离,其实就是我们手指释放的那一刻,滑动的总的大小。我们只需要将View缓缓划过这一段距离,就可以产生弹性滑动的效果。我们看下面的代码如何处理:
1 /** 2 * 执行滑动效果 3 * 使用scroller实现 4 * @param dy 5 */ 6 private void smoothScrollByScroller(int dy){ 7 mScroller.startScroll(0,dy,0,dy*-1,1000); 8 invalidate(); 9 } 10 11 @Override 12 public void computeScroll() { 13 if (mScroller.computeScrollOffset()) { 14 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); 15 postInvalidate(); 16 } 17 }
我们从代码中看到,我们使用了上一篇博客中的代码范式,将需要滑动的距离dy作为参数进行传递,然后使用startScroll方法来实现滑动。这个方法在上一篇文章中已经介绍过。
使用动画实现滑动
我们上一篇文章中还介绍了使用动画来实现弹性滑动效果,现在我们给出代码来看一下具体的实现思路:
1 /** 2 * 使用动画来实现 3 * @param dy 4 */ 5 private void smoothScrollByAnim(int dy){ 6 final float delayY=dy; 7 ValueAnimator valueAnimator=ValueAnimator.ofInt(0,1).setDuration(1000); 8 valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 9 @Override 10 public void onAnimationUpdate(ValueAnimator animation) { 11 //计算动画完成的百分比 12 float percent=animation.getAnimatedFraction(); 13 float dy=(1.0f-percent)*delayY; 14 scrollTo(0,(int)dy); 15 } 16 }); 17 valueAnimator.start(); 18 }
我们看到第一步也是记录滑动的总的距离,然后使用动画的addUpdateListener方法来监听动画的每一帧,然后根据执行动画的百分比来计算现在需要滑动的位置,使用scrollTo方法滑动到指定的位置。dy计算出来都是负数并且越往后,越接近0,也就是可视区域逐渐往下滑动,这样我们看起来就是View往上恢复。
使用Handler或者延时策略
下面我们介绍最后一种方法,使用延时策略来模拟Scroller。我们将1000毫秒分成50此执行,每一次延时20ms,然后在handler中根据执行的次数来计算完成的比例,然后修改View的位置实现滑动,代码如下:
1 private int count; 2 private int delayY; 3 /** 4 * 使用Handler来实现 5 * @param dy 6 */ 7 private void smoothScrollByHandler(int dy){ 8 delayY=dy; 9 count=0; 10 scrollHandler.sendEmptyMessageDelayed(0,20); 11 } 12 13 private Handler scrollHandler=new Handler(){ 14 @Override 15 public void handleMessage(Message msg) { 16 switch (msg.what){ 17 case 0: 18 count++; 19 if(count<=50){ 20 float percent=count/50.0f; 21 int scrollY=(int)(delayY*(1.0f-percent)); 22 Log.d("scrollY:",String.valueOf(scrollY)); 23 scrollTo(0,scrollY); 24 scrollHandler.sendEmptyMessageDelayed(0,20); 25 } 26 break; 27 default: 28 break; 29 } 30 } 31 };
我们看到代码思路和使用动画类似。
总结
上面3种实现弹性滑动的方法,我们建议还是优先选择Scroller来实现,其他两种方法指示提供类似的思路。下面我贴出详细的代码,供各位朋友实验学习。详细代码如下:
1 package com.research.gong.android_view_research.view; 2 3 import android.animation.ValueAnimator; 4 import android.content.Context; 5 import android.os.Handler; 6 import android.os.Message; 7 import android.util.AttributeSet; 8 import android.util.Log; 9 import android.view.LayoutInflater; 10 import android.view.MotionEvent; 11 import android.view.View; 12 import android.view.ViewGroup; 13 import android.widget.Scroller; 14 15 /** 16 * 模拟下拉组件 17 */ 18 public final class PullView extends ViewGroup { 19 20 private int mLastY; 21 private Context mContext; 22 private Scroller mScroller; 23 //子View的个数 24 private int mChildCount; 25 26 public PullView(Context context){ 27 this(context,null); 28 } 29 30 public PullView(Context context, AttributeSet attributeSet){ 31 super(context,attributeSet); 32 mContext=context; 33 initView(); 34 } 35 36 private void initView(){ 37 mScroller=new Scroller(mContext); 38 } 39 40 @Override 41 public boolean onTouchEvent(MotionEvent event) { 42 int y=(int)event.getY(); 43 switch (event.getAction()){ 44 //手指按下时,初始化按下位置的X,Y位置值 45 case MotionEvent.ACTION_DOWN: 46 mLastY=y; 47 break; 48 //计算滑动的偏移量,产生滑动效果 49 case MotionEvent.ACTION_MOVE: 50 //手指向下滑动delayY>0,向上滑动delayY<0 51 int delayY=y-mLastY; 52 delayY=delayY*-1; 53 scrollBy(0,delayY); 54 break; 55 case MotionEvent.ACTION_UP: 56 /** 57 * scrollY是指:View的上边缘和View内容的上边缘(其实就是第一个ChildView的上边缘)的距离 58 * scrollY=上边缘-View内容上边缘,scrollTo/By方法滑动的知识View的内容 59 * 往下滑动scrollY是负值 60 */ 61 int scrollY=getScrollY(); 62 //smoothScrollByScroller(scrollY); 63 //smoothScrollByAnim(scrollY); 64 smoothScrollByHandler(scrollY); 65 break; 66 } 67 mLastY=y; 68 return true; 69 } 70 71 /** 72 * 执行滑动效果 73 * 使用scroller实现 74 * @param dy 75 */ 76 private void smoothScrollByScroller(int dy){ 77 mScroller.startScroll(0,dy,0,dy*-1,1000); 78 invalidate(); 79 } 80 81 @Override 82 public void computeScroll() { 83 if (mScroller.computeScrollOffset()) { 84 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); 85 postInvalidate(); 86 } 87 } 88 89 /** 90 * 使用动画来实现 91 * @param dy 92 */ 93 private void smoothScrollByAnim(int dy){ 94 final float delayY=dy; 95 ValueAnimator valueAnimator=ValueAnimator.ofInt(0,1).setDuration(1000); 96 valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 97 @Override 98 public void onAnimationUpdate(ValueAnimator animation) { 99 //计算动画完成的百分比 100 float percent=animation.getAnimatedFraction(); 101 float dy=(1.0f-percent)*delayY; 102 scrollTo(0,(int)dy); 103 } 104 }); 105 valueAnimator.start(); 106 } 107 108 private int count; 109 private int delayY; 110 /** 111 * 使用Handler来实现 112 * @param dy 113 */ 114 private void smoothScrollByHandler(int dy){ 115 delayY=dy; 116 count=0; 117 scrollHandler.sendEmptyMessageDelayed(0,20); 118 } 119 120 private Handler scrollHandler=new Handler(){ 121 @Override 122 public void handleMessage(Message msg) { 123 switch (msg.what){ 124 case 0: 125 count++; 126 if(count<=50){ 127 float percent=count/50.0f; 128 int scrollY=(int)(delayY*(1.0f-percent)); 129 Log.d("scrollY:",String.valueOf(scrollY)); 130 scrollTo(0,scrollY); 131 scrollHandler.sendEmptyMessageDelayed(0,20); 132 } 133 break; 134 default: 135 break; 136 } 137 } 138 }; 139 140 /** 141 * 重新计算子View的高度和宽度 142 * @param widthMeasureSpec 143 * @param heightMeasureSpec 144 */ 145 @Override 146 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 147 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 148 int measuredWidth; 149 int measureHeight; 150 mChildCount = getChildCount(); 151 //测量子View 152 measureChildren(widthMeasureSpec, heightMeasureSpec); 153 int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec); 154 int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec); 155 int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec); 156 int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec); 157 158 //获取横向的padding值 159 int paddingLeft=getPaddingLeft(); 160 int paddingRight=getPaddingRight(); 161 final View childView = getChildAt(0); 162 /** 163 * 如果子View的数量是0,就读取LayoutParams中数据 164 * 否则就对子View进行测量 165 * 此处主要是针对wrap_content这种模式进行处理,因为默认情况下 166 * wrap_content等于match_parent 167 */ 168 if (mChildCount == 0) { 169 ViewGroup.LayoutParams layoutParams=getLayoutParams(); 170 if(layoutParams!=null){ 171 setMeasuredDimension(layoutParams.width,layoutParams.height); 172 }else { 173 setMeasuredDimension(0, 0); 174 } 175 } else if (heightSpaceMode == MeasureSpec.AT_MOST && widthSpaceMode == MeasureSpec.AT_MOST) { 176 measuredWidth = childView.getMeasuredWidth() * mChildCount; 177 measureHeight = getChildMaxHeight(); 178 //将两侧的padding值加上去 179 measuredWidth=paddingLeft+measuredWidth+paddingRight; 180 setMeasuredDimension(measuredWidth, measureHeight); 181 } else if (heightSpaceMode == MeasureSpec.AT_MOST) { 182 measureHeight = getChildMaxHeight(); 183 setMeasuredDimension(widthSpaceSize, measureHeight); 184 } else if (widthSpaceMode == MeasureSpec.AT_MOST) { 185 measuredWidth = childView.getMeasuredWidth() * mChildCount; 186 measuredWidth=paddingLeft+measuredWidth+paddingRight; 187 setMeasuredDimension(measuredWidth, heightSpaceSize); 188 } 189 } 190 191 192 /** 193 * 获取子View中最大高度 194 * @return 195 */ 196 private int getChildMaxHeight(){ 197 int maxHeight=0; 198 for (int i = 0; i < mChildCount; i++) { 199 View childView = getChildAt(i); 200 if (childView.getVisibility() != View.GONE) { 201 int height = childView.getMeasuredHeight(); 202 if(height>maxHeight){ 203 maxHeight=height; 204 } 205 } 206 } 207 return maxHeight; 208 } 209 210 211 /** 212 * 设置子View的布局 213 * @param changed 214 * @param l 215 * @param t 216 * @param r 217 * @param b 218 */ 219 @Override 220 protected void onLayout(boolean changed, int l, int t, int r, int b) { 221 int childLeft = 0; 222 for (int i = 0; i < mChildCount; i++) { 223 View childView = getChildAt(i); 224 if (childView.getVisibility() != View.GONE) { 225 int childWidth = childView.getMeasuredWidth(); 226 childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight()); 227 childLeft += childWidth; 228 } 229 } 230 } 231 }