View的滑動實現方式

轉載請以連接形式標明出處: 本文出自:103style的博客java

《Android開發藝術探索》 學習記錄android

base on Android-29bash


能夠帶着如下問題來看本文:ide

  • scrollTo 和 scrollBy 改變是 View 的什麼屬性?
  • 補間動畫和屬性動畫的使用?
  • 如何改變 View 的LayoutParams ?
  • Scroller實現平滑滑動的原理?

目錄

  • scrollTo 和 scrollBy
  • 使用動畫
  • 改變佈局參數
  • 彈性滑動Scroller
  • 問題的解答

scrollTo 和 scrollBy

咱們先來看看這兩個方法的源碼:函數

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();
        }
    }
}
public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}
複製代碼

咱們能夠看到 scrollBy 也是調用了 scrollTo 方法。佈局

首先咱們先看下 mScrollX 和 mScrollY 的示例圖: post

mScrollX,mScrollY 示例圖

經過上圖,咱們能夠很明顯的看出:學習

當View的內容往左往上時,mScrollX 和 mScrollY 爲正。動畫

當View的內容往右往下時,mScrollX 和 mScrollY 爲負。ui

也就是說在View的座標系中, mScrollX、mScrollY 分別爲View的邊緣減去對應內容邊緣的大小

而且 scrollTo 和 scrollBy 改變的是其內容的位置,而不是其在佈局中的位置!

咱們來看個示例:

//activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/bt_anim"
        android:layout_width="160dp"
        android:layout_height="160dp"
        android:text="Anim"
        android:textAllCaps="false" />
</LinearLayout>
複製代碼
//MainActivity.java
public class MainActivity extends AppCompatActivity {
    private Button btAnim;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btAnim = findViewById(R.id.bt_anim);
        btAnim.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                btAnim.scrollTo(100, 100);
            }
        });
    }
}
複製代碼

點擊能夠明顯看到,內容往左上方移動了。

scrollTo(100, 100)
咱們把 btAnim.scrollTo(100, 100); 改爲 btAnim.scrollTo(-100, -100); 看看,能夠看到內容往右下方移動了。
scrollTo(-100, -100)


使用動畫

動畫這塊咱們後面會單獨具體介紹,這裏先簡單介紹下怎麼使用動畫來實現滑動。

還記得咱們在 View的基礎知識介紹 中說到的View的位置參數中的 translationXtranslationY 嗎?動畫實現滑動就是改變這個屬性的值。

下面經過補間動畫和屬性動畫來實現View的滑動:

// res/anim/translation.xml
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:fromXDelta="0"
    android:fromYDelta="0"
    android:toXDelta="400"
    android:toYDelta="400" />
複製代碼

這個補間動畫的意思是 將 View 從 (0,0) 在 2s 內移動到 (400,400)

屬性動畫的用法則爲: ObjectAnimator.ofFloat(view, "translationX", 0, 400).setDuration(2000).start(); 意思是 將 view 的 translationX 屬性在兩秒內從 0 移動到 400.

//MainActivity.java
public class MainActivity extends AppCompatActivity {
    private Button btAnim;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btAnim = findViewById(R.id.bt_anim);
        btAnim.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Animation animation = AnimationUtils.loadAnimation(MainActivity.this, R.anim.translation);
                btAnim.startAnimation(animation);
            }
        });
        btAnim.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                ObjectAnimator
                        .ofFloat(btAnim, "translationX", 0, 400)
                        .setDuration(2000)
                        .start();
                return true;
            }
        });
    }
}
複製代碼

上面代碼中咱們經過監聽 btAnim 的點擊事件來觸發 補間動畫,監聽長按事件來觸發 屬性動畫。

效果圖

這裏咱們須要注意的是:

補間動畫 實現的平移 實際上只是對View的影像作操做,並不會真正改變View的位置參數。

若是咱們添加 android:fillAfter="true" 的話,當動畫結束後,則會停在最後的位置。

此時你會發現一個問題,當咱們再次點擊View時,並不會觸發動畫效果,可是點擊以前的位置則會觸發

咱們修改例子來看看。

// res/anim/translation.xml
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:fillAfter="true"
    android:fromXDelta="0"
    android:fromYDelta="0"
    android:toXDelta="400"
    android:toYDelta="400" />
複製代碼
//MainActivity.java
public class MainActivity extends AppCompatActivity {
    private Button btAnim;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btAnim = findViewById(R.id.bt_anim);
        btAnim.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                show();
                Animation animation = AnimationUtils.loadAnimation(MainActivity.this, R.anim.translation);
                btAnim.startAnimation(animation);
            }
        });
        btAnim.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                show();
                ObjectAnimator
                        .ofFloat(btAnim, "translationX", 0, 400)
                        .setDuration(2000)
                        .start();
                return true;
            }
        });
    }
    private void show(){
        Toast.makeText(this,"start anim", Toast.LENGTH_SHORT).show();
    }
}
複製代碼

補間動畫

屬性動畫

咱們能夠看到 補間動畫 完了以後,只有點擊以前所在位置才能觸發點擊事件,

而 屬性動畫 則只有點到View所在的位置纔會觸發長按事件。

是什麼緣由咱們後面在將動畫的時候再介紹吧。


改變佈局參數

改變佈局參數很簡單,就是改變其 LayoutParams,咱們能夠經過 View.getLayoutParams() 來獲取這個佈局參數,而後修改屬性, 再經過 setLayoutParams() 或者 requestLayout() 來從新佈局。

具體咱們來下面這個示例:

