欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解Android應(yīng)用開發(fā)中Scroller類的屏幕滑動(dòng)功能運(yùn)用

 更新時(shí)間:2016年02月29日 17:55:05   作者:qinjuning  
這篇文章主要介紹了詳解Android應(yīng)用開發(fā)中Scroller類的屏幕滑動(dòng)功能運(yùn)用,文中包括各種觸摸滑屏手勢相關(guān)方法的示例,需要的朋友可以參考下

今天給大家介紹下Android中滑屏功能的一個(gè)基本實(shí)現(xiàn)過程以及原理初探,最后給大家重點(diǎn)講解View視圖中scrollTo 與scrollBy這兩個(gè)函數(shù)的區(qū)別 。
 
首先 ,我們必須明白在Android View視圖是沒有邊界的,Canvas是沒有邊界的,只不過我們通過繪制特定的View時(shí)對Canvas對象進(jìn)行了一定的操作,例如 : translate(平移)、clipRect(剪切)等,以便達(dá)到我們的對該Canvas對象繪制的要求 ,我們可以將這種無邊界的視圖稱為“視圖坐標(biāo)”-----它不受物理屏幕限制。通常我們所理解的一個(gè)Layout布局文件只是該視圖的顯示區(qū)域,超過了這個(gè)顯示區(qū)域?qū)⒉荒茱@示到父視圖的區(qū)域中 ,對應(yīng)的,我們可以將這種有邊界的視圖稱為“布局坐標(biāo)”父視圖給子視圖分配的布局(layout)大小。而且, 一個(gè)視圖的在屏幕的起始坐標(biāo)位于視圖坐標(biāo)起始處,如下圖所示。
 
這么來說吧 ,世界本是無邊無界的,可是我們的眼睛我們的心約束了我們所看到的“世界” 。
 
如下所示:

2016229174501999.gif (527×343)

黑色框框表示該子視圖的布局坐標(biāo), 褐色框框表示該子視圖的視圖坐標(biāo)--該坐標(biāo)是無限的,超過了父視圖給子視圖規(guī)定的區(qū)域后,不再顯示該超出內(nèi)容。
 
那么下面的問題就是:如何將我們的視圖的任意坐標(biāo)能顯示到該視圖的中心坐標(biāo)上呢? 由于該布局位置是只能顯示特定的一塊視圖內(nèi)容 ,因此我們需要通過scrollTo()或者scrollBy()方法將我們期望的視圖“滾動(dòng)”至布局坐標(biāo)上。
 
      在View.java中提供了了如下兩個(gè)變量以及相應(yīng)的屬性方法去讀取滾動(dòng)值 ,如下: View.java類中  

/** 
   * The offset, in pixels, by which the content of this view is scrolled 
   * horizontally. 
   * {@hide} 
   */ 
  protected int mScrollX;  //該視圖內(nèi)容相當(dāng)于視圖起始坐標(biāo)的偏移量  , X軸 方向 
  /** 
   * The offset, in pixels, by which the content of this view is scrolled 
   * vertically. 
   * {@hide} 
   */ 
  protected int mScrollY;  //該視圖內(nèi)容相當(dāng)于視圖起始坐標(biāo)的偏移量  , Y軸方向 
 
  /** 
   * Return the scrolled left position of this view. This is the left edge of 
   * the displayed part of your view. You do not need to draw any pixels 
   * farther left, since those are outside of the frame of your view on 
   * screen. 
   * 
   * @return The left edge of the displayed part of your view, in pixels. 
   */ 
  public final int getScrollX() { 
    return mScrollX; 
  } 
 
  /** 
   * Return the scrolled top position of this view. This is the top edge of 
   * the displayed part of your view. You do not need to draw any pixels above 
   * it, since those are outside of the frame of your view on screen. 
   * 
   * @return The top edge of the displayed part of your view, in pixels. 
   */ 
  public final int getScrollY() { 
    return mScrollY; 
  } 

 
注意,所謂的“by which the content of this view is scrolled”表示該偏移量只針對于該View中onDraw()方法里的具體內(nèi)容實(shí)現(xiàn),而不針對繪制背景圖片等 。具體原因可參考<Android中View繪制流程以及invalidate()等相關(guān)方法分析>
 
提示:下文中提到的當(dāng)前視圖內(nèi)容是在繪制在布局坐標(biāo)處的內(nèi)容。
 

public void scrollTo(int x, int y)

說明:在當(dāng)前視圖內(nèi)容偏移至(x , y)坐標(biāo)處,即顯示(可視)區(qū)域位于(x , y)坐標(biāo)處。
方法原型為: View.java類中

