View的滑動方式
View的滑動是Android自定義控制元件的基礎,同時在開發中我們也難免會遇到View的滑動處理。其實不管是哪種滑動方式,其基本思想都是類似的:當點選事件傳到View時,系統記下觸控點的座標,手指移動時系統記下移動後觸控的座標並算出偏移量,並通過偏移量來修改View的座標。
一、座標系
Android系統中有兩種座標系,分別為Android座標系和View座標系。瞭解這兩種座標系能夠幫助我們實現View的各種操作,比如我們要實現View的滑動,必須要知道這個View的位置,才能去操作,首先我們來看看Android座標系。
1.Android座標系
在Android中,將螢幕左上角的頂點作為Android座標系的原點,這個原點向右是X軸正方向,向下是Y 軸正方向。另外在觸控事件中,使用getRawX()和getRawY()方法獲得的座標也是 Android座標系的座標。
2.View座標系
View座標系以當前控制元件左上角為座標原點,向左為 X 軸正方向,向下為 Y 軸正方向,MotionEvent 的 getX()、getY() 方法獲取的是點選位置在檢視座標系中的座標,View 的 mLeft、mTop 等屬性也是 View 在父控制元件的檢視座標系中的座標。它與Android座標系並不衝突,兩者是共同存在的,它們一起來幫助開發者更好地控制View。

