本文出自門心叼龍的頭條号,屬于原創内容,轉載請注明出處。
我們知道,在功能機時代我們在手機上的任何操作都是在鍵盤上完成的,隻有通過鍵盤才能完成輸入操作,隻能通過鍵盤才能和手機互動,進入智能機時代以後我們所有操作都可以通過觸摸屏的方式來完成,而我們最常見的操作就是滑動,手機螢幕和PC端的顯示屏最大的差別就是,PC顯示器螢幕很大,一屏可以顯示跟多内容,而手機螢幕就小了很多,一螢幕所能顯示的内容就非常有限,我們可以通過上下滑動,左右滑動翻頁來顯示我們想要看到的内容。我們打開任意一款手機應用,無處不在的上滑,下滑,左滑,右滑操作,由此可見滑動操作在移動手機開發當中是多麼的重要,是以今天我們來研究View的滑動。
在Android系統中View給我們提供了兩個非常重要關于滑動操作的方法scrollTo和scrollBy,下面我們通過scrollTo和scrollBy來完成View的滑動。
View的滑動
布局檔案如下:
布局檔案中有兩個控件,一個TextView和一個Button,我們點選按鈕Button調用TextView的scrollTo方法和scrollBy方法,來觀察View滾動的效果。
mBtnScroll.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View v) { mTxtScroll.scrollTo(200,200); } });
此時Hello world往上方進行了移動,再次點選按鈕調用 mTxtScroll.scrollTo(200,200),發現HelloWorld的位置沒有發生任何的變化。
接下來我們把調用參數修改為-200,即:
mTxtScroll.scrollTo(-200,-200);
再看看效果,HelloWorld往右下方移動,scrollTo測試完畢,我們在看看scrollBy是什麼效果
mBtnScroll.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View v) { mTxtScroll.scrollBy(200,200); } });
我連續點選了三次,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"?>
确定子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){ // 如果滑動的速度快也跳到下一個位置 mChildIndex = xVelocity > 0 ? mChildIndex - 1:mChildIndex + 1; }else{ //1.如果滑動速度慢且滑動沒有過半兒,應該還在目前位置,.如果已經過半則滑動到下一個位置 mChildIndex = (scrollX + childWidth / 2) / childWidth; }//越界處理 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