/** 
 * 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) { 
  //偏移位置發(fā)生了改變 
  if (mScrollX != x || mScrollY != y) { 
    int oldX = mScrollX; 
    int oldY = mScrollY; 
    mScrollX = x; //賦新值,保存當(dāng)前便宜量 
    mScrollY = y; 
    //回調(diào)onScrollChanged方法 
    onScrollChanged(mScrollX, mScrollY, oldX, oldY); 
    if (!awakenScrollBars()) { 
      invalidate(); //一般都引起重繪 
    } 
  } 
} 
 
   public void scrollBy(int x, int y)  

說明:在當(dāng)前視圖內(nèi)容繼續(xù)偏移(x , y)個(gè)單位,顯示(可視)區(qū)域也跟著偏移(x,y)個(gè)單位。方法原型為: View.java類中

/** 
  * 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 
  */ 
 // 看出原因了吧 。。 mScrollX 與 mScrollY 代表我們當(dāng)前偏移的位置 , 在當(dāng)前位置繼續(xù)偏移(x ,y)個(gè)單位 
 public void scrollBy(int x, int y) { 
   scrollTo(mScrollX + x, mScrollY + y); 
 } 

        
            
第一個(gè)小Demo非常簡單 ,大家重點(diǎn)理解與掌握scrollTo() 與 scrollBy()函數(shù)的用法和區(qū)別。
第二個(gè)小Demo則有了Launcher的模樣,能夠左右切換屏幕 。實(shí)現(xiàn)功能如下: 采用了一個(gè)自定義ViewGroup,該ViewGroup對象包含了3個(gè)LinearLayout子視圖,并且以一定的布局坐標(biāo)(由layout()方法指定)顯示在ViewGroup上。 接下來,即可調(diào)用該ViewGroup對象的scrollTo或者scrollBy()方法切換指定視圖內(nèi)容了,即切換屏幕。 呵呵 ,挺好玩的吧 。

2016229174555827.gif (299×405)

自定義ViewGroup如下:

//自定義ViewGroup , 包含了三個(gè)LinearLayout控件,存放在不同的布局位置,通過scrollBy或者scrollTo方法切換 
public class MultiViewGroup extends ViewGroup { 
 
  private Context mContext; 
 
  private static String TAG = "MultiViewGroup"; 
 
  public MultiViewGroup(Context context) { 
    super(context); 
    mContext = context; 
    init(); 
  } 
 
  public MultiViewGroup(Context context, AttributeSet attrs) { 
    super(context, attrs); 
    mContext = context; 
    init(); 
  } 
 
  private void init() { 
    // 初始化3個(gè) LinearLayout控件 
    LinearLayout oneLL = new LinearLayout(mContext); 
    oneLL.setBackgroundColor(Color.RED); 
    addView(oneLL); 
     
    LinearLayout twoLL = new LinearLayout(mContext); 
    twoLL.setBackgroundColor(Color.YELLOW); 
    addView(twoLL); 
     
    LinearLayout threeLL = new LinearLayout(mContext); 
    threeLL.setBackgroundColor(Color.BLUE); 
    addView(threeLL); 
  } 
 
  // measure過程 
  @Override 
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
 
    Log.i(TAG, "--- start onMeasure --"); 
 
    // 設(shè)置該ViewGroup的大小 
    int width = MeasureSpec.getSize(widthMeasureSpec); 
    int height = MeasureSpec.getSize(heightMeasureSpec); 
    setMeasuredDimension(width, height); 
 
    int childCount = getChildCount(); 
    Log.i(TAG, "--- onMeasure childCount is -->" + childCount); 
    for (int i = 0; i < childCount; i++) { 
      View child = getChildAt(i); 
      // 設(shè)置每個(gè)子視圖的大小 , 即全屏 
      child.measure(MultiScreenActivity.screenWidth, MultiScreenActivity.scrrenHeight); 
    } 
  } 
 
  // layout過程 
  @Override 
  protected void onLayout(boolean changed, int l, int t, int r, int b) { 
    // TODO Auto-generated method stub 
    Log.i(TAG, "--- start onLayout --"); 
    int startLeft = 0; // 每個(gè)子視圖的起始布局坐標(biāo) 
    int startTop = 10; // 間距設(shè)置為10px 相當(dāng)于 android:marginTop= "10px" 
    int childCount = getChildCount(); 
    Log.i(TAG, "--- onLayout childCount is -->" + childCount); 
    for (int i = 0; i < childCount; i++) { 
      View child = getChildAt(i); 
      child.layout(startLeft, startTop,  
          startLeft + MultiScreenActivity.screenWidth,  
          startTop + MultiScreenActivity.scrrenHeight); 
      startLeft = startLeft + MultiScreenActivity.screenWidth ; //校準(zhǔn)每個(gè)子View的起始布局位置 
      //三個(gè)子視圖的在屏幕中的分布如下 [0 , 320] / [320,640] / [640,960] 
    } 
  } 
 
} 

