Android View滑動總結

前言

View的滑動是Android自定義控件的基礎胆筒,在開發(fā)中我們難免會遇到View的滑動處理朽色。

其實不管是哪種滑動方式,基本思想都是差不多的:
1,當點擊事件傳到View時齿兔,系統(tǒng)記下觸摸點的坐標橱脸;
2,手指移動時系統(tǒng)記下移動后觸摸的坐標并算出偏移量,并通過偏移量來修改View的坐標愧驱;

實現(xiàn)View滑動有很多種方法慰技,這里主要講下以下6種:

  • 1,layout();
  • 2,offsetLeftAndRight()與offsetTopAndBottom();
  • 3,LayoutParams、
  • 4,動畫组砚、
  • 5,scollTo 與 scollBy吻商,
  • 6,Scroller。

1.layout()方式

首先看一下layout()糟红,在View進行繪制時艾帐,會調用OnLayout()方法來設置View顯示的位置,同時可以修改View的left盆偿,top,right,bottom四個屬性來控制View的坐標柒爸。這個坐標是View坐標系


a.png

代碼如下

class DragView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    private var lastX: Int = 0
    private var lastY: Int = 0

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val x = event.x.toInt()
        val y = event.y.toInt()
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                lastX = x
                lastY = y
            }
            MotionEvent.ACTION_MOVE -> {
                val offsetX = x - lastX
                val offsetY = y - lastX
                //四個參數(shù),left,top,right,bottom
                layout(left + offsetX, top + offsetY, right + offsetX, bottom + offsetY)
            }
        }
        return true

    }
}

效果如下

20180730174826839.gif

2,offsetLeftAndRight()與offsetTopAndBottom()方式

這種方式和layout()的方式基本是一樣的,根據(jù)名字也知道這兩個方法分別是設置左邊和右邊,上面和下面的偏離值

class DragView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private var lastX: Int = 0
    private var lastY: Int = 0

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val x = event.x.toInt()
        val y = event.y.toInt()
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                lastX = x
                lastY = y
            }
            MotionEvent.ACTION_MOVE -> {
                val offsetX = x - lastX
                val offsetY = y - lastX
                offsetLeftAndRight(offsetX)
                offsetTopAndBottom(offsetY)
            }
        }
        return true
    }



}

這樣就可以了

3,LayoutParams的方式

LayoutParams主要保存了一個View的布局參數(shù)事扭,因此我們可以通過LayoutParams來改變View的布局參
數(shù)從而達到改變View位置的效果;

class DragView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    private var lastX: Int = 0
    private var lastY: Int = 0

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val x = event.x.toInt()
        val y = event.y.toInt()
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                lastX = x
                lastY = y
            }
            MotionEvent.ACTION_MOVE -> {
                val offsetX = x - lastX
                val offsetY = y - lastX
                val layoutParams = this.layoutParams as ViewGroup.MarginLayoutParams
                layoutParams.leftMargin = left + offsetX
                layoutParams.topMargin = top + offsetY
                setLayoutParams(layoutParams)
            }
        }
        return true

    }
}

這里如果viewgroup是relayoutlayout,如果設置了android:layout_centerInParent="true"是不起作用的;

4.動畫方式

采用動畫的方式來進行View的滑動捎稚,主要就涉及到兩個動畫:屬性動畫和補間動畫
補間動畫的實現(xiàn)

  • 通過xml的方式
    在res目錄新建anim文件夾并創(chuàng)建translate.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"   android:duration="2000">
    <translate
        android:fromXDelta="0"
        android:toXDelta="300"/>
</set>

然后在代碼中調用

  dragView.animation = AnimationUtils.loadAnimation(this, R.anim.translate)

效果如下

c.gif

運行程序,我們設置的方塊會向右平移300像素求橄,然后又會回到原來的位置今野。為了解決這個問題,我們
需要在translate.xml中加上fillAfter="true"罐农,代碼如下所示条霜。運行代碼后會發(fā)現(xiàn),方塊向右平移300像素后就
停留在當前位置了涵亏。

<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" 
     android:duration="2000"
     android:fillAfter="true">
    <translate
        android:fromXDelta="0"
        android:toXDelta="300"/>
</set>

需要注意的是宰睡,View動畫并不能改變View的位置參數(shù)。如果對一個Button進行如上的平移動畫操作气筋,
當Button平移300像素停留在當前位置時拆内,我們點擊這個Button并不會觸發(fā)點擊事件,但在我們點擊這個
Button的原始位置時卻觸發(fā)了點擊事件宠默。這就是補間動畫和屬性動畫的區(qū)別