//MainActivity.java
public class MainActivity extends AppCompatActivity {
    private Button btAnim;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btAnim = findViewById(R.id.bt_anim);
        btAnim.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                changeLayoutParams();
            }
        });
    }
    private void changeLayoutParams() {
        LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams) btAnim.getLayoutParams();
        llp.width += 100;
        llp.height += 100;
        llp.setMarginStart(llp.getMarginStart() + 50);
        llp.topMargin += 50;
        btAnim.setLayoutParams(llp);
//        btAnim.requestLayout();
    }
}
複製代碼

咱們經過每次點擊使其 寬高 增長 100px, 左邊距 和 上邊距 增長 50px,效果圖以下:

改變位置參數的效果圖


這裏咱們先來總結下上面三種實現滑動的方法:

  1. scollTo/scollBy : 操做簡單,只能移動View的內容。
  2. 動畫:操做簡單,主要用於沒有交互的View 和 複雜的動畫效果
  3. 改變佈局參數:操做稍微複雜,適用於有交互的View.

經過效果圖,咱們能夠很明顯的看到 scollTo/scollBy 和 改變佈局參數 這兩種實現滑動的方法 效果比較生硬,用戶體驗不太好。 動畫 方式實現的效果則會體驗好不少。

下面咱們來介紹經過 Scroller 來實先動畫那樣用戶體驗相對較好的 的滑動效果。


彈性滑動Scroller

咱們在 View的基礎知識介紹 中有介紹 Scroller 的用法,再從新回顧下:

  • 建立一個Scroller;
  • 重寫 viewcomputeScroll 方法;
  • 而後經過 mScroller.startScroll()來實現滑動。
//TestScroller.java
public class TestScroller extends TextView {
    Scroller mScroller;
    public TestScroller(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
    }
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }
    public void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();
        int scrollY = getScrollY();
        int deltaX = destX - scrollX;
        int deltaY = destY - scrollY;
        mScroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000);
        invalidate();
    }
}
複製代碼
//activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <com.lxk.viewdemo.TestScroller
        android:id="@+id/tv"
        android:layout_width="320dp"
        android:layout_height="320dp"
        android:layout_margin="8dp"
        android:background="@color/colorAccent"
        android:gravity="center"
        android:padding="8dp"
        android:text="Hello World!" />
</LinearLayout>
複製代碼
//MainActivity.java
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final TestScroller scroller = findViewById(R.id.tv);
        scroller.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                scroller.smoothScrollTo(200, 200);
            }
        });
}
複製代碼

運行,能夠看到點擊以後,內容在 1s 內往左上方各平移了 200px而且改變的也是View的內容

Scroll

首先咱們來看看 smoothScrollTo 中調用的 ScrollerstartScroll 方法,咱們能夠看到它其實只是保存咱們傳入的參數。

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;
}
複製代碼

而後經過在 smoothScrollTo 調用 invalidate() 方法,經過 invalidate() 觸發重繪,來調用 computeScroll 方法,

而後經過Scroller.computeScrollOffset()判斷狀態,

知足則經過 mScroller.getCurrX()mScroller.getCurrY() 獲取當前的位置,

而後經過 scrollTo 實現滑動,

而後經過 postInvalidate() 來繼續觸發重繪。

咱們來看看 ScrollercomputeScrollOffset()方法:

//Scroller.java
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 {
        mCurrX = mFinalX;
        mCurrY = mFinalY;
        mFinished = true;
    }
    return true;
}
複製代碼

咱們能夠看到裏面經過插值器來計算對應時間對應的 mCurrXmCurrY

mInterpolator.getInterpolation(timePassed * mDurationReciprocal);

而後經過重繪來達到平滑滑動的效果。

插值器 屬於 動畫那塊的內容,咱們在將動畫的時候在具體介紹,暫時當它是一個數學函數就能夠了。

至此,咱們大體知道了 Scroller實現滑動的原理爲: 咱們經過 Scroller 的 startScroll() 來設置要滑動的位置, 而後經過 invalidate() 觸發重繪 來調用 View 的 computeScroll() 方法, 而後在 Scroller 的computeScrollOffset()中 經過插值器計算 這個滑動時間中 每一個時間點對應的 目標距離, 而後再經過 scrollTo() 滑動這個時間點對應的距離, 而後繼續重繪到對應時間點來實現滑動。

因此實際上 Scroller 自己並不能實現View的滑動,他須要配合View的 computeScroll() 方法才能達到平滑滑動的效果。


問題解答

  • scrollTo 和 scrollBy 改變是 View 的什麼屬性?

    A:mScrollX 和 mScrollY,內容往上往左滑 這兩個值爲正, 反之爲負。

  • 補間動畫和屬性動畫的使用?

    A: 補間動畫: 經過 AnimationUtils.loadAnimation(context, R.anim.translation) 來獲取補間動畫,而後經過 view.startAnimation(animation) 來執行動畫,補間動畫進改變View的影像,並不改變其實際位置,因此點擊事件只有點擊原位置纔會響應。

    屬性動畫:經過 ObjectAnimator.ofFloat(view, 對應屬性, 起始值, 結束值).setDuration(時間).start() 來實現。

  • 如何改變 View 的LayoutParams ?

    A:經過 View.getLayoutParams() 獲取LayoutParams,而後修改寬高、邊距等,再經過 setLayoutParams() 或者 requestLayout() 來從新佈局。

  • Scroller實現平滑滑動的原理?

    A:問題解答標題 上面就有,就再也不贅述了。


若是以爲不錯的話,請幫忙點個讚唄。

以上


掃描下面的二維碼,關注個人公衆號 Android1024, 點關注,不迷路。

Android1024

`

相關文章
相關標籤/搜索