詳解Android中實現View滑動的幾種方式

jxjl8299 8年前發布 | 6K 次閱讀 安卓開發 Android開發 移動開發

關于View我們需要知道的

什么是View

Android中的View類是所有UI控件的基類(base class),我們平時所有到的各種UI控件,比如Button、ImagView等都繼承自View類。LinearLayout、FrameLayout等布局管理器的直接父類是ViewGroup,而ViewGroup也由View類派生。總的來說,View是對UI控件的抽象,它代表了屏幕上的一個矩形區域。通過繼承View,并重寫相應方法,我們就能夠實現具有各種外觀及行為的UI控件。Button等控件我們之所以能夠直接拿來即用,是因為Google已經幫我們完成了繼承View并重寫相應方法的工作。

View的位置

View在屏幕上的位置由它的以下四個參數所決定:

  • top:View的上邊緣與父View的上邊緣的距離,對應著View類中的成員變量mTop,可由getTop()方法獲得;
  • left:View的左邊緣與父View的左邊緣的距離,對應著View類中的成員變量mLeft,可由getLeft方法獲得;
  • bottom:View的下邊緣與父View的下邊緣的距離,對應著View類中的成員變量mBottom,可由getBottom方法獲得;
  • right:View的右邊緣與父View的右邊緣的距離,對應著View類中的成員變量mRight,可由getRight方法獲得。

一圖勝千言:

有了這四個參數,計算View的寬高就很容易了:width = right - left;height = bottom - top。

關于View還有幾個參數需要我們注意:

  • translationX:代表View平移的水平距離;
  • translationY:代表View平移的垂直距離;
  • x、y分別為View的左上角的橫縱坐標。

View若經過了平移,改變的是它的x、y(代表當前View的左上角位置),它的四個位置參數代表了View的原始位置信息,是始終不變的。View在平移的過程中始終滿足如下關系:

x = left + translationX
y = top + translationY

實現View滑動的幾種方式

我們在使用View的過程中,經常需要實現View的滑動效果。比如ListView、跟隨手指移動的自定義View等等,前者的滑動效果是SDK為我們提供的,而對于我們自定義View的滑動效果就需要自己來實現。下面來詳細介紹一下實現View滑動的幾種方式。

使用scrollTo/scrollBy實現View的滑動

實現滑動的最樸素直接的方式就是使用View類自帶的scrollTo/scrollBy方法了。scrollBy方法是滑動指定的位移量,而scrollTo方法是滑動到指定位置。這兩個方法的源碼如下:

