[Android] 滑動操作的原理及處理

滑動效果的產(chǎn)生


滑動一個 View ,其實就是移動一個 View,本質(zhì)上是對 View 的坐標(biāo)位置進(jìn)行不停的改變。那么要實現(xiàn)這個效果,就必須要監(jiān)聽用戶的觸摸事件,根據(jù)傳入的事件類型和坐標(biāo),動態(tài)且不斷的改變 View 的坐標(biāo)。

以下Demo 在 github 均能獲取

首先,我們需要了解坐標(biāo)系的概念。

Android 坐標(biāo)系

在現(xiàn)實中,要描述一個物體的運(yùn)動,就需要一個參考系。所謂的滑動,就是相對于參考系的運(yùn)動。在 Android 中,將屏幕的左上角頂點(diǎn)作為 Android 坐標(biāo)系的原點(diǎn),從這個原點(diǎn)往右為 X 軸的正方向,從這個點(diǎn)往下是 Y 軸的正方向。

android坐標(biāo)系.png

在觸控事件中,使用 getRawX() 以及 getRawY() 可以獲取到當(dāng)前觸摸點(diǎn)相對于 Android 坐標(biāo)系的坐標(biāo)。

視圖坐標(biāo)系

除了上面說的這種坐標(biāo)系之外,還有一個視圖坐標(biāo)系。跟 Android 坐標(biāo)系類似,也是從這個原點(diǎn)往右為 X 軸的正方向,從這個點(diǎn)往下是 Y 軸的正方向,但是原點(diǎn)的位置不再是屏幕的左上角頂點(diǎn),而是父視圖的左上角坐標(biāo)原點(diǎn),找觸控事件中可以使用 getX() 以及 getY() 獲取到當(dāng)前觸摸點(diǎn)相對于視圖坐標(biāo)系中的坐標(biāo)。

視圖坐標(biāo)系.jpg

觸控事件 -- MotionEvent

觸控事件 MotionEvent 在用戶交互中十分重要的,MotionEvent 中封裝了一些事件常量:

觸摸按下動作
ACTION_DOWN

觸摸移動動作
ACTION_MOVE

觸摸動作取消
ACTION_CANCEL

觸摸動作離開
ACTION_UP

一般我們會在 onTouchEvent(MotionEvent event) 方法中通過傳進(jìn)來的 MotionEvent 引用的 getAction 方法來獲取時間的類型,并用 switch-case 的方法來進(jìn)行篩選,根據(jù)不同的時間進(jìn)行不同的邏輯操作。

通常模板如下:

public boolean onTouchEvent(MotionEvent event) {
  switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
      break;
    case MotionEvent.ACTION_MOVE:
      break;
    case MotionEvent.ACTION_UP:
      break;
  }
  return true; 
}

在 Android 中提供了很多的方法來獲取坐標(biāo)值,相對距離等,下面總結(jié)了一些 api 來看看在不同的坐標(biāo)系下面應(yīng)該如何使用。

這些方法可以分成如下兩個類型:

  • View 提供的獲取坐標(biāo)方法
    • getTop:獲取到的是 View 自身的頂邊到父布局頂邊的距離
    • getLeft:獲取到的是 View 自身的左邊到父布局左邊的距離
    • getRight:獲取到的是 View 自身的右邊到父布局右邊的距離
    • getBottom:獲取到的是 View 自身的底邊到父布局底邊的距離
  • MotionEvent 提供的獲取坐標(biāo)方法
    • getX : 獲取觸摸點(diǎn)距離當(dāng)前控件左邊的距離,也就是視圖坐標(biāo)
    • getY : 獲取觸摸點(diǎn)距離當(dāng)前控件頂邊的距離,也就是視圖坐標(biāo)
    • getRawX : 獲取觸摸點(diǎn)距離屏幕左邊的距離,也就是絕對坐標(biāo)
    • getRawY : 獲取觸摸點(diǎn)距離屏幕頂邊的距離,也就是絕對坐標(biāo)
各種方法獲取到的位置.jpg