座標系
二、滑動原理
View 的滑動原理,其實滑動的原理與動畫效果的實現非常相似,都是通過不斷改變 View 的座標來實現這一效果。所以要實現滑動效果就必須要監聽使用者的觸控事件,並根據事件傳入的座標,動態且不斷的改變 View 的座標,從而實現 View 跟隨使用者觸控的滑動而滑動。
三、滑動方式
實現View滑動有很多種 方法,在這裡主要講解6種滑動方法,分別是layout()、offsetLeftAndRight()與 offsetTopAndBottom()、LayoutParams、動畫、scollTo 與 scollBy,以及Scroller。
1.layout()
View進行繪製的時候會呼叫onLayout()方法來設定顯示的位置,因此我們同樣也可以通過修改View 的left、top、right、bottom這4種屬性來控制View的位置。
首先我們要自定義一個View,在 onTouchEvent()方法中獲取觸控點的座標,程式碼如下所示:
public class CustomView extends View { int lastX; int lastY; public CustomView(Context context) { super(context); } public CustomView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean onTouchEvent(MotionEvent event) { // 記錄觸控點座標 int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //當按下的時候執行 lastX = x; lastY = y; break; case MotionEvent.ACTION_MOVE: //當移動的時候執行 // 計算偏移量 int offsetX = x - lastX; int offsetY = y - lastY; // 在當前left、top、right、bottom的基礎上加上偏移量來控制View的位置 layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY); break; case MotionEvent.ACTION_UP: //當抬起的時候執行 break; } return true; } }
我們需要自定義一個CustomView 繼承自View,需要重寫onTouchEvent()方法。
在MotionEvent.ACTION_DOWN事件中獲取當前觸控點的座標位置,然後在MotionEvent.ACTION_MOVE事件中計算偏移量,再呼叫layout()方法重新放置這個CustomView的位置即可。在每次移動時都會呼叫layout()方法對螢幕重新佈局,從而達到移動View的效果。
在佈局檔案中引用CustomView即可:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="20dp" android:orientation="vertical" tools:context=".ui.CustomActivity"> <com.example.monkey.myapplication.view.CustomView android:layout_width="50dp" android:layout_height="50dp" android:background="@color/colorPrimary" /> </LinearLayout>
具體效果,可以自己動手試試。
2.offsetLeftAndRight()與 offsetTopAndBottom()
這兩種方法和layout()方法的效果差不多,其使用方式也差不多。我們將ACTION_MOVE中的程式碼替 換成如下程式碼:
case MotionEvent.ACTION_MOVE: //當移動的時候執行 // 計算偏移量 int offsetX = x - lastX; int offsetY = y - lastY; //// 在當前left、top、right、bottom的基礎上加上偏移量來控制View的位置 //layout(getLeft() + offsetX, //getTop() + offsetY, //getRight() + offsetX, //getBottom() + offsetY); //左右偏移 offsetLeftAndRight(offsetX); //上下偏移 offsetTopAndBottom(offsetY); break;
3.LayoutParams
LayoutParams主要儲存了一個View的佈局引數,因此我們可以通過LayoutParams來改變View的佈局參 數從而達到改變View位置的效果。同樣,我們將 ACTION_MOVE中的程式碼替換成如下程式碼:
case MotionEvent.ACTION_MOVE: //當移動的時候執行 // 計算偏移量 int offsetX = x - lastX; int offsetY = y - lastY; LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams(); layoutParams.leftMargin = getLeft() + offsetX; layoutParams.topMargin = getTop() + offsetY; setLayoutParams(layoutParams); break;
前面我們的佈局檔案,因為父控制元件是 LinearLayout,所以我們用了 LinearLayout.LayoutParams。如果父控制元件是RelativeLayout, 則要使用RelativeLayout.LayoutParams。否則會報錯
java.lang.ClassCastException: android.widget.LinearLayout$LayoutParams cannot be cast to android.widget.RelativeLayout$LayoutParams at com.example.monkey.myapplication.view.CustomView.onTouchEvent(CustomView.java:60) at android.view.View.dispatchTouchEvent(View.java:11788)
當然除了使用佈局的LayoutParams外,我們還可以用 ViewGroup.MarginLayoutParams來實現。因為LinearLayout和RelativeLayout都是ViewGroup的子類。
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams(); layoutParams.leftMargin = getLeft() + offsetX; layoutParams.topMargin = getTop() + offsetY; setLayoutParams(layoutParams);
4.scollTo 與 scollBy
ScrollTo(dx,dy)指移動到一個具體的座標點(dx,dy),而ScrollBy(dx,dy)則表示移動的增量為dx,dy。我們將 ACTION_MOVE中的程式碼替換成如下程式碼:
case MotionEvent.ACTION_MOVE: //當移動的時候執行 // 計算偏移量 int offsetX = x - lastX; int offsetY = y - lastY; ((View) getParent()).scrollBy(-offsetX, -offsetY); break;
首先scrollBy移動的是View的內容content,而不是View本身,如TextView的content為文字,ImageView的content為drawable,而ViewGroup的content是View或是ViewGroup,所以要移動當前View本身,我們就需要通過它的ViewGroup改變自己的內容從而改變View本身的位置。其次,我們真正操作的是View的父控制元件ViewGroup,要讓View往左(上/右/下)移,應該要讓ViewGroup往相反方向移動,也就是右(下/左/上),即偏移量就是相反的(負的)。所以要實現 CustomView 隨手指移動的效果,就需要將偏移量設定為負值。若是正數,則會向相反的方向移動。
我們通過ScrollTo和ScrollBy的原始碼看下其區別:
/** * Set the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int, int, int, int)} and the view will be * invalidated. * @param x the x position to scroll to * @param y the y position to scroll to */ 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();//重新繪製 } } } /** * Move the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int, int, int, int)} and the view will be * invalidated. * @param x the amount of pixels to scroll by horizontally * @param y the amount of pixels to scroll by vertically */ public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); }
通過原始碼可以看到scrollBy()裡面呼叫了ScrollTo(),傳入的引數是mScrollX+x,也就是說這次x是一個增量,所以scrollBy實現的效果就是,在當前位置上,再偏移x距離 。這是ScrollTo()和ScrollBy()的重要區別。
- scrollTo與scrollBy都會使View立即重繪,所以移動是瞬間發生的
- scrollTo(x,y):指哪打哪,效果為View的左上角滾動到(x,y)位置,但由於View相對與父View是靜止的所以最終轉換為相對的View的內容滑動到(-x,-y)的位置。
- scrollBy(x,y): 此時的x,y為偏移量,既在原有的基礎上再次滾動
5.Scroller
通過上面的學習我們知道scrollTo與scrollBy可以實現滑動的效果,但是滑動的效果都是瞬間完成的,在事件執行的時候平移就已經完成了,這樣的效果會讓人感覺突兀,Google建議使用自然過渡的動畫來實現移動效果。因此,Scroller類這樣應運而生了。Scroller本身是不能實現View的滑動的,它需要與View的computeScroll()方法配合才能實現彈性滑動的效果。
public class CustomView extends View { int lastX; int lastY; Scroller mScroller; public CustomView(Context context) { this(context,null); } public CustomView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mScroller = new Scroller(context); } @Override public boolean onTouchEvent(MotionEvent event) { // 記錄觸控點座標 int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //當按下的時候執行 lastX = x; lastY = y; break; case MotionEvent.ACTION_MOVE: //當移動的時候執行 // 計算偏移量 int offsetX = x - lastX; int offsetY = y - lastY; smoothScrollBy(-offsetX,-offsetY); break; case MotionEvent.ACTION_UP: //當抬起的時候執行 break; } return true; } public void smoothScrollBy(int dx,int dy){ mScroller.startScroll(mScroller.getFinalX(),mScroller.getFinalY(),dx,dy,2000); invalidate(); // 必須呼叫改方法通知View重繪以便computeScroll方法被呼叫。 } @Override public void computeScroll() { super.computeScroll(); // 判斷Scroller滑動是否執行完畢 if (mScroller.computeScrollOffset()) { ((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); // 通過重繪讓系統呼叫onDraw,onDraw中又會呼叫computeScroll,如此不斷迴圈,直到Scroller執行完畢 invalidate(); } }
Android為View的滑動提供了Scroller輔助類,它本身並不能導致View滑動,需要藉助computeScroll和ScrollTo方法完成View的滑動。使用Scroller類完成View的平滑。
- 首先要建立Scroller類。
- 然後重寫computeScroll方法,這裡需要注意的是computeScroll方法在onDraw中會被呼叫,因此需要呼叫invalidate方法通知View呼叫onDraw重繪,然後再呼叫computeScroll完成View的滑動,過程為invalidate->onDraw->computeScroll->invalidate->…,無限迴圈直到mScroller的computeScrollOffset返回false,也就是滑動完成。
- 呼叫Scroller類的startScroll方法開啟滾動過程。
6.動畫
使用動畫來實現View的滑動主要通過改變View的translationX和translationY引數來實現,使用動畫的好處在於滑動效果是平滑的。這裡我們使用屬性動畫來移動view,我們讓 CustomView在5000ms內沿著X軸向右平移300畫素,具體實現如下:
CustomView customview =findViewById(R.id.customview); ObjectAnimator.ofFloat(customview,"translationX",0,300).setDuration(5000).start();
也可以使用補間動畫實現,在這裡就不做多介紹了。
本文到這裡就結束了,如果有不對的地方,還望指正。
參考資料
《Android進階之光》