国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 系統 > Android > 正文

Android Scroller完全解析

2019-12-12 04:59:46
字體:
來源:轉載
供稿:網友

在Android中,任何一個控件都是可以滾動的,因為在View類當中有scrollTo()和scrollBy()這兩個方法,如下圖所示:

這兩個方法的主要作用是將View/ViewGroup移至指定的坐標中,并且將偏移量保存起來。另外:

mScrollX 代表X軸方向的偏移坐標
mScrollY 代表Y軸方向的偏移坐標

這兩個方法都是用于對View進行滾動的,那么它們之間有什么區別呢?簡單點講,scrollBy()方法是讓View相對于當前的位置滾動某段距離,而scrollTo()方法則是讓View相對于初始的位置滾動某段距離。

關于偏移量的設置我們可以參看下源碼:

public class View {   ....   protected int mScrollX; //該視圖內容相當于視圖起始坐標的偏移量,X軸方向     protected int mScrollY; //該視圖內容相當于視圖起始坐標的偏移量,Y軸方向   //返回值   public final int getScrollX() {     return mScrollX;   }   public final int getScrollY() {     return mScrollY;   }   public void scrollTo(int x, int y) {     //偏移位置發生了改變     if (mScrollX != x || mScrollY != y) {       int oldX = mScrollX;       int oldY = mScrollY;       mScrollX = x; //賦新值,保存當前便宜量       mScrollY = y;       //回調onScrollChanged方法       onScrollChanged(mScrollX, mScrollY, oldX, oldY);       if (!awakenScrollBars()) {         invalidate(); //一般都引起重繪       }     }   }   // 看出區別了吧 。 mScrollX 與 mScrollY 代表我們當前偏移的位置 , 在當前位置繼續偏移(x ,y)個單位   public void scrollBy(int x, int y) {     scrollTo(mScrollX + x, mScrollY + y);   }   //... } 

于是,在任何時刻我們都可以獲取該View/ViewGroup的偏移位置了,即調用getScrollX()方法和getScrollY()方法。

下面我們寫個例子看下它們的區別吧:

<?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:id="@+id/layout"  android:layout_width="match_parent"  android:layout_height="match_parent"  android:orientation="vertical">  <Button    android:id="@+id/scroll_to_btn"    android:layout_width="wrap_content"    android:layout_height="wrap_content"    android:text="scrollTo"/>  <Button    android:id="@+id/scroll_by_btn"    android:layout_width="wrap_content"    android:layout_height="wrap_content"    android:layout_marginTop="10dp"    android:text="scrollBy"/></LinearLayout>

外層使用了一個LinearLayout,在里面包含了兩個按鈕,一個用于觸發scrollTo邏輯,一個用于觸發scrollBy邏輯。

public class MainActivity extends AppCompatActivity {  private LinearLayout layout;  private Button scrollToBtn;  private Button scrollByBtn;  @Override  protected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentView(R.layout.activity_main);    layout = (LinearLayout) findViewById(R.id.layout);    scrollToBtn = (Button) findViewById(R.id.scroll_to_btn);    scrollByBtn = (Button) findViewById(R.id.scroll_by_btn);    scrollToBtn.setOnClickListener(new View.OnClickListener() {      @Override      public void onClick(View v) {        layout.scrollTo(getResources().getDimensionPixelOffset(R.dimen.horizontal_scroll),            getResources().getDimensionPixelOffset(R.dimen.horizontal_scroll));      }    });    scrollByBtn.setOnClickListener(new View.OnClickListener() {      @Override      public void onClick(View v) {        layout.scrollBy(getResources().getDimensionPixelOffset(R.dimen.horizontal_scroll),            getResources().getDimensionPixelOffset(R.dimen.horizontal_scroll));      }    });  }}<resources>  <dimen name="horizontal_scroll">-20dp</dimen>  <dimen name="vertical_scroll">-30dp</dimen></resources>

當點擊了scrollTo按鈕時,我們調用了LinearLayout的scrollTo()方法,當點擊了scrollBy按鈕時,調用了LinearLayout的scrollBy()方法。那有的朋友可能會問了,為什么都是調用的LinearLayout中的scroll方法?這里一定要注意,不管是scrollTo()還是scrollBy()方法,滾動的都是該View內部的內容,而LinearLayout中的內容就是我們的兩個Button,如果你直接調用button的scroll方法的話,那結果一定不是你想看到的。