當然我們也可以直接通過代碼來實現(xiàn)矛纹,不用xml,主要使用的類為TranslateAnimation:

   val animation = TranslateAnimation(0f, 300f, 0f, 0f)
        animation.duration = 2000//設置動畫持續(xù)時間 
        animation.fillAfter=true
        dragView.animation =animation

效果和上面是一樣的

屬性動畫

屬性動畫是Android3.0之后推出的光稼,就是為了解決補間動畫的焦點問題或南,我們經常使用到的類主要有AnimatorSet和ObjectAnimator;使用 ObjectAnimator 進行更精細化的控制艾君,控制一個對象和一個屬性值采够,而使用多個ObjectAnimator組合到AnimatorSet形成一個動畫。屬性動畫通過調用屬性get冰垄、set方法來真實地控制一個View的屬性值蹬癌,因此,強大的屬性動畫框架基本可以實現(xiàn)所有的動畫效果虹茶。

ObjectAnimator

ObjectAnimator 是屬性動畫最重要的類逝薪,創(chuàng)建一個 ObjectAnimator 只需通過其靜態(tài)工廠類直接返還一個
ObjectAnimator對象。參數(shù)包括一個對象和對象的屬性名字蝴罪,但這個屬性必須有get和set方法董济,其內部會通
過Java反射機制來調用set方法修改對象的屬性值。下面看看平移動畫是如何實現(xiàn)的要门,代碼如下所示:

       val animation = ObjectAnimator.ofFloat(dragView, "translationX", 300f)
        animation.duration = 2000
        animation.start()

這和補間動畫的加了fillAfter屬性后動畫效果是一樣的虏肾,但是不一樣的地方就是焦點已經平移到了新的位置;

注:
需要注意的是欢搜,在使用ObjectAnimator的時候封豪,要操作的屬性必須要有get和set方法,不然
ObjectAnimator 就無法生效炒瘟。如果一個屬性沒有get吹埠、set方法,也可以通過自定義一個屬性類或包裝類來間
接地給這個屬性增加get和set方法〈埃現(xiàn)在來看看如何通過包裝類的方法給一個屬性增加get和set方法缘琅,代碼如
下所示:

class MyView(var mTarget: View) {

    fun getWidth(): Int {
        return mTarget.layoutParams.width
    }

    fun setWidth(width: Int) {
        mTarget.layoutParams.width = width
        mTarget.requestLayout()
    }
}

這里我們設置2秒增加view的寬度300個像素,使用時只需要操作包類就可以調用get斩个、set方法了:

     val myView = MyView(dragView)
     ObjectAnimator.ofInt(myView, "width", 300).setDuration(2000).start()

效果如圖

d.gif
ValueAnimator

ValueAnimator不提供任何動畫效果胯杭,它更像一個數(shù)值發(fā)生器,用來產生有一定規(guī)律的數(shù)字受啥,從而讓調
用者控制動畫的實現(xiàn)過程做个。通常情況下,在ValueAnimator的AnimatorUpdateListener中監(jiān)聽數(shù)值的變化滚局,從
而完成動畫的變換居暖,代碼如下所示:

        val valueAnimator = ValueAnimator.ofFloat(0f, 100f)
        valueAnimator.setTarget(dragView)
        valueAnimator.duration = 2000
        valueAnimator.start()
        valueAnimator.addUpdateListener({
            val animatedValue = it.animatedValue as Float
        })

這個代碼在自定義控件用的很多,特別是進度條的自定義控件藤肢;

5.scrollTo與scollBy

scrollTo(x太闺,y)表示移動到一個具體的坐標點,而scrollBy(dx嘁圈,dy)則表示移動的增量為dx省骂、dy蟀淮。其
中,scollBy最終也是要調用scollTo的钞澳。View.java的scollBy和scollTo的源碼如下所示:

    /**
     * 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);
    }
    
   /**
     * 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();
            }
        }
    }

scollTo怠惶、scollBy移動的是View的內容,如果在ViewGroup中使用轧粟,則是移動其所有的子View策治。
代碼如下

class DragView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    private var lastX: Int = 0
    private var lastY: Int = 0

    override fun onTouchEvent(event: MotionEvent): Boolean {
        val x = event.x.toInt()
        val y = event.y.toInt()
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                lastX = x
                lastY = y
            }
            MotionEvent.ACTION_MOVE -> {
                val offsetX = x - lastX
                val offsetY = y - lastX
                (parent as View).scrollBy(-offsetX, -offsetY)
            }
        }
        return true
    }
}

這樣就實現(xiàn)了和layout等一樣的拖動效果
注:至于這里為什么是負數(shù),其實是參照物不一樣導致的兰吟,scrollby和scrollto滾動的時候其實是類似于窗口里面的View并沒有移動通惫,而是手機屏幕在移動,如果我設置為正數(shù)混蔼,其實類似手機屏幕往右移動了履腋,view保持不動,這樣間接的其實類似于View左移了拄丰;

5.Scroller

我們在用scollTo/scollBy方法進行滑動時府树,這個過程是瞬間完成的,所以用戶體驗不大好料按。這里我們可
以使用 Scroller 來實現(xiàn)有過渡效果的滑動奄侠,這個過程不是瞬間完成的,而是在一定的時間間隔內完成的载矿。
Scroller本身是不能實現(xiàn)View的滑動的垄潮,它需要與View的computeScroll()方法配合才能實現(xiàn)彈性滑動的效
果。

  • computeScroll()
    系統(tǒng)會在繪制View的時候在draw()方法中調用computeScroll()方法闷盔。在這個方法中弯洗,我們調用父類的scrollTo()方法并通過Scroller來不斷獲取當前的滾動值,每滑動一小段距離我們就調用invalidate()方法不斷地進行重繪逢勾,重繪就會調用computeScroll()方法牡整,這樣我們通過不斷地移動一個小的距離并連貫起來就實現(xiàn)了平滑移動的效果。

寫個demo我們將view平滑滾動帶屏幕坐標(400,500)的位置溺拱,具體代碼如下

class DragView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    private var scroller: Scroller

    init {
        scroller = Scroller(context)
    }
    override fun computeScroll() {
        super.computeScroll()
        if (scroller.computeScrollOffset()) {
            (parent as View).scrollTo(scroller.currX, scroller.currY)
            invalidate()
        }
    }

    fun smoothScrollTo(destX: Int, destY: Int) {
        val scrollX = this.scrollX
        val scrollY = this.scrollY
        //偏移量
        val deltaX = destX - scrollX
        val deltaY = destY - scrollY
        scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 2000)
    }

}

調用

 dragView.smoothScrollTo(-400, -500)

實現(xiàn)效果


e.gif

到這里view的滑動就基本差不多了

歡迎大家掃描關注作者公眾號逃贝,長期推送Android技術干貨,感謝大家支持:


qrcode_for_gh_c5f1738c50f5_344.jpg
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末迫摔,一起剝皮案震驚了整個濱河市沐扳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌句占,老刑警劉巖沪摄,帶你破解...
    沈念sama閱讀 222,865評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡杨拐,警方通過查閱死者的電腦和手機祈餐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,296評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來戏阅,“玉大人昼弟,你說我怎么就攤上這事∞瓤穑” “怎么了?”我有些...
    開封第一講書人閱讀 169,631評論 0 364
  • 文/不壞的土叔 我叫張陵变骡,是天一觀的道長离赫。 經常有香客問我,道長塌碌,這世上最難降的妖魔是什么渊胸? 我笑而不...
    開封第一講書人閱讀 60,199評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮台妆,結果婚禮上翎猛,老公的妹妹穿的比我還像新娘。我一直安慰自己接剩,他們只是感情好切厘,可當我...
    茶點故事閱讀 69,196評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著懊缺,像睡著了一般疫稿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鹃两,一...
    開封第一講書人閱讀 52,793評論 1 314
  • 那天遗座,我揣著相機與錄音,去河邊找鬼俊扳。 笑死途蒋,一個胖子當著我的面吹牛,可吹牛的內容都是我干的馋记。 我是一名探鬼主播号坡,決...
    沈念sama閱讀 41,221評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼抗果!你這毒婦竟也來了筋帖?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 40,174評論 0 277
  • 序言:老撾萬榮一對情侶失蹤冤馏,失蹤者是張志新(化名)和其女友劉穎日麸,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 46,699評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡代箭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,770評論 3 343
  • 正文 我和宋清朗相戀三年墩划,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嗡综。...
    茶點故事閱讀 40,918評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡乙帮,死狀恐怖,靈堂內的尸體忽然破棺而出极景,到底是詐尸還是另有隱情察净,我是刑警寧澤,帶...
    沈念sama閱讀 36,573評論 5 351
  • 正文 年R本政府宣布盼樟,位于F島的核電站氢卡,受9級特大地震影響,放射性物質發(fā)生泄漏晨缴。R本人自食惡果不足惜译秦,卻給世界環(huán)境...
    茶點故事閱讀 42,255評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望击碗。 院中可真熱鬧筑悴,春花似錦、人聲如沸稍途。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,749評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽晰房。三九已至求摇,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間殊者,已是汗流浹背与境。 一陣腳步聲響...
    開封第一講書人閱讀 33,862評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留猖吴,地道東北人摔刁。 一個月前我還...
    沈念sama閱讀 49,364評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像海蔽,于是被迫代替她去往敵國和親共屈。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,926評論 2 361

推薦閱讀更多精彩內容