前言
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坐標系
代碼如下
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
}
}
效果如下
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)
效果如下
運行程序,我們設置的方塊會向右平移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()
效果如圖
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)效果
到這里view的滑動就基本差不多了
歡迎大家掃描關注作者公眾號逃贝,長期推送Android技術干貨,感謝大家支持: