Android View 的滑動方式

前言java

自定義View做爲Android進階的基礎,是咱們開發者不得不學習的知識,而酷炫的自定義View效果,都離不開View的滑動,因此接下來咱們來一塊兒探究View的滑動方式,看看View是如何滑動的,爲Android進階的道路打下基礎。android

  • View 座標系基本知識

    • 瞭解View的滑動方式,首先咱們得了解View在什麼位置,咱們能夠把手機屏幕區域當作是像數學中座標系同樣的區域,只不過是手機屏幕座標系的Y軸和數學中的座標系的Y軸正方向相反git

    • 肯定View的位置主要是根據View的left、top、right、bottom四個屬性來決定,須要注意的是View的這四個屬性是相對於它的父容器來講的,因此對應爲left是View的左上角相對於父容器的橫座標,top爲縱座標,right爲View右下角相對於父容器的橫座標,bottom爲縱座標。(具體能夠看下方示意圖A)github

      //獲取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原始位置的值,這是不會改變的,因此改變的是滑動偏移量加上原始值獲得新的左上角座標。bash

    View座標系和點擊事件示意圖

    • 當咱們觸摸屏幕,則能夠經過點擊事件來獲取當前點擊位置的值和相較於手機屏幕左上角的偏移量座標(如上圖B所示)。
  • Android View 的滑動方式

    • layout方法改變View位置滑動Viewapp

      • 首先咱們看看layout()方法源碼ide

        @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的內容。
    • offsetLeftAndRight()與offsetTopAndBottom() 方法改變View的位置讓其滑動post

      • 修改上面的方法,效果圖和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值的判斷

      • 結合上面的知識,咱們將上面滑動的例子改寫一下,若是使用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的位置沒有發生改變

    • 使用動畫讓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開始加入了屬性動畫(瞭解屬性動畫能夠查看郭霖大佬博客),屬性動畫不只能做用於View產生動畫效果,也能做用於其餘屬性來產生動畫效果,能夠說屬性動畫相較於補間動畫是很是靈活的,而且屬性動畫是真正改變View的位置屬性。
        • 屬性動畫通常咱們使用ObjectAnimator,讓View2秒時間水平平移到300位置,而且移動完後咱們點擊View看還能響應點擊事件(以下圖所示)
        ObjectAnimator.ofFloat(testScroll,"translationX",0,300).setDuration(2000).start();
        
        複製代碼
    • 改變佈局參數 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的滑動方式就已經瞭解完了。若是文章中有寫得不對的地方,請給我留言指出,你們一塊兒學習進步。若是以爲個人文章給予你幫助,也請給我一個喜歡和關注。

相關文章
相關標籤/搜索