實現(xiàn)滑動的幾種辦法


現(xiàn)在已經(jīng)了解關(guān)于坐標(biāo)系和觸控事件了,再來看看如何實現(xiàn)動態(tài)的修改一個 View 的坐標(biāo),即實現(xiàn)滑動效果。不管采用哪一種方式,實現(xiàn)的思路其實都是大致相同的。就是當(dāng)觸摸 View 的時候,系統(tǒng)記下當(dāng)前觸摸點(diǎn)的坐標(biāo);當(dāng)手指一動的時候,系統(tǒng)記下移動后的觸摸點(diǎn)坐標(biāo),兩次的相差就是這次移動的偏移量,然后通過偏移量來修改 View 的坐標(biāo)。這樣不斷重復(fù),就實現(xiàn)了滑動的過程。

流程圖.jpg

下面通過一個簡單的實例來實現(xiàn)這個效果,就是 View 隨著手指的滑動而滑動。這里我們需要自定義一個 View 并且重寫他的 onTouchEvent 方法。

跟隨觸摸滑動的View

layout 方法

在 View 繪制的過程中,會調(diào)用 onLayout 方法來定位,同樣,我們也可以手動調(diào)用此方法來對 View 進(jìn)行手動的坐標(biāo)定位。根據(jù)前面提供的思路,在按下的時候先保存一次觸摸按下時的坐標(biāo)。

  @Override public boolean onTouchEvent(MotionEvent event) {
    //檢測到觸摸事件后 第一時間得到相對于父控件的觸摸點(diǎn)坐標(biāo) 并賦值給x,y
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
      //觸摸事件中繞不開的第一步,必然執(zhí)行,將按下時的觸摸點(diǎn)坐標(biāo)賦值給 lastX 和 last Y
      case MotionEvent.ACTION_DOWN:
        lastX = x;
        lastY = y;
        break;
      //觸摸事件的第二步,這時候的x,y已經(jīng)隨著滑動操作產(chǎn)生了變化,用變化后的坐標(biāo)減去首次觸摸時的坐標(biāo)得到 相對的偏移量
      case MotionEvent.ACTION_MOVE:
        int offsetX = x - lastX;
        int offsetY = y - lastY;
        //使用 layout 進(jìn)行重新定位
        layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX,    getBottom() + offsetY);
        break;
    }
    return true;
  }

以上,通過代碼中的注釋,應(yīng)該都能明白操作的步驟了。

offsetLeftAndRight 與 offsetTopAndBottom

這兩個方法相當(dāng)于系統(tǒng)提供的一個對左右上下移動的 API 封裝,得到偏移量之后使用如下代碼就可以完成移動。

//使用 offsetLeftAndRight 和 offsetLeftAndRight 進(jìn)行偏移,從而移動view
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);

LayoutParams

LayoutParams 保存了一個 View 的布局參數(shù),通過改變 LayoutParams 來動態(tài)的修改一個布局的位置參數(shù),從而達(dá)到改變 View 位置的效果。我們可以很方便的在程序中使用 getLayoutParams 來獲取一個 View 的 LayoutParams。得到偏移量后,就可以通過 setLayoutParams 來改變。

RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin =getLeft()+offsetX;
layoutParams.topMargin = getTop()+ offsetY;
setLayoutParams(layoutParams);

這里的 RelativeLayout.LayoutParams 是根據(jù)你的父布局而定的 如果是 LinearLayout 的話就用 LinearLayout 的 LayoutParams。 當(dāng)然了 如果你連父布局都沒有,當(dāng)我沒說,那樣是不能用這個方法的。

scrollTo 與 scrollBy

這里其實 scrollTo 和 scrollBy 使用起來很簡單,但是理解起來稍微復(fù)雜一點(diǎn)。scrollTo 是直接移動到指定的坐標(biāo),而 scrollBy 是根據(jù)偏移量進(jìn)行相對移動。但是需要注意的是,這兩個都不是直接移動 View ,而是移動 View 中的 content ,比如 textView 中移動的是文字,imagView 中移動的是圖片,移動的是內(nèi)容,而不是本體。所以,我們應(yīng)該在想要移動的 View 的父布局中去使用它,用它來移動 ViewGroup 中的子 View。

