学习资料:
- Android群英传
- Android艺术探索
滑动效果就是实现动态修改一个View
的坐标。
实现滑动效果的基本思想:
手指落在屏幕触控屏幕时,系统记下当前的触摸点坐标;手指在屏幕移动时,系统记下移动后的触摸点坐标,获取到每一次相对前一次触摸点坐标的偏移量,通过偏移量来修改View
的坐标,不断重复,实现整个滑动过程
1.系统辅助类 <p>
同MotionEvent
一样,滑动事件系统还提供了另外一些类
1.1 TouchSlop最小距离 <p>
TouchSlop
是系统识别最小的滑动距离,是一个常量值。当手指在屏幕滑动距离小于这个值时,系统不会将动作视为滑动。这个常量值的具体大小和设备也有关,不同的屏幕分辨率,可能会不一样
获得方式:
ViewContfiguration.get(getConetxt()).getScaledTouchSlop()
利用这个临界值,可以将一些不想要的手指操作给过滤掉
1.2 VelocityTracker 速度追踪 <p>
用于追踪手指在滑动过程中的速度,包括水平速度和竖直方向的速度
使用过程:
- 第1步,在
View.onToucheEvent()
获取VelocityTracker
对象 - 第2步,使用拿到的
VelocityTracker
对象来计算x,y
轴方向的速度 - 第3步,在比较恰当及时的时机,将
VelocityTracker
对象释放掉,回收内存
代码:
public class ScrollerActivity extends AppCompatActivity {
private VelocityTracker velocityTracker;
private final String TAG = "ScrollerActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scroller);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//获取VelocityTracker
velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
//计算滑动速度
velocityTracker.computeCurrentVelocity(1000);//计算速度
float xVelocity = velocityTracker.getXVelocity();
float yVelocity = velocityTracker.getYVelocity();
Log.e(TAG,"&&&-->x = "+xVelocity+"---> y = "+yVelocity);
return super.onTouchEvent(event);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (null != velocityTracker){
velocityTracker.clear();//重置
velocityTracker.recycle();//回收内存
}
}
}
直接在Acvitity
测试,获取速度的结果,
-
x
轴速度,从左向右滑动时,速度为正,从右向左滑动为负; -
x
轴速度,从上向下滑动时,速度为正,从下向上滑动为负;
正负值就是要看滑动的方向和x,y
轴方向是否一致
注意:
在使用velocityTracker.getXVelocity(),velocityTracker.getYVelocity()
获取速度之前,要先根据设置的单位时间来计算速度。计算公式v = (终点- 起点) /t
。计算出来的速度是相对于设置的时间的。
计算出来的速度指的是一段时间内滑过的像素数。
velocityTracker.computeCurrentVelocity(t)
t = 1000
,在1000ms
内,假设匀速水平滑过了1000px
,水平速度就是1000
,也就是1000px/1000ms
t = 100
,在100ms
内,假设匀速水平滑过了100px
,水平速度就是100
,也就是100px/100ms
1.3 GestureDetector 手势检测 <p>
用于辅助检测单击、滑动、长按、双击
使用步骤:
- 第1步:创建
GestureDetector
对象,并实现OnGestureListener
接口。 - 第2步:接管目标
View
的onTouhEvent
方法
GestureDetector.setOnDoubleTapListener(onDoubleTapListener)
可以实现双击
以Activicty
为目标View
代码:
public class ScrollerActivity extends AppCompatActivity {
private Toast toast;
private GestureDetector mGestureDetector;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scroller);
initGestureDetector();
}
/**
* 初始化 GestureDetector
*/
private void initGestureDetector() {
mGestureDetector = new GestureDetector(ScrollerActivity.this,onGestureListener );
//解决屏幕长按后无法拖动
mGestureDetector.setIsLongpressEnabled(false);
}
private GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {//手指轻触屏幕的一瞬间,由一个ACTION_DOWN触发
showToast("轻触一下");
return true;
}
@Override
public void onShowPress(MotionEvent e) {//手指轻触屏幕,尚未松开或拖动,由一个ACTION_DOWN触发
showToast("轻触未松开");
}
@Override
public boolean onSingleTapUp(MotionEvent e) {//手指离开屏幕,伴随一个ACTION_UP触发,单击行为
showToast("单击");
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {//手指按下屏幕并拖动
// 由一个由一个ACTION_DOWN,多个ACTION_MOVE触发,是拖动行为
showToast("拖动");
return false;
}
@Override
public void onLongPress(MotionEvent e) {//长按
showToast("长按");
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
//按下屏幕,快速滑动后松开,由一个由一个ACTION_DOWN,多个ACTION_MOVE,一个ACTION_UP触发
showToast("快速滑动");
return false;
}
};
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
}
/**
* Toast
*/
private void showToast(String str) {
if (null == toast) {
toast = Toast.makeText(ScrollerActivity.this, str, Toast.LENGTH_LONG);
} else {
toast.setText(str);
}
toast.show();
}
}
在OnGestureListener
内onDown(),onSingleTapUp(),onScroll(),onFling()
方法都有一个boolean
类型的返回值,这个值表示是否消费事件
1.4 Scroller 弹性滑动对象 <p>
用于实现View
的弹性滑动。Scroller
本身无法实现弹性滑动,需要配合View
的computeScroll()
方法
Scroller
使用有个固定的3步走模式:
- 初始化
Scroller
对象 - 重写
View
的computeScroll()
方法 - 调用
mScroller.startScroll()
方法
简单使用:
public class ScrollerView extends LinearLayout {
private Scroller mScroller;
public ScrollerView(Context context, AttributeSet attrs) {
super(context, attrs);
initScroller();
}
/**
* 初始化Scroller
*/
private void initScroller() {
mScroller = new Scroller(getContext());
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {//判断Scroller是否执行完毕
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
public void smoothScrollTo(int destX, int destY) {
//计算相对于左上角的偏移量
final int deltaX = getScrollX() - destX;
final int deltaY = getScrollY() - destY;
//在1000ms内滑向destX destY
mScroller.startScroll(0, 0, deltaX, deltaY, 1000);
invalidate();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
smoothScrollTo((int) event.getX(), (int) event.getY());
break;
case MotionEvent.ACTION_UP://恢复左上角
mScroller.startScroll(getScrollX(), getScrollY(), -getScrollX(), -getScrollY(), 1000);
invalidate();
break;
}
return true;
}
}
效果便是手指点在屏幕哪里,一秒内,ScrollerView
内的所有子控件便会滑动到手指落的点的位置
关于Scroller
这里先了解一点点,打算之后再单独来学习
2.实现滑动的7种方法 <p>
在Android群英传
中,徐医生给出7种滑动方法:
- layout方法
- offsetLetAndRight()和offsetTopAndBottom()
- LayoutParams
- scrollTo和scrollBy
- Scroller
- 属性动画
- ViewDragHelper
5
上面刚刚有了解,以后还会继续补充学习,6
在Android动画基础知识学习(下)学习了解过。1234
在本篇会学习了解,这几个方法感觉效果都不是很好,滑动效果很突兀,最重要的便是方法7
,下篇单独来学习
2.1ayout方法
View
进行绘制时,会调用onLayout()
方法来设置显示的位置
代码:
public class ScrollerView extends LinearLayout {
private float lastX, lastY;
public ScrollerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
//计算偏移量
float offsetX = x - lastX;
float offsetY = y - lastY;
//计算四个顶点的位置
int left = (int) (getLeft() + offsetX);
int top = (int) (getTop() + offsetY);
int right = (int) (getRight() + offsetX);
int bottom = (int) (getBottom() + offsetY);
//布局回调
layout(left, right, top, bottom);
break;
}
return true;
}
}
不晓得是我代码有问题还是这个思路本身有问题,体验非常不好,childView
在滑动过程中,大小会发生改变
2.2 offsetLeftAndRight和offsetTopAndBottom <p>
系统提供的对左右上下移动的API
的封装,效果和使用与layout
方法类似
将layout
方法代码简单修改:
case MotionEvent.ACTION_MOVE:
//计算偏移量
float offsetX = x - lastX;
float offsetY = y - lastY;
Log.e("offset","&&&--"+offsetX+"-->"+offsetY);
offsetLeftAndRight((int)offsetX);
offsetTopAndBottom((int)offsetY);
break;
子控件会随着手指在屏幕滑动而滑动
offset有效区域这个方法遇到个问题,有些区域无效,只有黄色边框内才有效
2.3 LayoutParams 布局参数 <p>
LayoutParams
保存了一个View
的布局参数。可以通过LayoutParams
来动态地修改一个布局的位置参数。
简单的修改代码:
case MotionEvent.ACTION_MOVE:
//计算偏移量
float offsetX = x - lastX;
float offsetY = y - lastY;
Log.e("offset", "&&&--" + offsetX + "-->" + offsetY);
MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = (int) (getLeft() + offsetX);
layoutParams.topMargin = (int) (getTop() + offsetY);
setLayoutParams(layoutParams);
break;
同样有一个有效区域的问题
2.4 使用scrollTo或者scrollBy <p>
两个方法区别: scrollBy()
相对移动,scrollTo()
绝对移动
在View
内部有两个属性mScrllX
和mScrollY
,分别可以通过getScrollX()
和getScrollY()
方法得到
在滑动过程中,mScrollX
总是等于View
左边缘和View
内容左边缘在水平方方向的距离;mScrollY
总是等于View
上边缘和View
中内容上边缘在竖直方向的距离。View
边缘是指View
的位置也就是View
的四个顶点到父容器的距离,View
内边缘是内容距离View
四边的距离。
无论是scrollTo()
还是scrollBy()
都只能改变View
内容的位置而不能改变View
在布局中的位置
mScrollX/Y
单位为像素px
。当View
左边缘在View
内容左边缘右边时,mScrollX
为正值,反之为负值;同理,当View
上边缘在View
内容上边缘下边时,mScrollX
为正值,反之为负值。也就是说,View
从左向右滑动,mScrollX
为负值,反之为正值;从上往下滑动,mScrollY
为负值,反之为正值
白色为View
原始位置,紫色矩形为内容
简单使用:
case MotionEvent.ACTION_MOVE:
//计算偏移量
float offsetX = x - lastX;
float offsetY = y - lastY;
Log.e("offset", "&&&--" + offsetX + "-->" + offsetY);
//scrollBy((int)-offsetX,(int)-offsetY);
scrollTo((int)-offsetX,(int)-offsetY);
break;
根据规律图,手势和实际移动方向相反,在设置参数时,设置为了-offsetX
几种方式,简单总结
- scrollTo/By:操作简单,适合对
View
内容的滑动 - 属性动画:操作简单,适用于没交互
View
和实现复杂的动画效果 - 改变布局参数:操作复杂,适用于有交互的
View
3. 滑动冲突
滑动冲突常见的场景:
- 外部滑动方向和内部滑动方向不一致
- 外部滑动方向和内部滑动方向一致
- 上面两种情况嵌套
解决方式有两种:外部拦截,内部拦截
3.1 外部拦截 <p>
外部拦截思路:
点击事件都会先经过父容器的拦截处理,如果父容器需要处理此事就拦截,否则就不进行拦截。重写父容器的onInterceptTouchEvent()
方法
伪码:
public boolean onInterceptTouchEvent(MotionEvent event){
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_MOVE:
intercepted = true;
break;
case MotionEvent.Move:
if(父容器需要当前点击事件){
intercepted = true;
}else{
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
处理思路代码基本都是固定的。
首先,在ACTION_DOWN
中,父容器必须返回false
,不拦截ACTION_DOWN
事件。因为一旦拦截了ACTION_DOWN
后续的ACTION_MOVE
和ACTION_UP
都会又父容器来处理,这样事件就无法传递给childView
其次,在ACTION_MOVE
中,可以根据需要来进行拦截,需要就返回true
,否则就false
最后,在ACTION_UP
中,返回false
注意:
如果父容器在ACTION_UP
中,返回了true
,childView
就不会再收到ACTION_UP
事件,childView
的onClick
事件就不会触发。父容器比较特殊,一旦开始拦截某个事件,之后的序列事件都是交给父容器来处理,包括ACTION_UP
,即使在ACTION_UP
中返回false
,ACTION_UP
还是由父容器处理
3.2 内部拦截 <p>
内部拦截法指的是父容器不拦截任何事件,所有的事件都传递给childView
,根据需要,childView
来选择是否消费
,需要配合requestDisallowInterceptTouchEvent()
方法。重写childView
的dispatchTouchEvent()
方法
伪码:
public boolean dispatchTouchEvent(MotionEvent event){
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if(父容器需要此类点击事件){
parent.requestDisallowInterceptTopuchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
break;
}
mLastX = x ;
mLastY = y ;
return super.dispatchTouchEvent(event);
}
使用稍微比外部麻烦。
在ACTION_DOWN
中,使用parent.requestDisallowInterceptTouchEvent(true)
,让父容器不拦截ACTION_DOWN
事件,ACTION_DOWN
不受FLAG_DISALLOW_INTERCEPT
标记位控制
4.最后 <p>
国庆放假在家的效率有些低,事有点多。农村娃,还下地干了会活,哈哈。打算将自定义系列结束呢,完不成不计划了。按照学习计划,还剩下2篇学习内容
本人很菜,有错误请指出
共勉 :)
网友评论
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
firstX= x;
firstY =y;
break;
case MotionEvent.ACTION_MOVE:
float offsetX = x - firstX;
float offsetY = y - firstY;
Log.e("offset","&&&--"+offsetX+"-->"+offsetY);
scrollTo((int)offsetX,(int)offsetY);
break;
}
return true;
}