關(guān)于scrollTo()和scrollBy()以及偏移坐標(biāo)的設(shè)置/取值問題

scrollTo()和scrollBy()這兩個(gè)方法的主要作用是將View/ViewGroup移至指定的坐標(biāo)中,并且將偏移量保存起來。另外:
mScrollX 代表X軸方向的偏移坐標(biāo),mScrollY 代表Y軸方向的偏移坐標(biāo)

關(guān)于偏移量的設(shè)置我們可以參看下源碼:

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

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

Scroller類的介紹
在初次看Launcher滑屏的時(shí)候,我就對Scroller類的學(xué)習(xí)感到非常蛋疼,完全失去了繼續(xù)研究的欲望。如今,沒得辦法,得重新看Launcher模塊,基本上將Launcher大部分類以及功能給掌握了。當(dāng)然,也花了一天時(shí)間來學(xué)習(xí)Launcher里的滑屏實(shí)現(xiàn),基本上業(yè)是撥開云霧見真知了。
    
我們知道想把一個(gè)View偏移至指定坐標(biāo)(x,y)處,利用scrollTo()方法直接調(diào)用就OK了,但我們不能忽視的是,該方法本身來的的副作用:非常迅速的將View/ViewGroup偏移至目標(biāo)點(diǎn),而沒有對這個(gè)偏移過程有任何控制,對用戶而言可能是不太友好的。于是,基于這種偏移控制,Scroller類被設(shè)計(jì)出來了,該類的主要作用是為偏移過程制定一定的控制流程(后面我們會(huì)知道的更多),從而使偏移更流暢,更完美。
  
可能上面說的比較懸乎,道理也沒有講透。下面我就根據(jù)特定情景幫助大家分析下:

情景: 從上海如何到武漢?
普通的人可能會(huì)想,so easy : 飛機(jī)、輪船、11路公交車...
文藝的人可能會(huì)想,  小 case : 時(shí)空忍術(shù)(火影的招數(shù))、翻個(gè)筋斗(孫大圣的招數(shù))...

不管怎么樣,我們想出來的套路可能有兩種:
1、有個(gè)時(shí)間控制過程才能抵達(dá)(緩慢的前進(jìn))、對應(yīng)于Scroller的作用
假設(shè)做火車,這個(gè)過程可能包括: 火車速率,花費(fèi)周期等;
2、瞬間抵達(dá)(超神太快了,都眩暈了,用戶體驗(yàn)不太好)、對應(yīng)于scrollTo()的作用
 
模擬Scroller類的實(shí)現(xiàn)功能:

假設(shè)從上海做動(dòng)車到武漢需要10個(gè)小時(shí),行進(jìn)距離為1000km ,火車速率200/h 。采用第一種時(shí)間控制方法到達(dá)武漢的整個(gè)配合過程可能如下:我們每隔一段時(shí)間(例如1小時(shí)),計(jì)算火車應(yīng)該行進(jìn)的距離,然后調(diào)用scrollTo()方法,行進(jìn)至該處。10小時(shí)過完后,我們也就達(dá)到了目的地了。相信大家心里應(yīng)該有個(gè)感覺了。我們就分析下源碼里去看看Scroller類的相關(guān)方法.
其源代碼(部分)如下:
路徑位于 \frameworks\base\core\java\android\widget\Scroller.java

public class Scroller { 
 
  private int mStartX;  //起始坐標(biāo)點(diǎn) , X軸方向 
  private int mStartY;  //起始坐標(biāo)點(diǎn) , Y軸方向 
  private int mCurrX;   //當(dāng)前坐標(biāo)點(diǎn) X軸, 即調(diào)用startScroll函數(shù)后,經(jīng)過一定時(shí)間所達(dá)到的值 
  private int mCurrY;   //當(dāng)前坐標(biāo)點(diǎn) Y軸, 即調(diào)用startScroll函數(shù)后,經(jīng)過一定時(shí)間所達(dá)到的值 
   