上面說的只是其中一個難點(diǎn),還有一個難點(diǎn)就是參考系不同。
這樣理解吧,ViewGroup 是一個長方形的相框,在相框背后是一塊巨大的幕布,那么我們看到的內(nèi)容,就是相框中所能囊括下的內(nèi)容,在使用 scrollBy 進(jìn)行移動的時候,移動的是整個相框,而相框里的內(nèi)容沒動,但是因為相框移動了,所以內(nèi)容的位置也發(fā)生了變化。我們按照 X 軸將相框左邊移動的話,那相框中的內(nèi)容是在往右移動,所以在使用 scrollBy 的時候,內(nèi)容是往反方向運(yùn)動的,這里如果需要改為符合我們預(yù)期的移動方式,那么只需要將 scrollBy 的參數(shù)設(shè)置為負(fù)數(shù)即可。

迷之坐標(biāo).jpg

看看圖中星星的位置就能理解了,我們往正方向右下角移動了坐標(biāo),但是左圖中星星的位置在可視區(qū)域中是在往左上角移動,這是相反的。

((View) getParent()).scrollBy(-offsetX, -offsetY);

以上,就是使用 scrollBy 來進(jìn)行移動,注意參數(shù)使用負(fù)數(shù)即可。

Scroller

前面我們使用的不管是 scrollBy 還是 scrollTo ,移動其實都是在一瞬間完成的,只是因為我們的觸摸動作不斷觸發(fā),View 不斷改變位置,造成了一個過度動畫的假象。如果使用一個按鈕,比如點(diǎn)擊按鈕就會把 View 往右移動 100 像素,那么你會發(fā)現(xiàn)此時的 View 是瞬間變換位置,并不是慢慢移動到指定位置。這時候我們就需要 Scroller , Scroller 的內(nèi)部其實也是用 scrollTo 方法來實現(xiàn)的,但是它可以根據(jù)需要移動的總距離,以及設(shè)置的移動時間,計算出每一次需要移動的距離,然后不斷的進(jìn)行移動,這樣就實現(xiàn)了一個動畫的效果。

流程圖2.jpg

以下的流程圖是從手指抬起后開始。

下面我們使用 Scroller 來實現(xiàn)松開手指后 View 回到原來的位置的效果

彈彈彈

我們來看看以下代碼,基本所有的代碼的注釋我都已經(jīng)寫上,結(jié)合代碼理解應(yīng)該就能明白:

public class TestView1 extends View {
  //定義兩個變量用于存儲按下view時所處的坐標(biāo)
  int lastX = 0;
  int lastY = 0;

  //滑動~
  Scroller scroller;

  public TestView1(Context context, AttributeSet attrs) {
    super(context, attrs);
    scroller = new Scroller(context);
  }