/**

  • 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; onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { invalidate(); } }
    }

/**

  • 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); }</code></pre>

    通過以上代碼,我們可以看到,scrollBy方法內部也是調用了scrollTo方法來實現其功能的。以上源碼中我們注意到了mScrollX和mScrollY成員變量,前者是View的左邊緣減去View的內容的左邊緣,后者是View的上邊緣減去View的內容的上邊緣。示意圖如下:

    上圖中,黑色邊框代表View在屏幕上對應的矩形區域,藍色邊框代表View的內容。在上圖中,我們調用scrollTo/scrollBy把View向右滾動了一定距離。實際上實現的是View的內容的滾動,而View的四個位置參數是保持不變的。想一下我們平常使用ListView時,滾動的就是ListView的內容,而ListView本身在屏幕上的位置是不變的。上圖中,黑色左邊緣(即View的左邊緣)減去藍色左邊緣(即View的內容的左邊緣)即可得到mScrollX。由此我們還可以知道,向右滾動時mScrollX負的,向左滾動時mScrollX是正的。同理我們可以知道,向下滾動時,mScrollY是負的,向上滾動時,mScrollY是正的。

    經過以上的分析,我們了解到使用scrollTo/scrollBy方法實現View的滑動是很簡單直接的,那么簡單的背后有什么代價呢?代價就是滑動不是“彈性的”,彈性滑動指的是View的滑動應該是一個先加速再逐漸減速到停止的過程,這樣看起來很平滑,不會很突兀。scrollTo/scrollBy方法實現的滑動看起來就會很突兀,這樣的用戶體驗很不好。在解決這個問題之前,我們先來看看實現View滑動的其他方法。

    使用動畫來實現View的滑動

    使用動畫來實現View的滑動主要通過改變View的translationX和translationY參數來實現,使用動畫的好處在于滑動效果是平滑的。上面我們提到過,View的x、y參數決定View的當前位置,通過改變translationX和translationY,我們就可以改變View的當前位置。我們可以使用屬性動畫或者補間動畫來實現View的平移。

    首先,我們先來看一下如何使用補間動畫來實現View的平移。補間動畫資源定義如下(anim.xml):

    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android" android:fillAfter="true">
    <translate android:duration="100" 
    android:fromXDelta="0" 
    android:fromYDelta="0" 
    android:interpolator="@android:anim/linear_interpolator" 
    android:toXDelta="100" 
    android:toYDelta="100"/>
    </set>

    然后我們就可以通過以下代碼實現目標View(targetView)的滑動:

    final Animation anim = AnimationUtils.loadAnimation(this, R.anim.anim);
    targetView.startAnimation(anim);

    使用補間動畫實現View的滑動有一個缺陷,那就是移動的知識View的“影像”,這意味著其實View并未真正的移動,只是我們看起來它移動了而已。拿Button來舉例,假若我們通過補間動畫移動了一個Button,我們會發現,在Button的原來位置點擊屏幕會出發點擊事件,而在移動后的Button上點擊不會觸發點擊事件。

    接下來,我們看看如何用屬性動畫來實現View的平移。使用屬性動畫實現View的平移更加簡單,只需要以下一條語句:

    ObjectAnimator.ofFloat(targetView, "translationX", 0, 100).setDuration(100).start();

    以上代碼即實現了使用屬性動畫把targetView在100ms內向右平移100px。使用屬性動畫的限制在于真正的屬性動畫只可以在Android 3.0+使用(一些第三方庫實現的兼容低版本的屬性動畫不是真正的屬性動畫),優點就是它可以真正的移動View而不是僅僅移動View的影像。

    經過以上的描述,使用屬性動畫實現View的滑動看起來是個不錯的選擇,而且一些View的復雜的滑動效果只有通過動畫才能比較方便的實現。

    通過改變布局參數來實現View的滑動

    通過改變布局參數來實現View的滑動的思想很簡單:比如向右移動一個View,只需要把它的marginLeft參數增大,向其它方向移動同理,只需改變相應的margin參數。還有一種比較拐彎抹角的方法是在要移動的View的旁邊預先放一個View(初始寬高設為0)。然后比如我們要向右移動View,只需把預先放置的那個View的寬度增大,這樣就把View“擠”到右邊了。代碼示例如下:

    MarginLayoutParams params = (MarginLayoutParams) mButton.getLayoutParams();
    params.leftMargin += 100;
    // 請求重新對View進行measure、layout
    mButton.requestLayout();

    以上代碼即實現了把mButton向右滑動100px。通過改變布局參數來實現的滑動效果也不是平滑的。

    使用Scroller來實現彈性滑動

    上面我們提到了使用scrollTo/scrollBy方法實現View的滑動效果不是平滑的,好消息是我們可以使用Scroller方法來輔助實現View的彈性滑動。使用Scroller實現彈性滑動的慣用代碼如下:

    Scroller scroller = new Scroller(mContext); 

private void smoothScrollTo(int dstX, int dstY) { int scrollX = getScrollX(); int scrollY = getScrollY(); int deltaX = dstX - scrollX; int deltaY = dstY - scrollY; scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000); invalidate(); }

@Override public void computeScroll() { if (scroller.computeScrollOffset()) { scrollTo(scroller.getCurrX(), scroller.getCurY()); // 用于在非UI線程中更新用戶界面 postInvalidate(); } }</code></pre>

我們來看一下以上的代碼。首先我們獲取到View的mScrollX參數并存到scrollX變量中,獲取mScrollY并保存到scrollY變量中。然后計算要滑動的位移量,接著調用了scroller.startScroll()方法,我們來看看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; 
  . . .
}

從以上的源碼我們可以看到,startScroll方法中并沒有進行實際的滾動操作,而是把startX、startY、dx、dy等參數都保存了下來。那么究竟怎么實現View的滑動的呢?

我們先回到Scroller慣用代碼。可以看到smoothScrollTo()方法中調用了invalidate方法,這個方法會請求重繪View,這會導致View的draw()的方法被調用,而draw()方法內部會調用computeScroll()方法。因此我們重寫了computeScroll()方法,在其內部調用了scrollTo()方法,并傳入mScroller.getCurrX()和mScroller.getCurrY()方法作為參數,這兩個方法會分別獲取到mCurrX和mCurrY成員變量。這兩個成員變量會在computeScrollOffset()方法中被賦值,我們一起來看下相關代碼:

public boolean computeScrollOffset() { 
   ... 
   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.rounc(y * mDeltaY);
         break;
         ...
       }
    }
    return true;
}

mCurrX和mCurrY表示本次滑動的目標位置。computeScrollOffset()方法返回true表示滑動過程還未結束,返回false則表示滑動結束。

通過以上的分析,我們大概了解了Scroller實現彈性滑動的原理:

invaldate()方法會導致View的draw()方法被調用,而draw()方法會調用computeScroll()方法,因此重寫了computeScroll()方法,而computeScrollOffset()方法會根據時間的流逝動態的計算出很小的一段時間應該滑動多少距離。也就是把一次滑動拆分成無數次小距離滑動從而實現“彈性滑動”。

 

 

來自:http://www.jianshu.com/p/e8306ab5c1b6

 

 本文由用戶 jxjl8299 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!