  private float mDeltaX; //應(yīng)該繼續(xù)滑動(dòng)的距離, X軸方向 
  private float mDeltaY; //應(yīng)該繼續(xù)滑動(dòng)的距離, Y軸方向 
  private boolean mFinished; //是否已經(jīng)完成本次滑動(dòng)操作, 如果完成則為 true 
 
  //構(gòu)造函數(shù) 
  public Scroller(Context context) { 
    this(context, null); 
  } 
  public final boolean isFinished() { 
    return mFinished; 
  } 
  //強(qiáng)制結(jié)束本次滑屏操作 
  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. */  
  //根據(jù)當(dāng)前已經(jīng)消逝的時(shí)間計(jì)算當(dāng)前的坐標(biāo)點(diǎn),保存在mCurrX和mCurrY值中 
  public boolean computeScrollOffset() { 
    if (mFinished) { //已經(jīng)完成了本次動(dòng)畫控制,直接返回為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; 
  } 
  //開始一個(gè)動(dòng)畫控制,由(startX , startY)在duration時(shí)間內(nèi)前進(jìn)(dx,dy)個(gè)單位,即到達(dá)坐標(biāo)為(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; 
    ... 
  } 
} 

其中比較重要的兩個(gè)方法為:

public void startScroll(int startX, int startY, int dx, int dy, int duration)

函數(shù)功能說明:根據(jù)當(dāng)前已經(jīng)消逝的時(shí)間計(jì)算當(dāng)前的坐標(biāo)點(diǎn),保存在mCurrX和mCurrY值中

public void startScroll(int startX, int startY, int dx, int dy, int duration)

函數(shù)功能說明:開始一個(gè)動(dòng)畫控制,由(startX , startY)在duration時(shí)間內(nèi)前進(jìn)(dx,dy)個(gè)單位,到達(dá)坐標(biāo)為 (startX+dx , startY+dy)處。

PS : 強(qiáng)烈建議大家看看該類的源碼,便于后續(xù)理解。

computeScroll()方法介紹

為了易于控制滑屏控制,Android框架提供了 computeScroll()方法去控制這個(gè)流程。在繪制View時(shí),會(huì)在draw()過程調(diào)用該方法。因此, 再配合使用Scroller實(shí)例,我們就可以獲得當(dāng)前應(yīng)該的偏移坐標(biāo),手動(dòng)使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. 
   */由父視圖調(diào)用用來請求子視圖根據(jù)偏移值 mScrollX,mScrollY重新繪制 
  public void computeScroll() { //空方法 ,自定義ViewGroup必須實(shí)現(xiàn)方法體 
     
  } 

為了實(shí)現(xiàn)偏移控制,一般自定義View/ViewGroup都需要重載該方法 。其調(diào)用過程位于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(); 
  ... 
} 

Demo說明:
我們簡單的復(fù)用了之前寫的一個(gè)自定義ViewGroup,與以前一次有區(qū)別的是,我們沒有調(diào)用scrollTo()方法去進(jìn)行瞬間偏移。 本次做法如下:
第一、調(diào)用Scroller實(shí)例去產(chǎn)生一個(gè)偏移控制(對應(yīng)于startScroll()方法)
第二、手動(dòng)調(diào)用invalid()方法去重新繪制,剩下的就是在 computeScroll()里根據(jù)當(dāng)前已經(jīng)逝去的時(shí)間,獲取當(dāng)前應(yīng)該偏移的坐標(biāo)(由Scroller實(shí)例對應(yīng)的computeScrollOffset()計(jì)算而得),
第三、當(dāng)前應(yīng)該偏移的坐標(biāo),調(diào)用scrollBy()方法去緩慢移動(dòng)至該坐標(biāo)處。

2016229174754863.png (352×501)2016229174812081.png (328×464)

附:由于滑動(dòng)截屏很難,只是簡單的截取了兩個(gè)個(gè)靜態(tài)圖片,觸摸的話可以實(shí)現(xiàn)左右滑動(dòng)切屏了。