  @Override public boolean onTouchEvent(MotionEvent event) {
    //檢測到觸摸事件后 第一時間得到相對于父控件的觸摸點(diǎn)坐標(biāo) 并賦值給x,y
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
      //觸摸事件中繞不開的第一步,必然執(zhí)行,將按下時的觸摸點(diǎn)坐標(biāo)賦值給 lastX 和 last Y
      case MotionEvent.ACTION_DOWN:
        lastX = x;
        lastY = y;
        break;
      //觸摸事件的第二步,這時候的x,y已經(jīng)隨著滑動操作產(chǎn)生了變化,用變化后的坐標(biāo)減去首次觸摸時的坐標(biāo)得到 相對的偏移量
      case MotionEvent.ACTION_MOVE:
        int offsetX = x - lastX;
        int offsetY = y - lastY;

        ((View) getParent()).scrollBy(-offsetX, -offsetY);
        break;

      //觸摸事件的第三步,必然執(zhí)行,手指抬起時候觸發(fā),這里會將移動過的view還原到原來的位置,并且有過度效果不是突然移動
      case MotionEvent.ACTION_UP:
        //因為下面要使用父視圖的引用來得到偏移量 所以要獲得一個父視圖引用
        View viewGroup = (View) getParent();

        //調(diào)用 startScroll 方法,參數(shù)為 起始X坐標(biāo),起始Y坐標(biāo),目的X坐標(biāo),目的Y坐標(biāo),過度動畫持續(xù)時間
        //這里使用了 viewGroup.getScrollX() 和 viewGroup.getScrollY() 作為起始坐標(biāo),ScrollY 和 ScrollX 記錄了使用 scrollBy 進(jìn)行偏移的量
        //所以使用他們就等于是使用了現(xiàn)在的坐標(biāo)作為起始坐標(biāo),目的坐標(biāo)為他們的負(fù)數(shù),就是偏移量為0的位置,也是view在沒有移動之前的位置
        scroller.startScroll(viewGroup.getScrollX(), 
        viewGroup.getScrollY(),
        -viewGroup.getScrollX(), 
        -viewGroup.getScrollY(), 
        800);

        //刷新view,這里很重要,如果不執(zhí)行,下面的 computeScroll 方法就不會執(zhí)行 computeScroll 方法是由 onDraw 方法調(diào)用的,而刷新 View 會調(diào)用 onDraw。
        invalidate();
        break;
    }
    return true;
  }

  @Override public void computeScroll() {

    //在上面嘗試刷新視圖之后被調(diào)用,并且執(zhí)行了 computeScrollOffset 方法,
    //此方法根據(jù)上面?zhèn)鬟M(jìn)來的起始坐標(biāo)和目的坐標(biāo)還有動畫時間,進(jìn)行計算每次移動的偏移量
    //如果到達(dá)目的坐標(biāo) false ,如果不為零 說明沒有到達(dá)目的坐標(biāo)
    if (scroller.computeScrollOffset()) {
      //使用 scrollTo 方法進(jìn)行移動,參數(shù)是從 scroller 的 getCurrX 以及 getCurrY 方法得到的,
      // 這兩個參數(shù)每次在執(zhí)行 computeScrollOffset 之后都會改變,會越來越接近目的坐標(biāo)。
      ((View) getParent()).scrollTo(scroller.getCurrX(), scroller.getCurrY());
    
      // 再次刷新 view 也等于是在循環(huán)執(zhí)行此方法 直到 computeScrollOffset 判斷到達(dá)目的坐標(biāo)為止,
      // 循環(huán)次數(shù)和每次移動的坐標(biāo)距離相關(guān),每次移動的坐標(biāo)距離又跟目的坐標(biāo)的距離和動畫時長有關(guān)
      //通常距離越長,動畫時間越長,循環(huán)次數(shù)越多

      invalidate();
    }
  }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 導(dǎo)語 滑動算是Android比較常用的效果了,滑動的操作具有很好的用戶體驗性。 主要內(nèi)容 滑動效果是如何產(chǎn)生的 實...
    一個有故事的程序員閱讀 6,561評論 3 11
  • 內(nèi)容是博主照著書敲出來的,博主碼字挺辛苦的,轉(zhuǎn)載請注明出處,后序內(nèi)容陸續(xù)會碼出。 當(dāng)了解了Android坐標(biāo)系和觸...
    Blankj閱讀 6,866評論 3 60
  • 開發(fā)中,為了增加更多炫麗的效果,我們經(jīng)常在應(yīng)用中添加滑動效果,今天就來分析一下 View 中滑動效果的實現(xiàn)原理以及...
    任教主來也閱讀 3,162評論 0 14
  • 什么是View View 是 Android 中所有控件的基類。 View的位置參數(shù) View 的位置由它的四個頂...
    acc8226閱讀 1,385評論 0 7
  • 鍛煉,養(yǎng)花 經(jīng)濟(jì),科研 -------- 鋼筆,太極 英語,看書 音樂,禪 難過的時候打打太極,想想你音樂和鋼筆字...
    XTJ閱讀 158評論 0 0

友情鏈接更多精彩內(nèi)容