View的滑动方式
View的滑动是Android自定义控件的基础,同时在开发中我们也难免会遇到View的滑动处理。其实不管是哪种滑动方式,其基本思想都是类似的:当点击事件传到View时,系统记下触摸点的坐标,手指移动时系统记下移动后触摸的坐标并算出偏移量,并通过偏移量来修改View的坐标。
一、坐标系
Android系统中有两种坐标系,分别为Android坐标系和View坐标系。了解这两种坐标系能够帮助我们实现View的各种操作,比如我们要实现View的滑动,必须要知道这个View的位置,才能去操作,首先我们来看看Android坐标系。
1.Android坐标系
在Android中,将屏幕左上角的顶点作为Android坐标系的原点,这个原点向右是X轴正方向,向下是Y 轴正方向。另外在触控事件中,使用getRawX()和getRawY()方法获得的坐标也是 Android坐标系的坐标。
2.View坐标系
View坐标系以当前控件左上角为坐标原点,向左为 X 轴正方向,向下为 Y 轴正方向,MotionEvent 的 getX()、getY() 方法获取的是点击位置在视图坐标系中的坐标,View 的 mLeft、mTop 等属性也是 View 在父控件的视图坐标系中的坐标。它与Android坐标系并不冲突,两者是共同存在的,它们一起来帮助开发者更好地控制View。
坐标系
二、滑动原理
View 的滑动原理,其实滑动的原理与动画效果的实现非常相似,都是通过不断改变 View 的坐标来实现这一效果。所以要实现滑动效果就必须要监听用户的触摸事件,并根据事件传入的坐标,动态且不断的改变 View 的坐标,从而实现 View 跟随用户触摸的滑动而滑动。
三、滑动方式
实现View滑动有很多种 方法,在这里主要讲解6种滑动方法,分别是layout()、offsetLeftAndRight()与 offsetTopAndBottom()、LayoutParams、动画、scollTo 与 scollBy,以及Scroller。
1.layout()
View进行绘制的时候会调用onLayout()方法来设置显示的位置,因此我们同样也可以通过修改View 的left、top、right、bottom这4种属性来控制View的位置。
首先我们要自定义一个View,在 onTouchEvent()方法中获取触摸点的坐标,代码如下所示:
public class CustomView extends View {
int lastX;
int lastY;
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 记录触摸点坐标
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//当按下的时候执行
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
//当移动的时候执行
// 计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
// 在当前left、top、right、bottom的基础上加上偏移量来控制View的位置
layout(getLeft() + offsetX,
getTop() + offsetY,
getRight() + offsetX,
getBottom() + offsetY);
break;
case MotionEvent.ACTION_UP:
//当抬起的时候执行
break;
}
return true;
}
}
我们需要自定义一个CustomView 继承自View,需要重写onTouchEvent()方法。
在MotionEvent.ACTION_DOWN事件中获取当前触摸点的坐标位置,然后在MotionEvent.ACTION_MOVE事件中计算偏移量,再调用layout()方法重新放置这个CustomView的位置即可。在每次移动时都会调用layout()方法对屏幕重新布局,从而达到移动View的效果。
在布局文件中引用CustomView即可:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="20dp"
android:orientation="vertical"
tools:context=".ui.CustomActivity">
<com.example.monkey.myapplication.view.CustomView
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@color/colorPrimary" />
</LinearLayout>
具体效果,可以自己动手试试。
2.offsetLeftAndRight()与 offsetTopAndBottom()
这两种方法和layout()方法的效果差不多,其使用方式也差不多。我们将ACTION_MOVE中的代码替 换成如下代码:
case MotionEvent.ACTION_MOVE:
//当移动的时候执行
// 计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
// // 在当前left、top、right、bottom的基础上加上偏移量来控制View的位置
// layout(getLeft() + offsetX,
// getTop() + offsetY,
// getRight() + offsetX,
// getBottom() + offsetY);
//左右偏移
offsetLeftAndRight(offsetX);
//上下偏移
offsetTopAndBottom(offsetY);
break;
3.LayoutParams
LayoutParams主要保存了一个View的布局参数,因此我们可以通过LayoutParams来改变View的布局参 数从而达到改变View位置的效果。同样,我们将 ACTION_MOVE中的代码替换成如下代码:
case MotionEvent.ACTION_MOVE:
//当移动的时候执行
// 计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
前面我们的布局文件,因为父控件是 LinearLayout,所以我们用了 LinearLayout.LayoutParams。如果父控件是RelativeLayout, 则要使用RelativeLayout.LayoutParams。否则会报错
java.lang.ClassCastException: android.widget.LinearLayout$LayoutParams cannot be cast to android.widget.RelativeLayout$LayoutParams
at com.example.monkey.myapplication.view.CustomView.onTouchEvent(CustomView.java:60)
at android.view.View.dispatchTouchEvent(View.java:11788)
当然除了使用布局的LayoutParams外,我们还可以用 ViewGroup.MarginLayoutParams来实现。因为LinearLayout和RelativeLayout都是ViewGroup的子类。
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
4.scollTo 与 scollBy
ScrollTo(dx,dy)指移动到一个具体的坐标点(dx,dy),而ScrollBy(dx,dy)则表示移动的增量为dx,dy。我们将 ACTION_MOVE中的代码替换成如下代码:
case MotionEvent.ACTION_MOVE:
//当移动的时候执行
// 计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
((View) getParent()).scrollBy(-offsetX, -offsetY);
break;
首先scrollBy移动的是View的内容content,而不是View本身,如TextView的content为文本,ImageView的content为drawable,而ViewGroup的content是View或是ViewGroup,所以要移动当前View本身,我们就需要通过它的ViewGroup改变自己的内容从而改变View本身的位置。其次,我们真正操作的是View的父控件ViewGroup,要让View往左(上/右/下)移,应该要让ViewGroup往相反方向移动,也就是右(下/左/上),即偏移量就是相反的(负的)。所以要实现 CustomView 随手指移动的效果,就需要将偏移量设置为负值。若是正数,则会向相反的方向移动。
我们通过ScrollTo和ScrollBy的源码看下其区别:
/**
* 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();//重新绘制
}
}
}
/**
* 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);
}
通过源码可以看到scrollBy()里面调用了ScrollTo(),传入的参数是mScrollX+x,也就是说这次x是一个增量,所以scrollBy实现的效果就是,在当前位置上,再偏移x距离 。这是ScrollTo()和ScrollBy()的重要区别。
- scrollTo与scrollBy都会使View立即重绘,所以移动是瞬间发生的
- scrollTo(x,y):指哪打哪,效果为View的左上角滚动到(x,y)位置,但由于View相对与父View是静止的所以最终转换为相对的View的内容滑动到(-x,-y)的位置。
- scrollBy(x,y): 此时的x,y为偏移量,既在原有的基础上再次滚动
5.Scroller
通过上面的学习我们知道scrollTo与scrollBy可以实现滑动的效果,但是滑动的效果都是瞬间完成的,在事件执行的时候平移就已经完成了,这样的效果会让人感觉突兀,Google建议使用自然过渡的动画来实现移动效果。因此,Scroller类这样应运而生了。Scroller本身是不能实现View的滑动的,它需要与View的computeScroll()方法配合才能实现弹性滑动的效果。
public class CustomView extends View {
int lastX;
int lastY;
Scroller mScroller;
public CustomView(Context context) {
this(context,null);
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScroller = new Scroller(context);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 记录触摸点坐标
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//当按下的时候执行
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
//当移动的时候执行
// 计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
smoothScrollBy(-offsetX,-offsetY);
break;
case MotionEvent.ACTION_UP:
//当抬起的时候执行
break;
}
return true;
}
public void smoothScrollBy(int dx,int dy){
mScroller.startScroll(mScroller.getFinalX(),mScroller.getFinalY(),dx,dy,2000);
invalidate(); // 必须调用改方法通知View重绘以便computeScroll方法被调用。
}
@Override
public void computeScroll() {
super.computeScroll();
// 判断Scroller滑动是否执行完毕
if (mScroller.computeScrollOffset()) {
((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
// 通过重绘让系统调用onDraw,onDraw中又会调用computeScroll,如此不断循环,直到Scroller执行完毕
invalidate();
}
}
Android为View的滑动提供了Scroller辅助类,它本身并不能导致View滑动,需要借助computeScroll和ScrollTo方法完成View的滑动。使用Scroller类完成View的平滑。
- 首先要创建Scroller类。
- 然后重写computeScroll方法,这里需要注意的是computeScroll方法在onDraw中会被调用,因此需要调用invalidate方法通知View调用onDraw重绘,然后再调用computeScroll完成View的滑动,过程为invalidate->onDraw->computeScroll->invalidate->…,无限循环直到mScroller的computeScrollOffset返回false,也就是滑动完成。
- 调用Scroller类的startScroll方法开启滚动过程。
6.动画
使用动画来实现View的滑动主要通过改变View的translationX和translationY参数来实现,使用动画的好处在于滑动效果是平滑的。这里我们使用属性动画来移动view,我们让 CustomView在5000ms内沿着X轴向右平移300像素,具体实现如下:
CustomView customview = findViewById(R.id.customview);
ObjectAnimator.ofFloat(customview,"translationX",0,300).setDuration(5000).start();
也可以使用补间动画实现,在这里就不做多介绍了。
本文到这里就结束了,如果有不对的地方,还望指正。
参考资料
《Android进阶之光》