引言
上一篇文章我們介紹了實現彈性滑動的三種方式,但僅僅是給出了代碼片段和方法理論。今天我們結合一個具體的例子來談一下如何使用這三種方法來實現彈性滑動。今天我們的例子是仿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 }