前言
View的滑动是Android自定义控件的基础,在开发中我们难免会遇到View的滑动处理。
其实不管是哪种滑动方式,基本思想都是差不多的:
1,当点击事件传到View时,系统记下触摸点的坐标;
2,手指移动时系统记下移动后触摸的坐标并算出偏移量,并通过偏移量来修改View的坐标;
实现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
//四个参数,left,top,right,bottom
layout(left + offsetX, top + offsetY, right + offsetX, bottom + offsetY)
}
}
return true
}
}
效果如下
2,offsetLeftAndRight()与offsetTopAndBottom()方式
这种方式和layout()的方式基本是一样的,根据名字也知道这两个方法分别是设置左边和右边,上面和下面的偏离值
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的布局参数,因此我们可以通过LayoutParams来改变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
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的滑动,主要就涉及到两个动画:属性动画和补间动画
补间动画的实现
- 通过xml的方式
在res目录新建anim文件夹并创建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",代码如下所示。运行代码后会发现,方块向右平移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的位置参数。如果对一个Button进行如上的平移动画操作,
当Button平移300像素停留在当前位置时,我们点击这个Button并不会触发点击事件,但在我们点击这个
Button的原始位置时却触发了点击事件。这就是补间动画和属性动画的区别
当然我们也可以直接通过代码来实现,不用xml,主要使用的类为TranslateAnimation:
val animation = TranslateAnimation(0f, 300f, 0f, 0f)
animation.duration = 2000//设置动画持续时间
animation.fillAfter=true
dragView.animation =animation
效果和上面是一样的
属性动画
属性动画是Android3.0之后推出的,就是为了解决补间动画的焦点问题,我们经常使用到的类主要有AnimatorSet和ObjectAnimator;使用 ObjectAnimator 进行更精细化的控制,控制一个对象和一个属性值,而使用多个ObjectAnimator组合到AnimatorSet形成一个动画。属性动画通过调用属性get、set方法来真实地控制一个View的属性值,因此,强大的属性动画框架基本可以实现所有的动画效果。
ObjectAnimator
ObjectAnimator 是属性动画最重要的类,创建一个 ObjectAnimator 只需通过其静态工厂类直接返还一个
ObjectAnimator对象。参数包括一个对象和对象的属性名字,但这个属性必须有get和set方法,其内部会通
过Java反射机制来调用set方法修改对象的属性值。下面看看平移动画是如何实现的,代码如下所示:
val animation = ObjectAnimator.ofFloat(dragView, "translationX", 300f)
animation.duration = 2000
animation.start()
这和补间动画的加了fillAfter属性后动画效果是一样的,但是不一样的地方就是焦点已经平移到了新的位置;
注:
需要注意的是,在使用ObjectAnimator的时候,要操作的属性必须要有get和set方法,不然
ObjectAnimator 就无法生效。如果一个属性没有get、set方法,也可以通过自定义一个属性类或包装类来间
接地给这个属性增加get和set方法。现在来看看如何通过包装类的方法给一个属性增加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不提供任何动画效果,它更像一个数值发生器,用来产生有一定规律的数字,从而让调
用者控制动画的实现过程。通常情况下,在ValueAnimator的AnimatorUpdateListener中监听数值的变化,从
而完成动画的变换,代码如下所示:
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
}
}
这样就实现了和layout等一样的拖动效果
注:至于这里为什么是负数,其实是参照物不一样导致的,scrollby和scrollto滚动的时候其实是类似于窗口里面的View并没有移动,而是手机屏幕在移动,如果我设置为正数,其实类似手机屏幕往右移动了,view保持不动,这样间接的其实类似于View左移了;
5.Scroller
我们在用scollTo/scollBy方法进行滑动时,这个过程是瞬间完成的,所以用户体验不大好。这里我们可
以使用 Scroller 来实现有过渡效果的滑动,这个过程不是瞬间完成的,而是在一定的时间间隔内完成的。
Scroller本身是不能实现View的滑动的,它需要与View的computeScroll()方法配合才能实现弹性滑动的效
果。
- computeScroll()
系统会在绘制View的时候在draw()方法中调用computeScroll()方法。在这个方法中,我们调用父类的scrollTo()方法并通过Scroller来不断获取当前的滚动值,每滑动一小段距离我们就调用invalidate()方法不断地进行重绘,重绘就会调用computeScroll()方法,这样我们通过不断地移动一个小的距离并连贯起来就实现了平滑移动的效果。
写个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)
实现效果
到这里view的滑动就基本差不多了
欢迎大家扫描关注作者公众号,长期推送Android技术干货,感谢大家支持: