Android開發藝術筆記 | View的滑動(三種普遍實現方式及其對比、實戰)

  • 2020 年 3 月 26 日
  • 筆記

滑動在Android開發中具有很重要的作用, 不管一些滑動效果多麼絢麗, 歸根結底,它們都是由不同的滑動外加一些特效所組成的。 因此,掌握滑動的方法是實現絢麗的自定義控制項的基礎。

  • 常見的實現View的滑動的三種方式: 第一種是通過View本身提供的scrollTo/scrollBy方法來實現滑動; 第二種是通過動畫給View施加平移效果來實現滑動; 第三種是通過改變View的LayoutParams使得View重新布局從而實現滑動。

The 1. 使用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方法, 它實現了基於當前位置的相對滑動, 而scrollTo則實現了基於所傳遞參數的絕對滑動。
  • 這裡要注意 滑動過程中View內部的兩個屬性mScrollX和mScrollY的改變規則, 這兩個屬性可以通過getScrollX和getScrollY方法分別得到。
  • 在滑動過程中, mScrollX的值總是 等於View左邊緣和View內容左邊緣在水平方向的距離, (即 mScrollX = View左邊緣的X值 - View內容左邊緣的X值) 而mScrollY的值總是 等於View上邊緣和View內容上邊緣在豎直方向的距離。 (即 mScrollY = View上邊緣的Y值 - View內容上邊緣的Y值)
  • View邊緣是指View的位置,由四個頂點組成, 而View內容邊緣是指View中的內容的邊緣, 【上一篇筆記(事件體系) 說了,頂點即View的位置資訊, 不改變View的布局參數LayoutParams,它們的值便不會變動!!! 所以,這裡的View邊緣 即View在繪製時layout階段確定了的原始布局位置!!!】
  • scrollTo和scrollBy只能改變View內容的位置 即,本方式實現的是View 內容的滑動!!! 而不能改變View 本身在布局中的位置和頂點坐標!!! 也就是說,不管怎麼滑動, 也不可能將當前View滑動到附近View所在的區域;
  • mScrollX和mScrollY的單位為像素, 並且當View左邊緣在View內容左邊緣的右邊時,mScrollX為正值, 反之為負值; 當View上邊緣在View內容上邊緣的下邊時,mScrollY為正值, 反之為負值。 換句話說, 如果從左向右滑動,那麼mScrollX為負值,反之為正值; 如果從上往下滑動,那麼mScrollY為負值,反之為正值。

綠色邊框代表View在螢幕上對應的矩形區域,灰色陰影代表View的內容

The 2. 使用動畫

  • 通過動畫可以讓一個View進行平移,而平移就是一種滑動。
  • 主要是操作View的translationX和translationY屬性, 既可以採用傳統的View動畫,也可以採用屬性動畫;
  • 如果採用屬性動畫的話, 為了能夠兼容3.0以下的版本,需要採用開源動畫庫nineoldandroids(http://nineoldandroids.com/)。
  • View動畫實例,在100ms內將一個View從原始位置向右下角移動100個像素:
    <?xml version="1.0" encoding="utf-8"?>      <set xmlns:android="http://schemas.android.com/apk/res/android"           android:fillAfter="true"           android:zAdjustment="normal" >           <translate               android:duration="100"               android:fromXDelta="0"               android:fromYDelta="0"               android:interpolator="@android:anim/linear_interpolator"               android:toXDelta="100"               android:toYDelta="100" />      </set>
  • 屬性動畫示例,將一個View在100ms內從原始位置向右平移100像素。
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
  • 注意,
    • View動畫是對View的影像做操作, 並不能真正改變View的位置參數,包括寬/高, 並且如果希望動畫後的狀態得以保留還必須將fillAfter屬性設置為true, 否則動畫完成後動畫結果會消失。
    • 使用屬性動畫並不會存在上述問題, 但是在Android 3.0以下無法使用屬性動畫, 需使用動畫兼容庫nineoldandroids來實現屬性動畫, 不過, 在Android 3.0以下的手機上通過nineoldandroids來實現的屬性動畫 本質上仍然是View動畫。
  • 同時View動畫還有一個很嚴重的問題, 比如我們通過View動畫將一個Button向右移動100px, 並且這個View設置的有單擊事件, 這樣子單擊新位置無法觸發onClick事件, 而單擊原始位置仍然可以觸發onClick事件, 儘管Button已經不在原始位置了。 它的位置資訊(四個頂點和寬/高)並不會隨著動畫而改變, 因此在系統眼裡,這個Button並沒有發生任何改變, 它的真身仍然在原始位置, 在新位置上只是View的影像而已。 基於這一點, 我們不能簡單地給一個View做平移動畫 並且還希望它在新位置繼續觸發事件。

The 3. 改變布局參數

  • 改變布局參數,即改變LayoutParams,使View重新布局。 比如我們想把一個Button向右平移100px, 我們只需要將這個Button的LayoutParams里的marginLeft參數的值增加100px即可, 示例程式碼:
MarginLayoutParams params = (MarginLayoutParams)mButton1.getLayoutParams();  params.width += 100;  params.leftMargin += 100;  mButton1.requestLayout();//或mButton1.setLayoutParams(params);

各種滑動效果實現方式的對比

The 1. scrollTo/scrollBy:

  • View提供的原生方法,專門用於View的滑動,
  • 操作簡單方便,
  • 不影響內部元素的單擊事件。
  • 缺點:它只能滑動View的內容,並不能滑動View本身。

The 2. 動畫:

  • 使用動畫實現View的滑動,要分情況。
    • Android 3.0以上並採用屬性動畫的方式, 沒有明顯的缺點; 適用於需具有交互性的View;
    • 使用View動畫或者在Android 3.0以下使用屬性動畫, 則均不能改變View本身的屬性。 適用於View不需要響應交互的情況, 需要交互則不太適合。
  • 優點:一些複雜的效果必須要通過動畫才能實現。

The 3. 改變布局參數

  • 適用於需具有交互性的View;
  • 缺點:操作稍微複雜;

實戰

  • 一三種方式, 我們使用自定義View的方式來實現View的滑動, 主要是配置一下背景顏色,以及在onTouchEvent()中做一下事件處理; 這裡貼上關鍵程式碼,詳細請見GitHub項目地址;

The 1. scrollTo/scrollBy

public class DragView4 extends View {        private int lastX;      private int lastY;        public DragView4(Context context) {          super(context);          ininView();      }        public DragView4(Context context, AttributeSet attrs) {          super(context, attrs);          ininView();      }        public DragView4(Context context, AttributeSet attrs, int defStyleAttr) {          super(context, attrs, defStyleAttr);          ininView();      }        private void ininView() {          setBackgroundColor(Color.BLUE);      }        @Override      public boolean onTouchEvent(MotionEvent event) {          int x = (int) event.getX();          int y = (int) event.getY();          switch (event.getAction()) {              case MotionEvent.ACTION_DOWN:                  lastX = (int) event.getX();                  lastY = (int) event.getY();                  break;              case MotionEvent.ACTION_MOVE:                  int offsetX = x - lastX;                  int offsetY = y - lastY;                  ((View) getParent()).scrollBy(-offsetX, -offsetY);                  break;          }          return true;      }  }

The 2. 動畫

  • 以上已經對View動畫、屬性動畫分別給過例子;

或參考以下部落格:

The 3. 改變布局參數

public class DragView3 extends View {        private int lastX;      private int lastY;        public DragView3(Context context) {          super(context);          ininView();      }      public DragView3(Context context, AttributeSet attrs) {          super(context, attrs);          ininView();      }      public DragView3(Context context, AttributeSet attrs, int defStyleAttr) {          super(context, attrs, defStyleAttr);          ininView();      }        private void ininView() {          setBackgroundColor(Color.BLUE);      }        @Override      public boolean onTouchEvent(MotionEvent event) {          int x = (int) event.getX();          int y = (int) event.getY();          switch (event.getAction()) {              case MotionEvent.ACTION_DOWN:                  // 記錄觸摸點坐標                  lastX = (int) event.getX();                  lastY = (int) event.getY();                  break;              case MotionEvent.ACTION_MOVE:                  // 計算偏移量                  int offsetX = x - lastX;                  int offsetY = y - lastY;                    ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();  //                LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();                  layoutParams.leftMargin = getLeft() + offsetX;                  layoutParams.topMargin = getTop() + offsetY;                  setLayoutParams(layoutParams);                  break;          }          return true;      }  }

Tags: