參考鏈接 向劉老師學(xué)習(xí)英染,所以摘抄了老師的筆記毕泌,只想作為自己的技術(shù)積累蛙奖。
簡介
不管是哪種滑動(dòng)的方式基本思想都是類似的:當(dāng)觸摸事件傳到View時(shí)吹艇,系統(tǒng)記下觸摸點(diǎn)的坐標(biāo),手指移動(dòng)時(shí)系統(tǒng)記下移動(dòng)后的觸摸的坐標(biāo)并算出偏移量驮履,并通過偏移量來修改View的坐標(biāo)矫夷。
實(shí)現(xiàn)View滑動(dòng)有很多種方法,這篇文章主要講解六種滑動(dòng)的方法阱飘,分別是:layout()
、offsetLeftAndRight()與offsetTopAndBottom()
师崎、LayoutParams
丐巫、動(dòng)畫
治筒、scollTo與scollBy
和Scroller
启绰。
實(shí)現(xiàn) View 滑動(dòng)的六種方法
layout()
view進(jìn)行繪制的時(shí)候會(huì)調(diào)用onLayout()方法來設(shè)置顯示的位置,因此我們同樣也可以通過修改View的left脸侥、top滩援、right、bottom這四種屬性來控制View的坐標(biāo)谅辣。首先我們要自定義一個(gè)View,在onTouchEvent()方法中獲取觸摸點(diǎn)的坐標(biāo):
public boolean onTouchEvent(MotionEvent event) {
//獲取到手指處的橫坐標(biāo)和縱坐標(biāo)
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
...
接下來我們?cè)贏CTION_MOVE事件中計(jì)算偏移量厦幅,再調(diào)用layout()方法重新放置這個(gè)自定義View的位置就好了:
case MotionEvent.ACTION_MOVE:
//計(jì)算移動(dòng)的距離
int offsetX = x - lastX;
int offsetY = y - lastY;
//調(diào)用layout方法來重新放置它的位置
layout(getLeft()+offsetX, getTop()+offsetY,
getRight()+offsetX , getBottom()+offsetY);
break;
當(dāng)我們每次移動(dòng)時(shí)都會(huì)調(diào)用layout()方法來對(duì)自己重新布局篙骡,從而達(dá)到移動(dòng)View的效果。
View的寬高是有top、left夺欲、right、bottom參數(shù)決定褐着。
自定義View的全部代碼(MovingView.kt):
package com.kevin.viewmovingtest
import android.content.Context
import android.support.constraint.ConstraintLayout
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.widget.LinearLayout
/**
* Created by Kobe on 2017/10/11.
* Moving view
*/
class MovingView : View {
companion object {
private val TAG = "MovingView"
}
// 記錄點(diǎn)擊時(shí)的坐標(biāo)
private var lastX = 0
private var lastY = 0
// 構(gòu)造函數(shù)
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
// 觸碰監(jiān)聽
override fun onTouchEvent(event: MotionEvent?): Boolean {
event?.let {
// 獲取當(dāng)前坐標(biāo)
val currentX: Int = event.x.toInt()
val currentY: Int = event.y.toInt()
when (event.action) {
MotionEvent.ACTION_DOWN -> {
Log.d(TAG, "MotionEvent.ACTION_DOWN")
lastX = currentX
lastY = currentY
}
MotionEvent.ACTION_MOVE -> {
Log.d(TAG, "MotionEvent.ACTION_MOVE")
// 移動(dòng)距離
val offsetX = currentX - lastX
val offsetY = currentY - lastY
// 設(shè)置View的坐標(biāo)
Log.d(TAG, "Left:$left,Right:$right,Top:$top,Bottom:$bottom")
// 使用layout()
// layout(left + offsetX, top + offsetY, right + offsetX, bottom + offsetY)
// 使用offsetLeftAndRight()和offsetTopAndBottom()
// offsetLeftAndRight(offsetX)
// offsetTopAndBottom(offsetY)
// 使用LayoutParams
val lp: ConstraintLayout.LayoutParams = layoutParams as ConstraintLayout.LayoutParams
lp.leftMargin = offsetX + left
lp.topMargin = offsetY + top
layoutParams = lp
}
}
}
return true
}
}
布局中引用自定義View:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/cl_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.kevin.viewmovingtest.MainActivity">
<com.kevin.viewmovingtest.MovingView
android:id="@+id/mv_test"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@color/colorPrimary"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="0dp"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="0dp"
android:layout_marginRight="0dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintVertical_bias="0.0"
android:layout_marginLeft="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintHorizontal_bias="0.0" />
</android.support.constraint.ConstraintLayout>
offsetLeftAndRight()與offsetTopAndBottom()
這兩種方法和layout()方法效果方法差不多掌腰,使用也差不多狰住,我們將ACTION_MOVE中的代碼替換成如下代碼:
case MotionEvent.ACTION_MOVE:
//計(jì)算移動(dòng)的距離
int offsetX = x - lastX;
int offsetY = y - lastY;
//對(duì)left和right進(jìn)行偏移
offsetLeftAndRight(offsetX);
//對(duì)top和bottom進(jìn)行偏移
offsetTopAndBottom(offsetY);
break;
上面代碼塊對(duì)應(yīng)的變化如下:
MotionEvent.ACTION_MOVE -> {
Log.d(TAG, "MotionEvent.ACTION_MOVE")
// 移動(dòng)距離
val offsetX = currentX - lastX
val offsetY = currentY - lastY
// 設(shè)置View的坐標(biāo)
Log.d(TAG, "Left:$left,Right:$right,Top:$top,Bottom:$bottom")
// layout(left + offsetX, top + offsetY, right + offsetX, bottom + offsetY)
offsetLeftAndRight(offsetX)
offsetTopAndBottom(offsetY)
}
LayoutParams(改變布局參數(shù))
LayoutParams主要保存了一個(gè)View的布局參數(shù),因此我們可以通過LayoutParams來改變View的布局的參數(shù)從而達(dá)到了改變View的位置的效果齿梁。同樣的我們將ACTION_MOVE中的代碼替換成如下代碼:
LinearLayout.LayoutParams layoutParams= (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
上面對(duì)應(yīng)的代碼塊變化如下催植,由于我這里使用ConstraintLayout
,所以和劉老師的有點(diǎn)不同勺择,需要在布局文件中做好約束:
MotionEvent.ACTION_MOVE -> {
Log.d(TAG, "MotionEvent.ACTION_MOVE")
// 移動(dòng)距離
val offsetX = currentX - lastX
val offsetY = currentY - lastY
// 設(shè)置View的坐標(biāo)
Log.d(TAG, "Left:$left,Right:$right,Top:$top,Bottom:$bottom")
// 使用layout()
// layout(left + offsetX, top + offsetY, right + offsetX, bottom + offsetY)
// 使用offsetLeftAndRight()和offsetTopAndBottom()
// offsetLeftAndRight(offsetX)
// offsetTopAndBottom(offsetY)
// 使用LayoutParams
val lp: ConstraintLayout.LayoutParams = layoutParams as ConstraintLayout.LayoutParams
lp.leftMargin = offsetX + left
lp.topMargin = offsetY + top
layoutParams = lp
}
動(dòng)畫
可以采用View動(dòng)畫來移動(dòng)创南,在res目錄新建anim文件夾并創(chuàng)建translate.xml:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0" android:toXDelta="300" android:duration="1000"/>
</set>
在代碼中引用:
mv_test.animation = AnimationUtils.loadAnimation(this, R.anim.translate)
ObjectAnimator.ofFloat(mv_test, "translationX", 0.toFloat(), 300.toFloat()).setDuration(1000).start()
使用scrollTo與scrollBy
- scollTo(x,y)表示移動(dòng)到一個(gè)具體的坐標(biāo)點(diǎn)
- scollBy(dx,dy)則表示移動(dòng)的增量為dx、dy
scollTo省核、scollBy移動(dòng)的是View的內(nèi)容稿辙,如果在ViewGroup中使用則是移動(dòng)他所有的子View。我們將ACTION_MOVE中的代碼替換成如下代碼:
((View)getParent()).scrollBy(-offsetX,-offsetY);
這里要實(shí)現(xiàn)CustomView隨著我們手指移動(dòng)的效果的話气忠,我們就需要將偏移量設(shè)置為負(fù)值邻储。
上面對(duì)應(yīng)的代碼塊變化如下:
MotionEvent.ACTION_MOVE -> {
Log.d(TAG, "MotionEvent.ACTION_MOVE")
// 移動(dòng)距離
val offsetX = currentX - lastX
val offsetY = currentY - lastY
// 設(shè)置View的坐標(biāo)
Log.d(TAG, "Left:$left,Right:$right,Top:$top,Bottom:$bottom")
// 使用layout()
// layout(left + offsetX, top + offsetY, right + offsetX, bottom + offsetY)
// 使用offsetLeftAndRight()和offsetTopAndBottom()
// offsetLeftAndRight(offsetX)
// offsetTopAndBottom(offsetY)
// 使用LayoutParams
// val lp: ConstraintLayout.LayoutParams = layoutParams as ConstraintLayout.LayoutParams
// lp.leftMargin = offsetX + left
// lp.topMargin = offsetY + top
// layoutParams = lp
// 使用scrollTo與scrollBy
(parent as View).scrollBy(-offsetX, -offsetY)
}
Scroller
用scollTo/scollBy方法來進(jìn)行滑動(dòng)時(shí),這個(gè)過程是瞬間完成的旧噪,所以用戶體驗(yàn)不大好吨娜。這里我們可以使用Scroller來實(shí)現(xiàn)有過度效果的滑動(dòng),這個(gè)過程不是瞬間完成的淘钟,而是在一定的時(shí)間間隔完成的宦赠。Scroller本身是不能實(shí)現(xiàn)View的滑動(dòng)的,它需要配合View的computeScroll()方法才能彈性滑動(dòng)的效果日月。
在這里我們實(shí)現(xiàn)CustomView平滑的向右移動(dòng)袱瓮。
- 首先我們要初始化Scroller:
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
- 接下來重寫
computeScroll()
方法,系統(tǒng)會(huì)在繪制View的時(shí)候在draw()
方法中調(diào)用該方法爱咬,這個(gè)方法中我們調(diào)用父類的scrollTo()
方法并通過Scroller來不斷獲取當(dāng)前的滾動(dòng)值尺借,每滑動(dòng)一小段距離我們就調(diào)用invalidate()
方法不斷的進(jìn)行重繪,重繪就會(huì)調(diào)用computeScroll()
方法精拟,這樣我們就通過不斷的移動(dòng)一個(gè)小的距離并連貫起來就實(shí)現(xiàn)了平滑移動(dòng)的效果:
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
((View) getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
//通過不斷的重繪不斷的調(diào)用computeScroll方法
invalidate();
}
}
- 調(diào)用
Scroller.startScroll()
方法燎斩。我們?cè)贑ustomView中寫一個(gè)smoothScrollTo()
方法虱歪,調(diào)用Scroller.startScroll()
方法,在2000毫秒內(nèi)沿X軸平移delta像素:
public void smoothScrollTo(int destX,int destY){
int scrollX=getScrollX();
int delta=destX-scrollX;
//1000秒內(nèi)滑向destX
mScroller.startScroll(scrollX,0,delta,0,2000);
invalidate();
}
- 最后我們?cè)赩iewSlideActivity.java中調(diào)用CustomView的smoothScrollTo()方法:
//使用Scroll來進(jìn)行平滑移動(dòng)
mCustomView.smoothScrollTo(-400,0);
這里我們是設(shè)定CustomView沿著X軸向右平移400像素栅表。
invalidate()
說明一下
說明:請(qǐng)求重繪View樹笋鄙,即draw()
過程,假如視圖發(fā)生大小沒有變化就不會(huì)調(diào)用layout()
過程怪瓶,并且只繪制那些“需要重繪的”視圖萧落,即誰(View的話,只繪制該View 洗贰;ViewGroup找岖,則繪制整個(gè)ViewGroup)請(qǐng)求invalidate()
方法,就繪制該視圖敛滋。
一般引起invalidate()
操作的函數(shù)如下:
- 直接調(diào)用
invalidate()
方法许布,請(qǐng)求重新draw()
,但只會(huì)繪制調(diào)用者本身绎晃。setSelection()
方法 :請(qǐng)求重新draw()
蜜唾,但只會(huì)繪制調(diào)用者本身。setVisibility()
方法 : 當(dāng)View可視狀態(tài)在INVISIBLE轉(zhuǎn)換VISIBLE時(shí)庶艾,會(huì)間接調(diào)用invalidate()
方法袁余,繼而繪制該View。setEnabled()
方法 : 請(qǐng)求重新draw()
咱揍,但不會(huì)重新繪制任何視圖包括該調(diào)用者本身泌霍。
按照上述,重新修改了代碼述召,如下:
- 創(chuàng)建全局變量
mScroller
lateinit var mScroller: Scroller
- 初始化對(duì)象
private fun init(context: Context) {
// 使用Scroller
mScroller = Scroller(context)
}
- 重寫
computeScroll()
方法
override fun computeScroll() {
super.computeScroll()
if (mScroller.computeScrollOffset()) {
(parent as View).scrollTo(mScroller.currX, mScroller.currY)
//通過不斷的重繪不斷的調(diào)用computeScroll方法
invalidate()
}
}
- 生成接口
smoothScrollTo()
fun smoothScrollTo(destX: Int, destY: Int) {
val scrollX = scrollX
val delta = destX - scrollX
val scrollY = scrollY
// mScroller.startScroll(scrollX,0,delta,0,4000)
mScroller.startScroll(scrollX,scrollY,destX,destY,6000)
invalidate()
}
- 使用該接口
mv_test.smoothScrollTo(-900,-200)
MotionEvent.ACTION_DOWN
按下后只會(huì)調(diào)用一次,直到第二次按下