//自定義ViewGroup , 包含了三個(gè)LinearLayout控件,存放在不同的布局位置,通過scrollBy或者scrollTo方法切換 
public class MultiViewGroup extends ViewGroup { 
  ... 
  //startScroll開始移至下一屏 
  public void startMove(){ 
    curScreen ++ ; 
    Log.i(TAG, "----startMove---- curScreen " + curScreen); 
     
    //使用動(dòng)畫控制偏移過程 , 3s內(nèi)到位 
    mScroller.startScroll((curScreen-1) * getWidth(), 0, getWidth(), 0,3000); 
    //其實(shí)點(diǎn)擊按鈕的時(shí)候,系統(tǒng)會(huì)自動(dòng)重新繪制View,我們還是手動(dòng)加上吧。 
    invalidate(); 
    //使用scrollTo一步到位 
    //scrollTo(curScreen * MultiScreenActivity.screenWidth, 0); 
  } 
  // 由父視圖調(diào)用用來請求子視圖根據(jù)偏移值 mScrollX,mScrollY重新繪制 
  @Override 
  public void computeScroll() {   
    // TODO Auto-generated method stub 
    Log.e(TAG, "computeScroll"); 
    // 如果返回true,表示動(dòng)畫還沒有結(jié)束 
    // 因?yàn)榍懊鎠tartScroll,所以只有在startScroll完成時(shí) 才會(huì)為false 
    if (mScroller.computeScrollOffset()) { 
      Log.e(TAG, mScroller.getCurrX() + "======" + mScroller.getCurrY()); 
      // 產(chǎn)生了動(dòng)畫效果,根據(jù)當(dāng)前值 每次滾動(dòng)一點(diǎn) 
      scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); 
       
      Log.e(TAG, "### getleft is " + getLeft() + " ### getRight is " + getRight()); 
      //此時(shí)同樣也需要刷新View ,否則效果可能有誤差 
      postInvalidate(); 
    } 
    else 
      Log.i(TAG, "have done the scoller -----"); 
  } 
  //馬上停止移動(dòng),如果已經(jīng)超過了下一屏的一半,我們強(qiáng)制滑到下一個(gè)屏幕 
  public void stopMove(){ 
     
    Log.v(TAG, "----stopMove ----"); 
     
    if(mScroller != null){ 
      //如果動(dòng)畫還沒結(jié)束,我們就按下了結(jié)束的按鈕,那我們就結(jié)束該動(dòng)畫,即馬上滑動(dòng)指定位置 
      if(!mScroller.isFinished()){ 
         
        int scrollCurX= mScroller.getCurrX() ; 
        //判斷是否超過下一屏的中間位置,如果達(dá)到就抵達(dá)下一屏,否則保持在原屏幕 
        // 這樣的一個(gè)簡單公式意思是:假設(shè)當(dāng)前滑屏偏移值即 scrollCurX 加上每個(gè)屏幕一半的寬度,除以每個(gè)屏幕的寬度就是 
        // 我們目標(biāo)屏所在位置了。 假如每個(gè)屏幕寬度為320dip, 我們滑到了500dip處,很顯然我們應(yīng)該到達(dá)第二屏,索引值為1 
        //即(500 + 320/2)/320 = 1 
        int descScreen = ( scrollCurX + getWidth() / 2) / getWidth() ; 
         
        Log.i(TAG, "-mScroller.is not finished scrollCurX +" + scrollCurX); 
        Log.i(TAG, "-mScroller.is not finished descScreen +" + descScreen); 
        mScroller.abortAnimation(); 
 
        //停止了動(dòng)畫,我們馬上滑倒目標(biāo)位置 
        scrollTo(descScreen *getWidth() , 0); 
        curScreen = descScreen ; //糾正目標(biāo)屏位置 
      } 
      else 
        Log.i(TAG, "----OK mScroller.is finished ---- "); 
    }   
  } 
  ... 
} 

如何實(shí)現(xiàn)觸摸滑屏?

其實(shí)網(wǎng)上有很多關(guān)于Launcher實(shí)現(xiàn)滑屏的博文,基本上也把道理闡釋的比較明白了 。我這兒也是基于自己的理解,將一些
重要方面的知識(shí)點(diǎn)給補(bǔ)充下,希望能幫助大家理解。想要實(shí)現(xiàn)滑屏操作,值得考慮的事情包括如下幾個(gè)方面:

其中:onInterceptTouchEvent()主要功能是控制觸摸事件的分發(fā),例如是子視圖的點(diǎn)擊事件還是滑動(dòng)事件。其他所有處理過程均在onTouchEvent()方法里實(shí)現(xiàn)了。
1、屏幕的滑動(dòng)要根據(jù)手指的移動(dòng)而移動(dòng)  ---- 主要實(shí)現(xiàn)在onTouchEvent()方法中
2、當(dāng)手指松開時(shí),可能我們并沒有完全滑動(dòng)至某個(gè)屏幕上,這是我們需要手動(dòng)判斷當(dāng)前偏移至去計(jì)算目標(biāo)屏(當(dāng)前屏或者前后屏),并且優(yōu)雅的偏移到目標(biāo)屏(當(dāng)然是用Scroller實(shí)例咯)。
3、調(diào)用computeScroll ()去實(shí)現(xiàn)緩慢移動(dòng)過程。
 
知識(shí)點(diǎn)介紹:             
VelocityTracker類
功能:  根據(jù)觸摸位置計(jì)算每像素的移動(dòng)速率。
常用方法有:    
public void addMovement (MotionEvent ev)
功能:添加觸摸對象MotionEvent , 用于計(jì)算觸摸速率。  
public void computeCurrentVelocity (int units)
功能:以每像素units單位考核移動(dòng)速率。額,其實(shí)我也不太懂,賦予值1000即可。
參照源碼 該units的意思如下:
參數(shù) units : The units you would like the velocity in.  A value of 1
provides pixels per millisecond, 1000 provides pixels per second, etc.
public float getXVelocity ()
功能:獲得X軸方向的移動(dòng)速率。
ViewConfiguration類
功能: 獲得一些關(guān)于timeouts(時(shí)間)、sizes(大小)、distances(距離)的標(biāo)準(zhǔn)常量值 。
常用方法:
public int getScaledEdgeSlop()
說明:獲得一個(gè)觸摸移動(dòng)的最小像素值。也就是說,只有超過了這個(gè)值,才代表我們該滑屏處理了。
public static int getLongPressTimeout()
說明:獲得一個(gè)執(zhí)行長按事件監(jiān)聽(onLongClickListener)的值。也就是說,對某個(gè)View按下觸摸時(shí),只有超過了這個(gè)時(shí)間值在,才表示我們該對該View回調(diào)長按事件了;否則,小于這個(gè)時(shí)間點(diǎn)松開手指,只執(zhí)行onClick監(jiān)聽
 
 
我能寫下來的也就這么多了,更多的東西參考代碼注釋吧。 在掌握了上面我羅列的知識(shí)后(重點(diǎn)scrollTo、Scroller類),其他方面的知識(shí)都是關(guān)于點(diǎn)與點(diǎn)之間的計(jì)算了以及觸摸事件的分發(fā)了。這方面感覺也沒啥可寫的。

//自定義ViewGroup , 包含了三個(gè)LinearLayout控件,存放在不同的布局位置,通過scrollBy或者scrollTo方法切換 
public class MultiViewGroup extends ViewGroup { 
 
  private static String TAG = "MultiViewGroup"; 
   
  private int curScreen = 0 ; //當(dāng)前屏幕 
  private Scroller mScroller = null ; //Scroller對象實(shí)例 
   