另外還有一點需要注意,就是兩個scroll方法中傳入的參數,第一個參數x表示相對于當前位置橫向移動的距離,正值向左移動,負值向右移動。第二個參數y表示相對于當前位置縱向移動的距離,正值向上移動,負值向下移動。
運行一下程序:

當我們點擊scrollTo按鈕時,兩個按鈕會一起向右下方滾動,之后再點擊scrollTo按鈕就沒有任何作用了,界面不會再繼續滾動,只有點擊scrollBy按鈕界面才會繼續滾動,并且不停點擊scrollBy按鈕界面會一起滾動下去。

Scroller類

從上面例子運行結果可以看出,利用scrollTo()/scrollBy()方法把一個View偏移至指定坐標(x,y)處,整個過程是直接跳躍的,沒有對這個偏移過程有任何控制,對用戶而言不太友好。于是,基于這種偏移控制,Scroller類被設計出來了,該類的主要作用是為偏移過程制定一定的控制流程,從而使偏移更流暢,更完美。
我們分析下源碼里去看看Scroller類的相關方法,其源代碼(部分)如下: 路徑位于 /frameworks/base/core/Java/android/widget/Scroller.java

public class Scroller {   private int mStartX;  //起始坐標點 , X軸方向   private int mStartY;  //起始坐標點 , Y軸方向   private int mCurrX;   //當前坐標點 X軸, 即調用startScroll函數后,經過一定時間所達到的值   private int mCurrY;   //當前坐標點 Y軸, 即調用startScroll函數后,經過一定時間所達到的值   private float mDeltaX; //應該繼續滑動的距離, X軸方向   private float mDeltaY; //應該繼續滑動的距離, Y軸方向   private boolean mFinished; //是否已經完成本次滑動操作, 如果完成則為 true   //構造函數   public Scroller(Context context) {     this(context, null);   }   public final boolean isFinished() {     return mFinished;   }   //強制結束本次滑屏操作   public final void forceFinished(boolean finished) {     mFinished = finished;   }   public final int getCurrX() {     return mCurrX;   }    /* Call this when you want to know the new location. If it returns true,    * the animation is not yet finished. loc will be altered to provide the    * new location. */    //根據當前已經消逝的時間計算當前的坐標點,保存在mCurrX和mCurrY值中   public boolean computeScrollOffset() {     if (mFinished) { //已經完成了本次動畫控制,直接返回為false       return false;     }     int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);     if (timePassed < mDuration) {       switch (mMode) {       case SCROLL_MODE:         float x = (float)timePassed * mDurationReciprocal;         ...         mCurrX = mStartX + Math.round(x * mDeltaX);         mCurrY = mStartY + Math.round(x * mDeltaY);         break;       ...     }     else {       mCurrX = mFinalX;       mCurrY = mFinalY;       mFinished = true;     }     return true;   }   //開始一個動畫控制,由(startX , startY)在duration時間內前進(dx,dy)個單位,即到達坐標為(startX+dx , startY+dy)出   public void startScroll(int startX, int startY, int dx, int dy, int duration) {     mFinished = false;     mDuration = duration;     mStartTime = AnimationUtils.currentAnimationTimeMillis();     mStartX = startX;    mStartY = startY;     mFinalX = startX + dx; mFinalY = startY + dy;     mDeltaX = dx;      mDeltaY = dy;     ...   } } 

其中比較重要的兩個方法為:

public boolean computeScrollOffset()
函數功能說明:根據當前已經消逝的時間計算當前的坐標點,保存在mCurrX和mCurrY值中。

public void startScroll(int startX, int startY, int dx, int dy, int duration)
函數功能說明:開始一個動畫控制,由(startX , startY)在duration時間內前進(dx,dy)個單位,到達坐標為(startX+dx , startY+dy)處。

computeScroll()方法介紹:
為了易于控制滑屏控制,Android框架提供了 computeScroll()方法去控制這個流程。在繪制View時,會在draw()過程調用該方法。因此, 再配合使用Scroller實例,我們就可以獲得當前應該的偏移坐標,手動使View/ViewGroup偏移至該處。
computeScroll()方法原型如下,該方法位于ViewGroup.java類中

/**    * 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.    * 由父視圖調用用來請求子視圖根據偏移值 mScrollX,mScrollY重新繪制 */  public void computeScroll() { //空方法 ,自定義ViewGroup必須實現方法體       } 

為了實現偏移控制,一般自定義View/ViewGroup都需要重載該方法 。其調用過程位于View繪制流程draw()過程中,如下:

@Override protected void dispatchDraw(Canvas canvas){   ...   for (int i = 0; i < count; i++) {     final View child = children[getChildDrawingOrder(count, i)];     if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {       more |= drawChild(canvas, child, drawingTime);     }   } } protected boolean drawChild(Canvas canvas, View child, long drawingTime) {   ...   child.computeScroll();   ... } 

實例演示

ViewPager相信每個人都再熟悉不過了,因此它實在是太常用了,我們可以借助ViewPager來輕松完成頁面之間的滑動切換效果,但是如果問到它是如何實現的話,我感覺大部分人還是比較陌生的。其實說到ViewPager最基本的實現原理主要就是兩部分內容,一個是事件分發,一個是Scroller。對于事件分發,不了解的同學可以參考我這篇博客Android事件的分發、攔截和執行
接下來我將結合事件分發和Scroller來實現一個簡易版的ViewPager。首先自定義一個ViewGroup,不了解的可以參考Android自定義ViewGroup(一)之CustomGridLayout這篇文章。平滑偏移的主要做法如下:

第一、調用Scroller實例去產生一個偏移控制(對應于startScroll()方法)
第二、手動調用invalid()方法去重新繪制,剩下的就是在computeScroll()里根據當前已經逝去的時間,獲取當前應該偏移的坐標(由Scroller實例對應的computeScrollOffset()計算而得)
第三、當前應該偏移的坐標,調用scrollBy()方法去緩慢移動至該坐標處。

新建一個ScrollerLayout并讓它繼承自ViewGroup來作為我們的簡易ViewPager布局,代碼如下所示:

public class ScrollerLayout extends ViewGroup {  private Scroller mScroller; //用于完成滾動操作的實例  private VelocityTracker mVelocityTracker = null ; //處理觸摸的速率  public static int SNAP_VELOCITY = 600 ; //最小的滑動速率  private int mTouchSlop = 0 ;      //最小滑動距離,超過了,才認為開始滑動  private float mLastionMotionX = 0 ;  //上次觸發ACTION_MOVE事件時的屏幕坐標  private int curScreen = 0 ; //當前屏幕  private int leftBorder;  //界面可滾動的左邊界  private int rightBorder; //界面可滾動的右邊界  //兩種狀態: 是否處于滑屏狀態  private static final int TOUCH_STATE_REST = 0; //什么都沒做的狀態  private static final int TOUCH_STATE_SCROLLING = 1; //開始滑屏的狀態  private int mTouchState = TOUCH_STATE_REST; //默認是什么都沒做的狀態  public ScrollerLayout(Context context, AttributeSet attrs) {    super(context, attrs);    // 創建Scroller的實例    mScroller = new Scroller(context);    //初始化一個最小滑動距離    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();  }  @Override  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    int childCount = getChildCount();    for (int i = 0; i < childCount; i++) {      View childView = getChildAt(i);      // 為ScrollerLayout中的每一個子控件測量大小      measureChild(childView, widthMeasureSpec, heightMeasureSpec);    }  }  @Override  protected void onLayout(boolean changed, int l, int t, int r, int b) {    if (changed) {      int childCount = getChildCount();      for (int i = 0; i < childCount; i++) {        View childView = getChildAt(i);        // 為ScrollerLayout中的每一個子控件在水平方向上進行布局        childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());      }    }    // 初始化左右邊界值    leftBorder = getChildAt(0).getLeft();    rightBorder = getChildAt(getChildCount() - 1).getRight();  }  @Override  public boolean onInterceptTouchEvent(MotionEvent ev) {    final int action = ev.getAction();    //表示已經開始滑動了,不需要走該Action_MOVE方法了(第一次時可能調用)。    //該方法主要用于用戶快速松開手指,又快速按下的行為。此時認為是處于滑屏狀態的。    if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) {      return true;    }    final float x = ev.getX();    switch (action) {      case MotionEvent.ACTION_MOVE:        final int xDiff = (int) Math.abs(mLastionMotionX - x);        //超過了最小滑動距離,就可以認為開始滑動了        if (xDiff > mTouchSlop) {          mTouchState = TOUCH_STATE_SCROLLING;        }        break;      case MotionEvent.ACTION_DOWN:        mLastionMotionX = x;        mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;        break;      case MotionEvent.ACTION_CANCEL:      case MotionEvent.ACTION_UP:        mTouchState = TOUCH_STATE_REST;        break;    }    return mTouchState != TOUCH_STATE_REST;  }  public boolean onTouchEvent(MotionEvent event){    super.onTouchEvent(event);    //獲得VelocityTracker對象,并且添加滑動對象    if (mVelocityTracker == null) {      mVelocityTracker = VelocityTracker.obtain();    }    mVelocityTracker.addMovement(event);    //觸摸點    float x = event.getX();    switch(event.getAction()){      case MotionEvent.ACTION_DOWN:        //如果屏幕的動畫還沒結束,你就按下了,我們就結束上一次動畫,即開始這次新ACTION_DOWN的動畫        if(mScroller != null){          if(!mScroller.isFinished()){            mScroller.abortAnimation();          }        }        mLastionMotionX = x ; //記住開始落下的屏幕點        break ;      case MotionEvent.ACTION_MOVE:        int detaX = (int)(mLastionMotionX - x ); //每次滑動屏幕,屏幕應該移動的距離        if (getScrollX() + detaX < leftBorder) {  //防止用戶拖出邊界這里還專門做了邊界保護,當拖出邊界時就調用scrollTo()方法來回到邊界位置          scrollTo(leftBorder, 0);          return true;        } else if (getScrollX() + getWidth() + detaX > rightBorder) {          scrollTo(rightBorder - getWidth(), 0);          return true;        }        scrollBy(detaX, 0);//開始緩慢滑屏咯。 detaX > 0 向右滑動 , detaX < 0 向左滑動        mLastionMotionX = x ;        break ;      case MotionEvent.ACTION_UP:        final VelocityTracker velocityTracker = mVelocityTracker ;        velocityTracker.computeCurrentVelocity(1000);        //計算速率        int velocityX = (int) velocityTracker.getXVelocity() ;        //滑動速率達到了一個標準(快速向右滑屏,返回上一個屏幕) 馬上進行切屏處理        if (velocityX > SNAP_VELOCITY && curScreen > 0) {          // Fling enough to move left          snapToScreen(curScreen - 1);        }        //快速向左滑屏,返回下一個屏幕        else if(velocityX < -SNAP_VELOCITY && curScreen < (getChildCount()-1)){          snapToScreen(curScreen + 1);        }        //以上為快速移動的 ,強制切換屏幕        else{          //我們是緩慢移動的,因此先判斷是保留在本屏幕還是到下一屏幕          snapToDestination();        }        //回收VelocityTracker對象        if (mVelocityTracker != null) {          mVelocityTracker.recycle();          mVelocityTracker = null;        }        //修正mTouchState值        mTouchState = TOUCH_STATE_REST ;        break;      case MotionEvent.ACTION_CANCEL:        mTouchState = TOUCH_STATE_REST ;        break;    }    return true ;  }  //我們是緩慢移動的,因此需要根據偏移值判斷目標屏是哪個  private void snapToDestination(){    //判斷是否超過下一屏的中間位置,如果達到就抵達下一屏,否則保持在原屏幕    //公式意思是:假設當前滑屏偏移值即 scrollCurX 加上每個屏幕一半的寬度,除以每個屏幕的寬度就是我們目標屏所在位置了。    int destScreen = (getScrollX() + getWidth() / 2 ) / getWidth() ;    snapToScreen(destScreen);  }  //真正的實現跳轉屏幕的方法  private void snapToScreen(int whichScreen){    //簡單的移到目標屏幕,可能是當前屏或者下一屏幕,直接跳轉過去,不太友好,為了友好性,我們在增加一個動畫效果    curScreen = whichScreen ;    //防止屏幕越界,即超過屏幕數    if(curScreen > getChildCount() - 1)      curScreen = getChildCount() - 1 ;    //為了達到下一屏幕或者當前屏幕,我們需要繼續滑動的距離.根據dx值,可能向左滑動,也可能向右滑動    int dx = curScreen * getWidth() - getScrollX() ;    mScroller.startScroll(getScrollX(), 0, dx, 0, Math.abs(dx) * 2);    //由于觸摸事件不會重新繪制View,所以此時需要手動刷新View 否則沒效果    invalidate();  }  @Override  public void computeScroll() {    //重寫computeScroll()方法,并在其內部完成平滑滾動的邏輯    if (mScroller.computeScrollOffset()) {      scrollTo(mScroller.getCurrX(), mScroller.getCurrY());      invalidate();    }  }}

代碼比較長,但思路比較清晰。
(1)首先在ScrollerLayout的構造函數里面我們創建Scroller的實例,由于Scroller的實例只需創建一次,因此我們把它放到構造函數里面執行。另外在構建函數中我們還初始化的TouchSlop的值,這個值在后面將用于判斷當前用戶的操作是否是拖動。
(2)接著重寫onMeasure()方法和onLayout()方法,在onMeasure()方法中測量ScrollerLayout里的每一個子控件的大小,在onLayout()方法中為ScrollerLayout里的每一個子控件在水平方向上進行布局,布局類似于方向為horizontal的LinearLayout。
(3) 接著重寫onInterceptTouchEvent()方法, 在這個方法中我們記錄了用戶手指按下時的X坐標位置,以及用戶手指在屏幕上拖動時的X坐標位置,當兩者之間的距離大于TouchSlop值時,就認為用戶正在拖動布局,置狀態為TOUCH_STATE_SCROLLING,當用戶手指抬起,重置狀態為TOUCH_STATE_REST。這里當狀態值為TOUCH_STATE_SCROLLING時返回true,將事件在這里攔截掉,阻止事件傳遞到子控件當中。
(4)那么當我們把事件攔截掉之后,就會將事件交給ScrollerLayout的onTouchEvent()方法來處理。
如果當前事件是ACTION_MOVE,說明用戶正在拖動布局,那么我們就應該對布局內容進行滾動從而影響拖動事件,實現的方式就是使用我們剛剛所學的scrollBy()方法,用戶拖動了多少這里就scrollBy多少。另外為了防止用戶拖出邊界這里還專門做了邊界保護,當拖出邊界時就調用scrollTo()方法來回到邊界位置。
如果當前事件是ACTION_UP時,說明用戶手指抬起來了,但是目前很有可能用戶只是將布局拖動到了中間,我們不可能讓布局就這么停留在中間的位置,因此接下來就需要借助Scroller來完成后續的滾動操作。首先計算滾動速率,判斷當前動作是scroll還是fling。如果是fling,再根據fling的方向跳轉到上一頁或者下一頁,調用函數snapToScreen。如果是scroll,就調用函數snapToDestination,函數中首先根據當前的滾動位置來計算布局應該繼續滾動到哪一頁,滾動到哪一頁同樣調用snapToScreen。再來看看snapToScreen寫法吧,其實是調用startScroll()方法來滾動數據,緊接著調用invalidate()方法來刷新界面。
(5)重寫computeScroll()方法,并在其內部完成平滑滾動的邏輯 。在整個后續的平滑滾動過程中,computeScroll()方法是會一直被調用的,因此我們需要不斷調用Scroller的computeScrollOffset()方法來進行判斷滾動操作是否已經完成了,如果還沒完成的話,那就繼續調用scrollTo()方法,并把Scroller的curX和curY坐標傳入,然后刷新界面從而完成平滑滾動的操作。

現在ScrollerLayout已經準備好了,接下來我們修改activity_main.xml布局中的內容,如下所示:

<?xml version="1.0" encoding="utf-8"?><com.hx.scroller.ScrollerLayout xmlns:android="http://schemas.android.com/apk/res/android"  android:layout_width="match_parent"  android:layout_height="match_parent">  <ImageView    android:layout_width="match_parent"    android:layout_height="200dp"    android:background="@drawable/crazy_1" />  <ImageView    android:layout_width="match_parent"    android:layout_height="200dp"    android:background="@drawable/crazy_2" />  <ImageView    android:layout_width="match_parent"    android:layout_height="200dp"    android:background="@drawable/crazy_3" /></com.hx.scroller.ScrollerLayout>

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持武林網。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 武宁县| 海南省| 金沙县| 闻喜县| 海淀区| 炉霍县| 望奎县| 靖边县| 临沧市| 泾阳县| 绍兴县| 廉江市| 长寿区| 得荣县| 连州市| 上蔡县| 青河县| 大田县| 长治市| 集贤县| 禄劝| 临海市| 黑龙江省| 清涧县| 河西区| 安图县| 栾城县| 华安县| 全南县| 邹城市| 沐川县| 遂溪县| 芜湖县| 容城县| 扎囊县| 榕江县| 普兰店市| 台前县| 丹巴县| 陆丰市| 福贡县|