Android 自定义View:实现View的滑动效果

时间:2023-02-09 08:07:42

相关文章:

Android坐标系分析

Android自定义View 之 View的测量

Android 自定义View之View的绘制


之前学习了View的测量和绘制,我们已经可以定制自己喜欢外观的View了

今天再来学习一下如何定制View的滑动效果


View的滑动效果,本质上就是通过改变View的坐标来实现的。

关于Android 坐标系,之前我也写过文章特意讲了,我们再简单巩固一下。

坐标系分两种

一种是绝对坐标系,就是以手机屏幕左上角为原点

View.getLocationOnScreen(int[] location) 还有 MotionEvent.getRawX()MotionEvent.getRawY() 都是以这个坐标系为基准获取的坐标

我们称之为 绝对坐标

一种是视图坐标系,就是以父视图左上角为坐标原点

View.getLeft()等是以父布局为基准,

View.getLocationInWindow(int[] location)父窗体为基准,

Canvas绘制和MotionEvent.getX()等是以当前View(父视图)为基准的


通常我们移动View,都和手指在屏幕上的触碰,拖动离不开关系

那么,如何捕捉到手指的触控呢?

这就要用到View的onTouchEvent(MotionEvent event)方法啦

public class MyView extends View{

public MyView(Context context) {
super(context);
// TODO Auto-generated constructor stub
}

@Override
public boolean onTouchEvent(MotionEvent event){
return true;
}

}
onTouchEvent只有return true 这个事件的处理才会生效


而我们如何分辨传来的MotionEvent是什么类型呢?

这就要用到MotionEvent封装的事件常量,常用的有以下几种

MotionEvent事件常量
事件常量 描述
ACTION_DOWN = 0 单点触摸时按下动作
ACTION_UP = 1 单点触摸时离开动作
ACTION_MOVE = 2 触摸点移动动作
ACTION_CANCEL = 3 触摸动作取消
ACTION_OUTSIDE = 4 触摸动作超过边界
ACTION_POINTER_DOWN = 5 多点触摸按下动作
ACTION_POINTER_UP = 6 多点触摸离开动作









通过这些事件常量,我们就可以定位到相应的事件,做相应的处理

通常我们onTouchEvent这个方法会以下面模式重写

	@Override
public boolean onTouchEvent(MotionEvent event){
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
//检测到手指触碰屏幕
//写相关的事件
//比如,获得触点的x视图坐标
int x = (int)event.getX();
break;
case MotionEvent.ACTION_MOVE:
//手指在屏幕上滑动
break;
case MotionEvent.ACTION_UP:
//手指离开屏幕
break;
}
return true;
}
当然,你也可以用if来判断,只是架构不如switch清晰

此外,你也可以通过case分支检测其他的触摸事件并处理


实现滑动的七种方法


1.layout方法

View在进行绘制时,会调用onLayout()方法来设置当前显示的位置,最终通过调用layout(left,top,right,bottom)设置View左上右下顶点的坐标来设置位置

(为什么四个点就可以?因为View本身是矩形)

下面的代码,我们就通过这个方法的运用,来实现一个被手指拖动的小球。

public class MyView extends View{

//设置wrap_content时候View的大小
private int defaultWidth = 100;
private int defaultHeight = 100;
//画笔 用于画圆
private Paint p = new Paint();

//View当前的位置
private int rawX = 0;
private int rawY = 0;
//View之前的位置
private int lastX = 0;
private int lastY = 0;

public MyView(Context context){
super(context);
}
public MyView(Context context, AttributeSet set) {
super(context, set);
}

//画一个红色,圆心为View中心点,半径为View宽度的圆
public void onDraw(Canvas canvas){
//Log.e("onDraw执行","true");
p.setColor(Color.RED);
int x = this.getLeft() + this.getWidth()/2;
int y = this.getTop() + this.getHeight()/2;
canvas.drawCircle(this.getWidth()/2, this.getHeight()/2, this.getWidth()/2, p);
}

//注意,触摸事件的响应范围仅限于该View的区域
public boolean onTouchEvent(MotionEvent event){
//Log.e("onTouchEvent执行","true");
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
//Log.e("ACTION","down");
//获取手指落下的坐标并保存
rawX = (int)(event.getRawX());
rawY = (int)(event.getRawY());
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
//Log.e("ACTION","move");
//手指拖动时,获得当前位置
rawX = (int)event.getRawX();
rawY = (int)event.getRawY();
//手指移动的x轴和y轴偏移量分别为当前坐标-上次坐标
int offsetX = rawX - lastX;
int offsetY = rawY - lastY;
//通过View.layout来设置左上右下坐标位置
//获得当前的left等坐标并加上相应偏移量
layout(getLeft() + offsetX,
getTop() + offsetY,
getRight() + offsetX,
getBottom() + offsetY);
//移动过后,更新lastX与lastY
lastX = rawX;
lastY = rawY;
break;
}
return true;
}

//简单的重写onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(defaultWidth,defaultHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(defaultWidth,heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize, defaultHeight);
}else{
setMeasuredDimension(widthSpecSize, heightSpecSize);
}
}
}
引用MyView的布局文件

<RelativeLayout 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"
tools:context="com.example.androidslide.MainActivity" >

<com.example.androidslide.MyView
android:background="#00ffff"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<!-- 我们给View设置一个蓝色的背景,方便比较View的区域和我们绘制的圆的区域 -->

</RelativeLayout>


逻辑并不复杂,大家结合注释自己理解实验一下


2.offsetLeftAndRight()offsetTopAndBottom()

这两个也是View自带的方法,根据方法名也很容易看出来操作对象是 左右,上下

因为左右移动是x轴移动上下移动是y轴移动

结合上一部分layout的代码,我们只需稍作修改就可以

offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);

大家自己实践一下


3.LayoutParams

因为View一定是在一个布局中,而且View的位置常常是由父布局来定的

LayoutParams保存了一个View的布局参数,所以我们可以通过改变LayoutParams来改变View的位置达到移动的效果

ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams)getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);

也可以通过具体的父布局,比如 LinearLayout.LayoutParams 或者 RelativeLayout.LayoutParams 来实现。


4. scrollTo 与 scrollBy(等写完ViewGroup的自定义再讲)

5.Scroller(和scrollTo 和 scrollBy一起讲)

6.属性动画(单独作为一篇文章总结动画)

7.ViewDragHelper(要用到事件拦截机制,写完事件分发和拦截文章再讲)


对了,大家看完上述例子,千万不要以为,移动小球坐标都一定要在onTouchEvent内写。

你甚至可以在activity中,通过子线程不断更新UI线程来不断移动View实现跑马灯的效果

多尝试尝试,有问题欢迎留言~


通过layout方法实现滑动的小球的源码 点击打开链接