Android View 的滑動方式
前言
自定義View作為Android進階的基礎,是我們開發者不得不學習的知識,而酷炫的自定義View效果,都離不開View的滑動,所以接下來我們來一起探究View的滑動方式,看看View是如何滑動的,為Android進階的道路打下基礎。
-
View 座標系基本知識
-
瞭解View的滑動方式,首先我們得了解View在什麼位置,我們可以把手機螢幕區域看成是像數學中座標系一樣的區域,只不過是手機螢幕座標系的Y軸和數學中的座標系的Y軸正方向相反
-
確定View的位置主要是根據View的left、top、right、bottom四個屬性來決定,需要注意的是View的這四個屬性是相對於它的父容器來說的,所以對應為left是View的左上角相對於父容器的橫座標,top為縱座標,right為View右下角相對於父容器的橫座標,bottom為縱座標。(具體可以看下方示意圖A)
//獲取view位置的值 left = View.getLeft(); top = View.getTop(); right = View.getRight(); bottom = View.getBottom();
-
除了上面確定View位置的引數,還有x,y,translationX,translationY這四個引數,x和y代表View的左上角的座標值,而translationX,translationY是左上角座標相對於View的父容器的偏移量,預設為零,也就是view不移動,則x和y等於left和top,他們換算關係可看下面示意圖A,在View的滑動過程中,left和top表示的是View原始位置的值,這是不會改變的,所以改變的是滑動偏移量加上原始值得到新的左上角座標。
View座標系和點選事件示意圖.png
-
-
當我們觸控式螢幕幕,則可以通過點選事件來獲取當前點選位置的值和相較於手機螢幕左上角的偏移量座標(如上圖B所示)。
-
Android View 的滑動方式
-
layout方法改變View位置滑動View
-
首先我們看看layout()方法原始碼
@SuppressWarnings({"unchecked"}) public void layout(int l, int t, int r, int b) { ....... if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b); ........ } }
-
瞭解過自定義View的各位應該都知道,onLayout()是View繪製過程中的一個方法,可以通過它確定View的位置,也就是說我們通過layout()方法可以改變View的位置,下面我們通過onLayout方法做一個可以隨意滑動 view的例子
@Override public boolean onTouchEvent(MotionEvent event) { //獲取觸屏時候的座標 Log.e("毛麒添","getLeft:"+getLeft()+"getTop:"+getTop()+"getRight:"+getRight()+"getBottom:"+getBottom()); x = event.getRawX(); y = event.getRawY(); switch (event.getAction()){ case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: //手指移動偏移量 int offsetX = (int) (x-lastX); int offsetY = (int) (y-lastY); layout(getLeft()+offsetX,getTop()+offsetY,getRight()+offsetX,getBottom()+offsetY); break; case MotionEvent.ACTION_UP: Log.e("毛麒添","getLeft:"+getLeft()+"getTop:"+getTop()+"getRight:"+getRight()+"getBottom:"+getBottom()); break; } lastX=x; lastY=y; return super.onTouchEvent(event); }
- 通過列印left,top,right,bottom數值可以發現layout方法是真實改變了View的位置而不只是View的內容。
-
-

layout1.gif
-
offsetLeftAndRight()與offsetTopAndBottom() 方法改變View的位置讓其滑動
- 修改上面的方法,效果圖和onLayout一樣,同時offsetLeftAndRight()與offsetTopAndBottom()方法也是真實改變了View的位置而不只是View的內容。
case MotionEvent.ACTION_MOVE: //手指移動偏移量 int offsetX = (int) (x-lastX); int offsetY = (int) (y-lastY); offsetLeftAndRight(offsetX); offsetTopAndBottom(offsetY); break;
-
使用scrollTo()和scrollBy()滑動View
- scrollTo()和scrollBy()是View提供的滑動方法,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和mScrollY的規則我們需要明白: scrollTo()中mScrollX的值等於view左邊緣和view內容左邊緣在水平方向的距離,並且當view的左邊緣在view的內容左邊緣右邊時,mScrollX為正,反之為負;同理mScrollY等於view上邊緣和view內容上邊緣在豎直方向的距離,並且當view的上邊緣在view的內容上邊緣下邊時,mScrollY為正,反之為負 。當View沒有使用scrollTo()和scrollBy()進行滑動的時候,mScrollX和mScrollY預設等於零,也就是view的左邊緣與內容左邊緣重合。
- 根據上面的規則,我們假設將view內容右下滑動,得到下圖

mScrollX和mScrollY值的判斷.png
- 結合上面的知識,我們將上面滑動的例子改寫一下,如果使用scrollTo()則只是滑動到我們手指滑動偏移量的距離的點,達不到要求,而scrollBy()是在scrollTo()的基礎上偏移滑動的位置,正好符合我們自由滑動的要求,並且根據上面的分析mScrollX和mScrollY為負值,則滑動偏移也應該為負值才能達到我們想要的自由滑動效果(這個大家需要自己好好想明白可能才會更加清楚理解) ``` case MotionEvent.ACTION_MOVE: //手指移動偏移量 int offsetX = (int) (x-lastX); int offsetY = (int) (y-lastY); //滑動方式1 ((View)getParent()).scrollBy(-offsetX,-offsetY); break; ```
- 根據滑動列印的日誌我們可以看出,scrollBy()和scrollTo()在滑動的過程中只是改變了View內容的位置,而沒有改變初始的left,right,top,bottom的值

view的位置沒有發生改變.png
-
使用動畫讓View滑動
-
xml補間動畫的方式讓View滑動
- 定義一個xml檔案,500ms移動到500,500的位置並保持位置
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" android:fillAfter="true" android:duration="500" > <translate android:fromXDelta="0" android:fromYDelta="0" android:toXDelta="500" android:toYDelta="500" /> </set>
- 程式碼呼叫
startAnimation(AnimationUtils.loadAnimation(mContext, R.anim.testscroll));
- 補間對View的滑動也只是改變了View的顯示效果,不會對View的屬性做真正的改變,也就是說補間動畫也沒有真正改變View的位置
-
屬性動畫讓View滑動
- 自從Android3.0開始加入了屬性動畫(瞭解屬性動畫可以檢視 ofollow,noindex">郭霖大佬部落格 ),屬性動畫不僅能作用於View產生動畫效果,也能作用於其他屬性來產生動畫效果,可以說屬性動畫相較於補間動畫是非常靈活的,並且屬性動畫是真正改變View的位置屬性。
- 屬性動畫一般我們使用ObjectAnimator,讓View2秒時間水平平移到300位置,並且移動完後我們點選View看還能響應點選事件(如下圖所示)
ObjectAnimator.ofFloat(testScroll,"translationX",0,300).setDuration(2000).start();
-

objectAnimator.gif
- 改變佈局引數 LayoutParams 滑動View - 平常我們開發設定View的位置可以在xml中設定,也可以在程式碼中設定。LayoutParams有一個View的所有佈局引數資訊,所有我們可以通過設定View的LayoutParams引數的leftMargin和topMargin達到上面自由滑動View的效果。 ``` ...... case MotionEvent.ACTION_MOVE: //手指移動偏移量 int offsetX = (int) (x-lastX); int offsetY = (int) (y-lastY); //滑動方式5 moveView(offsetX,offsetY); break; ...... private void moveView(int offsetX, int offsetY) { ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams(); layoutParams.leftMargin = getLeft() + offsetX; layoutParams.topMargin = getTop() + offsetY; setLayoutParams(layoutParams); } ```
-
既然佈局引數已經改變,則View的實際位置肯定也已經改變。
-
Scroller彈性滑動
-
首先我們要明白什麼是Scroller?
- Scroller是彈性滑動的幫助類,它本身並不能實現View的彈性滑動,它必須要配合scrollTo()或scrollBy()和實現View的computeScroll的方法才能實現View的彈性滑動
- Scroller實現彈性滑動的典型例子
Scroller mScroller=new Scroller(context); public void smoothScrollTo(int desx,int desy){ int scaleX = (int) getScaleX(); int scaleY = (int) getScaleY(); int deltaX = desx-scaleX; int deltaY = desy-scaleY; //3秒內彈性滑到desx desy 位置 mScroller.startScroll(scaleX,scaleY,deltaX,deltaY,3000); //重新繪製介面 會呼叫computeScroll方法 invalidate(); } @Override public void computeScroll() { super.computeScroll(); if(mScroller.computeScrollOffset()){//還沒滑動到指定位置 ((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); postInvalidate(); } } //拿到自定View的例項物件呼叫smoothScrollTo實現右下方向3秒 //內到指定位置的彈性滑動 //為什麼是-300 請看scrollTo()或scrollBy()滑動解析 testScroll.smoothScrollTo(-300,-300);
-
下面我們從原始碼角度來分析一下Scroller是如何實現彈性滑動的
- 先看startScroll()方法
/** * Start scrolling by providing a starting point, the distance to travel, * and the duration of the scroll. * * @param startX Starting horizontal scroll offset in pixels. Positive *numbers will scroll the content to the left. * @param startY Starting vertical scroll offset in pixels. Positive numbers *will scroll the content up. * @param dx Horizontal distance to travel. Positive numbers will scroll the *content to the left. * @param dy Vertical distance to travel. Positive numbers will scroll the *content up. * @param duration Duration of the scroll in milliseconds. */ 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; } //View 中 computeScroll()方法沒有實現內容,需要子View 自行實現 /** * Called by a parent to request that a child update its values for mScrollX * and mScrollY if necessary. This will typically be done if the child is * animating a scroll using a {@link android.widget.Scroller Scroller} * object. */ public void computeScroll() { }
- 通過原始碼我們看到startScroll()方法只是傳遞了我們傳入的引數,滑動的起點startX、startY,滑動的距離dx、dy,和彈性滑動的時間,沒看到有滑動的操作,那Scroller是如何讓View滑動呢?而答案就是我們再呼叫startScroll()方法之後又呼叫了invalidate()方法,該方法會引起view的重繪,而View的重繪會呼叫computeScroll()方法,通過上面的原始碼,我們知道computeScroll()方法在view中是空實現,所以我們自己實現該放法的時候則呼叫scrollTo方法獲取scrollX和scrollY當前讓view進行滑動,但是這只是滑動一段距離,好像還沒有彈性滑動,別急,我們看看Scroller的computeScrollOffset()方法
/** * Call this when you want to know the new location.If it returns true, * the animation is not yet finished. */ public boolean computeScrollOffset() { if (mFinished) { return false; } int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); if (timePassed < mDuration) { switch (mMode) { case SCROLL_MODE: final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); mCurrX = mStartX + Math.round(x * mDeltaX); mCurrY = mStartY + Math.round(x * mDeltaY); break; ....... } } else { ....... } return true; }
- 通過computeScrollOffset()的原始碼,我們已經可以一目瞭然,根據時間流逝的百分比算出scrollX和scrollY改變的百分比並計算出他們的值,類似動畫中的插值器的概念。每次重繪緩慢滑動一段距離,在一段時間內緩慢滑動就成了彈性滑動,就比scrollTo方法的一下滑動完舒服多了,我們還需要注意computeScrollOffset()的返回值,如果返回false表示滑動完了,true則表示沒有滑動完。
- 這裡我們梳理一下Scroller實現彈性滑動的工作原理:Scroller必須要配合scrollTo()或scrollBy()和實現View的computeScroll的方法才能實現View的彈性滑動,invalidate()引發第一次重繪,重繪距離滑動開始時間有一個時間間隔,在這個時間間隔中獲取View滑動的位置,通過scrollTo()進行滑動,滑動完postInvalidate()再次進行重繪,沒有滑動完則繼續上面的操作,最終組成彈性滑動。
-
到此,View的滑動方式就已經瞭解完了。如果文章中有寫得不對的地方,請給我留言指出,大家一起學習進步。如果覺得我的文章給予你幫助,也請給我一個喜歡和關注。
-
參考連結
-
參考書籍
- 《Android開發藝術探索》
- 《Android進階之光》