我們知道,在功能機時代我們在手機上的任何操作都是在鍵盤上完成的,只有通過鍵盤才能完成輸入操作,只能通過鍵盤才能和手機互動,進入智慧機時代以後我們所有操作都可以通過觸控式螢幕的方式來完成,而我們最常見的操作就是滑動,手機螢幕和PC端的顯示屏最大的區別就是,PC顯示器螢幕很大,一屏可以顯示跟多內容,而手機螢幕就小了很多,一螢幕所能顯示的內容就非常有限,我們可以通過上下滑動,左右滑動翻頁來顯示我們想要看到的內容。我們開啟任意一款手機應用,無處不在的上滑,下滑,左滑,右滑操作,由此可見滑動操作在移動手機開發當中是多麼的重要,因此今天我們來研究View的滑動。
在Android系統中View給我們提供了兩個非常重要關於滑動操作的方法scrollTo和scrollBy,下面我們通過scrollTo和scrollBy來完成View的滑動。
View的滑動佈局檔案如下:
<RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" xmlns:android="http://schemas.android.com/apk/res/android" > <TextView android:id="@+id/txt_scroll_to" android:layout_width="match_parent" android:layout_height="match_parent" android:text="Hello world" android:gravity="center" /> <Button android:id="@+id/btn_scroll_smooth" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:text="ScrollTo" android:textAllCaps="false" /></RelativeLayout>
佈局檔案中有兩個控制元件,一個TextView和一個Button,我們點選按鈕Button呼叫TextView的scrollTo方法和scrollBy方法,來觀察View滾動的效果。
此時Hello world往上方進行了移動,再次點選按鈕呼叫 mTxtScroll.scrollTo(200,200),發現HelloWorld的位置沒有發生任何的變化。
接下來我們把呼叫引數修改為-200,即:
再看看效果,HelloWorld往右下方移動,scrollTo測試完畢,我們在看看scrollBy是什麼效果
我連續點選了三次,HelloWorld連續往左上方移動了三次,這一點和scrollTo還是有些不同的,我們看看View的scrollTo和scrollBy的原始碼:
public void scrollTo(int x, int y) { if (mScrollX != x || mScrollY != y) { int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; mScrollY = y; invalidateParentCaches(); onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { postInvalidateOnAnimation(); } } } public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); }
通過原始碼我們不難解釋這個效果了,scrollBy內部呼叫了scrollTo,而且每次移動都是在目前mScrollX和mScrollY的基礎上進行的移動的,因此scrollTo是絕對移動,scrollBy是相對移動。
需要注意的上很多人在理解上有些轉不過彎認為x,y都為正應該往右下方移動,怎麼會向左上方移動呢,其實x,y並不是要 移動的座標位置,而是相對於Hello world的原始位置的偏移量,通常在View在預設的情況下,我們首先都會往上滑,或者往左滑,這都是一個習慣的操作,所以往左滑,往上滑為正值也就不難理解了。
另外我們需要注意的是scrollTo和scrollBy滑動的是View的內容,而View自身的位置並不會發生任何變化,不妨我們做個測試驗證一下頁面的初始開啟的時候我們列印下當前View的位置資訊 V/ScrollTestActivity: scrollX:0;scrollY:0|x:0.0;y:0.0 緊接著呼叫mTxtScroll.scrollTo(-200,-200);移動View的位置,然後我們再次列印View的位置資訊:V/ScrollTestActivity: scrollX:-200;scrollY:-200|x:0.0;y:0.0 你會驚奇的發現,View的x,y座標沒有任何變化,只是View的mScrollX和mScrollY的值發生了變化,也就是說View滑動的是自己的內容,而View本身在佈局中的位置並沒有發生任何的改變。
通過以上測試我們不難得到以下幾條結論:
1.scrollTo是絕對滑動,它是相對於Hello world原始位置的滑動2.scrollBy是相對移動,是相對於Hello world當前位置的滑動3.無論是scrollTo(x,y)還是呼叫scrollBy(x,y),x為正往左邊滑動,x為負往右邊滑動,y為正往上滑動,y為負往下滑動4.無論是scrollTo還是scrollBy它滑動的是View的內容,View在整個佈局中的位置不會發生任何改變Scroller實現彈性滑動另外我們有沒有發現這種滑動效果是瞬間完成的,沒有任何的平滑過渡效果,這種方式的使用者體驗是在是太差了,我們需要實現漸進式滑動,也就是今天我們所要講的彈性滑動,這種彈性滑動效果的實現方式有很多,但是實現的思想都是相同的,將view的一個大的滑動分割成若干個小的滑動並且在一段時間內完成,這樣就可以實現彈性滑動,可以藉助Scroller來完成,也可以通過Handler.postDelay和Thread.sleep來完成。下面我們就來介紹如何藉助Scroller和View的scrollTo方法來實現View的彈性滑動,其實也很簡單,我們只需自定義一個TextView並複寫他的computeScroll方法即可,主要的邏輯邏輯程式碼如下:
public class TestTextView extends android.support.v7.widget.AppCompatTextView{ private Scroller mScroller; public TestTextView(Context context) { super(context); initView(); } public void initView(){ mScroller = new Scroller(getContext()); } public void smoothScrollTo(int x,int y){ mScroller.startScroll(getScrollX(),getScrollY(),x,y,500); invalidate(); } @Override public void computeScroll() { if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); postInvalidate(); } }}
這就是彈性滑動的典型模板程式碼,我們只需要呼叫mTxtContent.smoothScrollTo(-300,-300);就可以實現TextView的彈性滑動我看一下所實現的效果:
就是這麼的簡單,上面是Scroller的典型的使用方法,當我們構造一個Scroller物件並且呼叫它的startScroll方法時,Scroller內部其實什麼也沒做,它只是儲存了我們傳遞的幾個引數,這幾個引數從startScroll的方法上就可以看出來,如下所示:
public void startScroll(int startX, int startY, int dx, int dy, int duration) { mMode = SCROLL_MODE; mFinished = false; mDuration = duration; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartX = startX; mStartY = startY; mFinalX = startX + dx; mFinalY = startY + dy; mDeltaX = dx; mDeltaY = dy; mDurationReciprocal = 1.0f / (float) mDuration; }
這個方法引數還是比較明確的startX和startY表示滑動的起點位置,dx和dy表示滑動的距離,duration表示滑動需要花費的時間,然後你有沒有發現這這個方法裡面都是一堆的賦值操作,並沒有呼叫View的scrollTo方法來進行滑動,也就是說僅僅呼叫Scroller的startScroll方法並不能讓View滑動起來,很奇怪,為什麼View就是開始滑動了?原因就在於mScroller.startScroll下面的這個invalidate方法,是不是很神奇,其實原因很簡單invalidate會導致View的重繪,也就是會呼叫他的onDraw方法,onDraw方法又會呼叫computeScroll方法,computeScroll方法是個空方法,裡面程式碼就是我們實現View滑動的核心程式碼,mScroller.computeScrollOffset來計算每次移動的距離,然後呼叫scrollTo方法進行平滑移動,移動完成再次呼叫postInvalidate方法,該方法又會呼叫onDraw方法的呼叫,onDraw繼續會呼叫computeScroll方法,如此反覆呼叫直到整個滑動結束,完成View的平滑移動。
通過上面的分析我們已經知道的Scroller的工作原理,Scroller本身並不會引起View的平滑移動,必須藉助View的computeScroll方法才能完成彈性滑動,它不斷讓View進行重繪,不斷的呼叫computeScroll方法來計算滑動距離再呼叫scrollTo方法進行滑動,每次都會滑動一小段距離,而多次滑動連線在一起就構成一次完美的彈性滑動,這就是Scroller的工作原理。
自定義一個ViewPager通過上面的學習我們已經知道了如何實現一個View的彈性滑動,只是簡單的介紹了它的使用方法,接下來我們要看看它在實戰開發過程中都有哪些應用。ViewPager大家都用過,通過他可以實現多個View的橫向的左滑右滑的橫向切換效果,現在我們就利用剛才所掌握的Scroller彈性滑動技術自定義實現一個自己的ViewPager,先來看下實現的效果:
現在我們來分析一下他的實現思路:
1.實現ViewPager裡面子View的位置問題2.手指在螢幕上左右拖動的時候子View進行左右移動3.當手指鬆開的時候如果滑動速度很快,如果是向左滑則切換到下一頁,如果是向右滑則切換到到上一頁,如果速度不是很快是左滑但是手指拖動當前的頁面已經劃出了螢幕一半那麼應該切換到下一頁,如果沒有沒有劃出當前頁面的一半那麼就回彈到初始的位置,當然左滑也是一樣的道理子view的新增首先我給ViewPager添加了三個Textview
mViewPager = findViewById(R.id.view_my_pager); for(int i =0; i < 3; i++){ TextView txtContent = (TextView) LayoutInflater.from(this).inflate(R.layout.item_test_view_pager, mViewPager,false); txtContent.setText(String.valueOf(i)); txtContent.setBackgroundColor(colors[i]); mViewPager.addView(txtContent); }
單個頁面view的佈局檔案itemtestview_pager.xml這個佈局檔案也很簡單,也就只有一個TextView
<?xml version="1.0" encoding="utf-8"?><TextView android:id="@+id/txt_content" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:android="http://schemas.android.com/apk/res/android" />
確定子view的位置
首先我們解決的是ViewPager的子View的位置問題,我們給ViewPager添加了三個子View,那他的位置是橫向一字排開,我們知道確定View的位置就是給view設定它的left,top,right,bottom的這四個引數;那麼第一個子View的位置就是left:0,top:0,right:子View的寬,bottom:子View的高,第二個子View的位置就是在一個第一個子View的基礎上計算得到的,left:第一個view的right,top:0,right:第一個view的right+第二個子View的寬,bottom:第二個子View的高,第三個子View的位置也是基於第二個子view的位置計算得到,具體的程式碼實現如下:
protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = getChildCount(); int childLeft = 0; for (int i = 0; i < childCount; i++) { View child = getChildAt(i); int measuredWidth = child.getMeasuredWidth(); int measuredHeight = child.getMeasuredHeight(); child.layout(childLeft, 0, childLeft + measuredWidth, measuredHeight); childLeft += measuredWidth; } Log.v(TAG, "view pager width:" + getMeasuredWidth() + ";height:" + getMeasuredHeight()); }
注意了,現在計算的話,child.getMeasuredWidth()和child.getMeasuredHeight()獲取的寬和高都為0,我們必須在onMeasure方法裡要測量子View的寬和高,這樣在onLayout方法才能獲取子view的寬和高,否則獲取的子view的寬和高的值始終是0.,具體的程式碼實現如下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); measureChildren(widthMeasureSpec, heightMeasureSpec); }
核心程式碼就是measureChildren(widthMeasureSpec, heightMeasureSpec);這一行
手指拖動左滑右滑的實現我們知道手指的拖動,他是由多個觸控事件元件的,手指按下應該是ACTIONDOWN,手指拖動是由多個ACTIONMOVE所組成的,手指抬起那就是ACITONUP了,此時我們需要處理的ACTIONMOVE型別的事件,我們只需要計算前後兩個相鄰的ACTION_MOVE事件的之間的滑動距離,然後在呼叫view的scrollBy方法就搞定了,注意了我們需要把上滑和下滑的事件過濾掉,只處理左滑和左滑的事件,具體的程式碼實現如下:
@Override public boolean onTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); Log.v(TAG, "onTouchEvent x:" + x + ";y:" + y); switch (event.getAction()) { case MotionEvent.ACTION_MOVE: int dx = x - mLastX; int dy = y - mLastY; if (Math.abs(dx) > Math.abs(dy)) { scrollBy(-dx, 0); } break; } mLastX = x; mLastY = y; return consume; }
手指滑動翻頁實現當手指鬆開的時候如果滑動速度很快如果是向左滑則切換到下一頁,如果是向右滑則切換到到上一頁,這裡我們需要藉助一個非常重要的工具,速度檢測器:VelocityTracker,通過他來計算滑動的速度大小,如果速度為正則為右滑,當前位置減1,如果為負值為左滑當前位置加1
速度檢測計算要滑動到的頁面下標if(Math.abs(xVelocity) > 50){ // 如果滑動的速度快也跳到下一個位置 mChildIndex = xVelocity > 0 ? mChildIndex - 1:mChildIndex + 1; }
根據拖動的距離來計算要滑動的頁面的下邊
mChildIndex = (scrollX + childWidth / 2) / childWidth;
*根據頁面下標mChildIndex計算將要滑動的距離
//越界處理 mChildIndex = Math.max(0, Math.min(mChildIndex, getChildCount() - 1));//計算索要滑動的距離int delx = mChildIndex * childWidth - scrollX;//彈性滑動開始smoothScrollTo(delx,0);
完整的上下翻頁的程式碼如下:
public boolean onTouchEvent(MotionEvent event) { mVelocityTracker.addMovement(event); boolean consume = false; int x = (int) event.getX(); int y = (int) event.getY(); Log.v(TAG, "onTouchEvent x:" + x + ";y:" + y); switch (event.getAction()) { case MotionEvent.ACTION_UP: //手指抬起的時候,首先要計算的是要滾動到哪個位置上,然後在計算滾動的距離是多少 //3. mVelocityTracker.computeCurrentVelocity(1000); float xVelocity = mVelocityTracker.getXVelocity(); int scrollX = getScrollX(); View child = getChildAt(mChildIndex); int childWidth = child.getMeasuredWidth(); if(Math.abs(xVelocity) > 50){ \t\t// 如果滑動的速度快也跳到下一個位置 mChildIndex = xVelocity > 0 ? mChildIndex - 1:mChildIndex + 1; }else{ //1.如果滑動速度慢且滑動沒有過半兒,應該還在當前位置,.如果已經過半則滑動到下一個位置 mChildIndex = (scrollX + childWidth / 2) / childWidth; }\t\t\t\t//越界處理 mChildIndex = Math.max(0, Math.min(mChildIndex, getChildCount() - 1)); //計算索要滑動的距離 int delx = mChildIndex * childWidth - scrollX; //彈性滑動開始 smoothScrollTo(delx,0); mVelocityTracker.clear(); break; } mLastX = x; mLastY = y; return consume; }
彈性滑動這是具體的彈性滑動的核心模板程式碼,在前面我們已經分析過了,在這裡我就不在重複了
private void smoothScrollTo(int x,int y) { mScroller.startScroll(getScrollX(), getScrollY(), x, y, 500); invalidate(); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } }
截止目前整個自定義ViewPager的彈性滑動的效果就徹底實現了,想必通過這個自定義View的實現,我們對彈性滑動的理解已經非常深刻了。最後我把整個測試程式碼的demo已經上傳到了github上,感興趣的可以下載原始碼檢視 https://github.com/mxdldev/android-custom-view/tree/master/app/src/main/java/com/mxdl/customview/test/view/MyViewPager.java