  public MultiViewGroup(Context context) { 
    super(context); 
    mContext = context; 
    init(); 
  } 
  public MultiViewGroup(Context context, AttributeSet attrs) { 
    super(context, attrs); 
    mContext = context; 
    init(); 
  } 
  //初始化 
  private void init() {   
    ... 
    //初始化Scroller實(shí)例 
    mScroller = new Scroller(mContext); 
    // 初始化3個(gè) LinearLayout控件 
    ... 
    //初始化一個(gè)最小滑動(dòng)距離 
    mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 
  } 
  // 由父視圖調(diào)用用來請求子視圖根據(jù)偏移值 mScrollX,mScrollY重新繪制 
  @Override 
  public void computeScroll() {   
    // TODO Auto-generated method stub 
    Log.e(TAG, "computeScroll"); 
    // 如果返回true,表示動(dòng)畫還沒有結(jié)束 
    // 因?yàn)榍懊鎠tartScroll,所以只有在startScroll完成時(shí) 才會(huì)為false 
    if (mScroller.computeScrollOffset()) { 
      Log.e(TAG, mScroller.getCurrX() + "======" + mScroller.getCurrY()); 
      // 產(chǎn)生了動(dòng)畫效果,根據(jù)當(dāng)前值 每次滾動(dòng)一點(diǎn) 
      scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); 
       
      Log.e(TAG, "### getleft is " + getLeft() + " ### getRight is " + getRight()); 
      //此時(shí)同樣也需要刷新View ,否則效果可能有誤差 
      postInvalidate(); 
    } 
    else 
      Log.i(TAG, "have done the scoller -----"); 
  } 
  //兩種狀態(tài): 是否處于滑屏狀態(tài) 
  private static final int TOUCH_STATE_REST = 0; //什么都沒做的狀態(tài) 
  private static final int TOUCH_STATE_SCROLLING = 1; //開始滑屏的狀態(tài) 
  private int mTouchState = TOUCH_STATE_REST; //默認(rèn)是什么都沒做的狀態(tài) 
  //--------------------------  
  //處理觸摸事件 ~ 
  public static int SNAP_VELOCITY = 600 ; //最小的滑動(dòng)速率 
  private int mTouchSlop = 0 ;       //最小滑動(dòng)距離,超過了,才認(rèn)為開始滑動(dòng) 
  private float mLastionMotionX = 0 ;    //記住上次觸摸屏的位置 
  //處理觸摸的速率 
  private VelocityTracker mVelocityTracker = null ; 
   
  // 這個(gè)感覺沒什么作用 不管true還是false 都是會(huì)執(zhí)行onTouchEvent的 因?yàn)樽觱iew里面onTouchEvent返回false了 
  @Override 
  public boolean onInterceptTouchEvent(MotionEvent ev) { 
    // TODO Auto-generated method stub 
    Log.e(TAG, "onInterceptTouchEvent-slop:" + mTouchSlop); 
 
    final int action = ev.getAction(); 
    //表示已經(jīng)開始滑動(dòng)了,不需要走該Action_MOVE方法了(第一次時(shí)可能調(diào)用)。 
    //該方法主要用于用戶快速松開手指,又快速按下的行為。此時(shí)認(rèn)為是出于滑屏狀態(tài)的。 
    if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) { 
      return true; 
    } 
     
    final float x = ev.getX(); 
    final float y = ev.getY(); 
 
    switch (action) { 
    case MotionEvent.ACTION_MOVE: 
      Log.e(TAG, "onInterceptTouchEvent move"); 
      final int xDiff = (int) Math.abs(mLastionMotionX - x); 
      //超過了最小滑動(dòng)距離,就可以認(rèn)為開始滑動(dòng)了 
      if (xDiff > mTouchSlop) { 
        mTouchState = TOUCH_STATE_SCROLLING; 
      } 
      break; 
 
    case MotionEvent.ACTION_DOWN: 
      Log.e(TAG, "onInterceptTouchEvent down"); 
      mLastionMotionX = x; 
      mLastMotionY = y; 
      Log.e(TAG, mScroller.isFinished() + ""); 
      mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING; 
 
      break; 
 
    case MotionEvent.ACTION_CANCEL: 
    case MotionEvent.ACTION_UP: 
      Log.e(TAG, "onInterceptTouchEvent up or cancel"); 
      mTouchState = TOUCH_STATE_REST; 
      break; 
    } 
    Log.e(TAG, mTouchState + "====" + TOUCH_STATE_REST); 
    return mTouchState != TOUCH_STATE_REST; 
  } 
  public boolean onTouchEvent(MotionEvent event){ 
 
    super.onTouchEvent(event); 
     
    Log.i(TAG, "--- onTouchEvent--> " ); 
 
    // TODO Auto-generated method stub 
    Log.e(TAG, "onTouchEvent start"); 
    //獲得VelocityTracker對象,并且添加滑動(dòng)對象 
    if (mVelocityTracker == null) { 
      mVelocityTracker = VelocityTracker.obtain(); 
    } 
    mVelocityTracker.addMovement(event); 
     
    //觸摸點(diǎn) 
    float x = event.getX(); 
    float y = event.getY(); 
    switch(event.getAction()){ 
    case MotionEvent.ACTION_DOWN: 
      //如果屏幕的動(dòng)畫還沒結(jié)束,你就按下了,我們就結(jié)束上一次動(dòng)畫,即開始這次新ACTION_DOWN的動(dòng)畫 
      if(mScroller != null){ 
        if(!mScroller.isFinished()){ 
          mScroller.abortAnimation();  
        } 
      } 
      mLastionMotionX = x ; //記住開始落下的屏幕點(diǎn) 
      break ; 
    case MotionEvent.ACTION_MOVE: 
      int detaX = (int)(mLastionMotionX - x ); //每次滑動(dòng)屏幕,屏幕應(yīng)該移動(dòng)的距離 
      scrollBy(detaX, 0);//開始緩慢滑屏咯。 detaX > 0 向右滑動(dòng) , detaX < 0 向左滑動(dòng) , 
       
      Log.e(TAG, "--- MotionEvent.ACTION_MOVE--> detaX is " + detaX ); 
      mLastionMotionX = x ; 
      break ; 
    case MotionEvent.ACTION_UP: 
       
      final VelocityTracker velocityTracker = mVelocityTracker ; 
      velocityTracker.computeCurrentVelocity(1000); 
      //計(jì)算速率 
      int velocityX = (int) velocityTracker.getXVelocity() ;  
      Log.e(TAG , "---velocityX---" + velocityX); 
       
      //滑動(dòng)速率達(dá)到了一個(gè)標(biāo)準(zhǔn)(快速向右滑屏,返回上一個(gè)屏幕) 馬上進(jìn)行切屏處理 
      if (velocityX > SNAP_VELOCITY && curScreen > 0) { 
        // Fling enough to move left 
        Log.e(TAG, "snap left"); 
        snapToScreen(curScreen - 1); 
      } 
      //快速向左滑屏,返回下一個(gè)屏幕) 
      else if(velocityX < -SNAP_VELOCITY && curScreen < (getChildCount()-1)){ 
        Log.e(TAG, "snap right"); 
        snapToScreen(curScreen + 1); 
      } 
      //以上為快速移動(dòng)的 ,強(qiáng)制切換屏幕 
      else{ 
        //我們是緩慢移動(dòng)的,因此先判斷是保留在本屏幕還是到下一屏幕 
        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 ; 
  } 
  ////我們是緩慢移動(dòng)的,因此需要根據(jù)偏移值判斷目標(biāo)屏是哪個(gè)? 
  private void snapToDestination(){ 
    //當(dāng)前的偏移位置 
    int scrollX = getScrollX() ; 
    int scrollY = getScrollY() ; 
     
    Log.e(TAG, "### onTouchEvent snapToDestination ### scrollX is " + scrollX); 
    //判斷是否超過下一屏的中間位置,如果達(dá)到就抵達(dá)下一屏,否則保持在原屏幕   
    //直接使用這個(gè)公式判斷是哪一個(gè)屏幕 前后或者自己 
    //判斷是否超過下一屏的中間位置,如果達(dá)到就抵達(dá)下一屏,否則保持在原屏幕 
    // 這樣的一個(gè)簡單公式意思是:假設(shè)當(dāng)前滑屏偏移值即 scrollCurX 加上每個(gè)屏幕一半的寬度,除以每個(gè)屏幕的寬度就是 
    // 我們目標(biāo)屏所在位置了。 假如每個(gè)屏幕寬度為320dip, 我們滑到了500dip處,很顯然我們應(yīng)該到達(dá)第二屏 
    int destScreen = (getScrollX() + MultiScreenActivity.screenWidth / 2 ) / MultiScreenActivity.screenWidth ; 
     
    Log.e(TAG, "### onTouchEvent ACTION_UP### dx destScreen " + destScreen); 
     
    snapToScreen(destScreen); 
  } 
  //真正的實(shí)現(xiàn)跳轉(zhuǎn)屏幕的方法 
  private void snapToScreen(int whichScreen){  
    //簡單的移到目標(biāo)屏幕,可能是當(dāng)前屏或者下一屏幕 
    //直接跳轉(zhuǎn)過去,不太友好 
    //scrollTo(mLastScreen * MultiScreenActivity.screenWidth, 0); 
    //為了友好性,我們在增加一個(gè)動(dòng)畫效果 
    //需要再次滑動(dòng)的距離 屏或者下一屏幕的繼續(xù)滑動(dòng)距離 
     
    curScreen = whichScreen ; 
    //防止屏幕越界,即超過屏幕數(shù) 
    if(curScreen > getChildCount() - 1) 
      curScreen = getChildCount() - 1 ; 
    //為了達(dá)到下一屏幕或者當(dāng)前屏幕,我們需要繼續(xù)滑動(dòng)的距離.根據(jù)dx值,可能想左滑動(dòng),也可能像又滑動(dòng) 
    int dx = curScreen * getWidth() - getScrollX() ; 
     
    Log.e(TAG, "### onTouchEvent ACTION_UP### dx is " + dx); 
     
    mScroller.startScroll(getScrollX(), 0, dx, 0,Math.abs(dx) * 2); 
     
    //由于觸摸事件不會(huì)重新繪制View,所以此時(shí)需要手動(dòng)刷新View 否則沒效果 
    invalidate(); 
  } 
  //開始滑動(dòng)至下一屏 
  public void startMove(){ 
    ...    
  } 
  //理解停止滑動(dòng) 
  public void stopMove(){ 
    ... 
  } 
  // measure過程 
  @Override 
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
    ... 
  } 
  // layout過程 
  @Override 
  protected void onLayout(boolean changed, int l, int t, int r, int b) { 
    ... 
  } 
} 

相關(guān)文章

最新評論