滑动
GestureDetector
GestureDetector手势检测:常用用来检测onSingleTapUp(单击),onFling(快速滑动),onScroll(拖动),onLongPress(长按),onDoubleTap(双击)
VelocityTracker
速度追踪器,就是用来计算手指的滑动速度
使用方法:
- ACTION_DOWN 事件到来时,通过 VelocityTracker.obtain()创建⼀个实例,或者使用 velocityTracker.clear() 把之前的某个实例重置
- 对于每个事件(包括 ACTION_DOWN 事件),使用velocityTracker.addMovement(event) 把事件添加进 VelocityTracker
- 在需要速度的时候(例如在 ACTION_UP 中计算是否达到 fling 速度),使用velocityTracker.computeCurrentVelocity(1000, maxVelocity) 来计算实时速度,并通过getXVelocity() / getYVelocity() 来获取计算出的速度。
方法参数中的 1000 是指的计算的时间长度,单位是 ms。例如这⾥填入 1000,那么getXVelocity() 返回的值就是每 1000ms (即⼀秒)时间内手指移动的像素数。第⼆个参数是速度上限,超过这个速度时,计算出的速度会回落到这个速度。例如这里填了 200,而实时速度是 300,那么实际的返回速度将是 200 ,maxVelocity 可以通过 viewConfiguration.getScaledMaximumFlingVelocity()来获取。
VelocityTracker velocityTracker = VelocityTracker.obtain();
public boolean onTouchEvent(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
velocityTracker.clear();
}
velocityTracker.addMovement(event);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
velocityTracker.computeCurrentVelocity(1000, maxVelocity);
break;
}
return true;
}
scrollTo,scrollBy,computeScroll
scrollTo(x, y)移动的是绝对值;scrollBy(deltaX, deltaY)移动的是相对值,内部也是调用scrollTo方法。
scrollTo() 是瞬时方法,不会自动使用动画。如果要用动画,需要配合 View.computeScroll()方法。computeScroll() 在 View 重绘时被自动调用
使用OverScroller实现缓慢滑动
// onTouchEvent() 中:
overScroller.startScroll(startX, startY, dx, dy);
postInvalidateOnAnimation();
......
// onTouchEvent() 外:
@Override
public void computeScroll() {
if (overScroller.computeScrollOffset()) { // 计算实时位置
scrollTo(overScroller.getCurrX(),
overScroller.getCurrY()); // 更新界⾯
postInvalidateOnAnimation(); // 下⼀帧继续
}
}
使用Scroller实现缓慢滑动
实现原理:startScroll记录下相关参数,invalidate导致view重绘,view的draw方法中又调用computeScroll,而computeScroll又会向Scroller获取当前scrollX和scrollY,然后通过scrollTo去实现滑动
private void smoothScrollTo(int destX, int destY){
int scrollX = getScrollX();
int deltaX = destX - scrollX;
int scrollY = getScrollY();
int deltaY = destY - scrollY;
//1000ms内滑向destX,效果就是慢慢滑动
mScroller.startScroll(scrollX,scrollY,deltaX,deltaY,1000);
invalidate();
}
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
super.computeScroll();
}
简单的自定义ViewPager
通过简单的自定义ViewPager来使用上面的VelocityTracker和computeScroll等使用
public class MyViewPager extends ViewGroup {
float downX;
float downY;
float downScrollX;
boolean scrolling;
float minVelocity;
float maxVelocity;
OverScroller overScroller;
ViewConfiguration viewConfiguration;
VelocityTracker velocityTracker = VelocityTracker.obtain();
public MyViewPager (Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
overScroller = new OverScroller(context);
viewConfiguration = ViewConfiguration.get(context);
maxVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
minVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
int childTop = 0;
int childRight = getWidth();
int childBottom = getHeight();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.layout(childLeft, childTop, childRight, childBottom);
childLeft += getWidth();
childRight += getWidth();
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
velocityTracker.clear();
}
velocityTracker.addMovement(ev);
boolean result = false;
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
scrolling = false;
downX = ev.getX();
downY = ev.getY();
downScrollX = getScrollX();
break;
case MotionEvent.ACTION_MOVE:
float dx = downX - ev.getX();
if (!scrolling) {
if (Math.abs(dx) > viewConfiguration.getScaledPagingTouchSlop()) {
scrolling = true;
getParent().requestDisallowInterceptTouchEvent(true);
result = true;
}
}
break;
}
return result;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
velocityTracker.clear();
}
velocityTracker.addMovement(event);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
downX = event.getX();
downY = event.getY();
downScrollX = getScrollX();
break;
case MotionEvent.ACTION_MOVE:
float dx = downX - event.getX() + downScrollX;
if (dx > getWidth()) {
dx = getWidth();
} else if (dx < 0) {
dx = 0;
}
scrollTo((int) (dx), 0);
break;
case MotionEvent.ACTION_UP:
velocityTracker.computeCurrentVelocity(1000, maxVelocity);
float vx = velocityTracker.getXVelocity();
int scrollX = getScrollX();
int targetPage;
if (Math.abs(vx) < minVelocity) {
targetPage = scrollX > getWidth() / 2 ? 1 : 0;
} else {
targetPage = vx < 0 ? 1 : 0;
}
int scrollDistance = targetPage == 1 ? (getWidth() - scrollX) : - scrollX;
overScroller.startScroll(getScrollX(), 0, scrollDistance, 0);
postInvalidateOnAnimation();
break;
}
return true;
}
@Override
public void computeScroll() {
if (overScroller.computeScrollOffset()) {
scrollTo(overScroller.getCurrX(), overScroller.getCurrY());
postInvalidateOnAnimation();
}
}
}
拖拽
OnDragListener
- 通过 startDrag() 来启动拖拽
startDrag最后会调用startDragAndDrop,
startDragAndDrop(ClipData data, DragShadowBuilder shadowBuilder,Object myLocalState, int flags)
内部有四个参数:
- ClipData data
其实就是一个封装数据的对象,通过拖放操作传递给接受者。该对象可以存放一个Item的集合,Item可以存放如下数据:
public static class Item {
final CharSequence mText;
final String mHtmlText;
final Intent mIntent;
Uri mUri;
}
- DragShadowBuilder shadowBuilder
用于创建拖拽view是的阴影,也就是跟随手指移动的视图,通常直接使用默认即可生成与一个原始view相同,带有透明度的阴影 - Object myLocalState
当你的拖拽行为是在同一个Activity中进行时可以传递一个任意对象,在监听中可以通过{@link android.view.DragEvent#getLocalState()}获得。如果是跨Activity拖拽中无法访问此数据,getLocalState()将返回null。 - int flags
控制拖放操作的标志。因为没有标志可以设置为0,flag标志拖动是否可以跨越窗口以及一些访问权限(需要API24+)
child.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
draggedView = v;
v.startDrag(null, new DragShadowBuilder(v), v, 0);
return false;
}
});
child.setOnDragListener(dragListener);
- 用setOnDragListener() 来监听
目标View:不是被拖拽的View,是要拖拽去哪个区域,这个区域就目标View,它要设置OnDragListener监听。
OnDragListener 内部只有⼀个方法: onDrag()。View中onDragEvent() 方法也会收到拖拽回调(界⾯中的每个 View 都会收到)
view.setOnDragListener(new View.OnDragListener() {
@Override
public boolean onDrag(View v, DragEvent event) {
//v 永远是设置该监听的view,这里即fl_blue
String simpleName = v.getClass().getSimpleName();
Log.w(BLUE, "view name:" + simpleName);
//获取事件
int action = event.getAction();
switch (action) {
case DragEvent.ACTION_DRAG_STARTED:
Log.i(BLUE, "开始拖拽");
break;
case DragEvent.ACTION_DRAG_ENDED:
Log.i(BLUE, "结束拖拽");
break;
case DragEvent.ACTION_DRAG_ENTERED:
Log.i(BLUE, "拖拽的view进入监听的view时");
break;
case DragEvent.ACTION_DRAG_EXITED:
Log.i(BLUE, "拖拽的view离开监听的view时");
break;
case DragEvent.ACTION_DRAG_LOCATION:
float x = event.getX();
float y = event.getY();
long l = SystemClock.currentThreadTimeMillis();
Log.i(BLUE, "拖拽的view在监听view中的位置:x =" + x + ",y=" + y);
break;
case DragEvent.ACTION_DROP:
Log.i(BLUE, "释放拖拽的view");
break;
}
//是否响应拖拽事件,true响应,返回false只能接受到ACTION_DRAG_STARTED事件,后续事件不会收到
return true;
}
});
ViewDragHelper
- 需要创建⼀个 ViewDragHelper 和 Callback()
ViewDragHelper create(ViewGroup forParent, Callback cb);一个静态的创建方法,
参数1:出入的是相应的ViewGroup
参数2:是一个回调Callback,在后面介绍包括其中的方法
ViewDragHelper dragHelper;
ViewDragHelper.Callback dragListener = new DragListener();
public DragHelperLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
dragHelper = ViewDragHelper.create(this, dragListener);
viewConfiguration = ViewConfiguration.get(context);
}
- 需要写在 ViewGroup 里面,重写 onIntercept() 和 onTouchevent()
- shouldInterceptTouchEvent(MotionEvent ev) 处理事件分发的(主要是将ViewGroup的事件拦截onInterceptTouchEvent,委托给ViewDragHelper进行处理)
- processTouchEvent(MotionEvent event) 处理相应TouchEvent的方法,这里要注意一个问题,处理相应的TouchEvent的时候要将结果返回为true,消费本次事件!否则将无法使用ViewDragHelper处理相应的拖拽事件!
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return dragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
dragHelper.processTouchEvent(event);
return true;
}
- ViewDragHelper.Callback的API(也就是创建ViewDragHelper传入的回调方法)
- tryCaptureView(View child, int pointerId) 这是一个抽象类,必须去实现,也只有在这个方法返回true的时候下面的方法才会生效;相当于事件的开始
参数1:捕获的View(也就是你拖动的这个View)
参数2:这个参数我也不知道什么意思API中写的一个什么指针,这里没有到也没有注意 - onViewDragStateChanged(int state) 当状态改变的时候回调,返回相应的状态(这里有三种状态)
STATE_IDLE 闲置状态
STATE_DRAGGING 正在拖动
STATE_SETTLING 放置到某个位置 - onViewPositionChanged(View changedView, int left, int top, int dx, int dy) 当你拖动的View位置发生改变的时候回调
参数1:你当前拖动的这个View
参数2:距离左边的距离
参数3:距离右边的距离
参数4:x轴的变化量
参数5:y轴的变化量 - onViewCaptured(View capturedChild, int activePointerId)捕获View的时候调用的方法
参数1:捕获的View(也就是你拖动的这个View)
参数2:这个参数我也不知道什么意思API中写的一个什么指针,这里没有到也没有注意 - onViewReleased(View releasedChild, float xvel, float yvel) 当View停止拖拽的时候调用的方法,一般在这个方法中重置一些参数,相当于事件的结束
参数1:你拖拽的这个View
参数2:x轴的速率
参数3:y轴的速率 - clampViewPositionVertical(View child, int top, int dy) 竖直拖拽的时候回调的方法
参数1:拖拽的View
参数2:距离顶部的距离
参数3:变化量
7.clampViewPositionHorizontal(View child, int left, int dx) 水平拖拽的时候回调的方法
参数1:拖拽的View
参数2:距离左边的距离
参数3:变化量
public class DragHelperLayout extends FrameLayout {
@Override
public void computeScroll() {
if (dragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
private class DragCallback extends ViewDragHelper.Callback {
float capturedOldLeft;
float capturedOldTop;
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
return true;
}
@Override
public void onViewDragStateChanged(int state) {
if (state == ViewDragHelper.STATE_IDLE) {
View capturedView = dragHelper.getCapturedView();
//。。。。
}
}
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
return left;
}
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
return top;
}
@Override
public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {
capturedOldLeft = capturedChild.getLeft();
capturedOldTop = capturedChild.getTop();
}
@Override
public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) {
}
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
//放回到原来的位置m
dragHelper.settleCapturedViewAt((int) capturedOldLeft, (int) capturedOldTop);
postInvalidateOnAnimation();
}
}
}
如何成为自定义高手(一)绘制
如何成为自定义高手(二)动画
如何成为自定义高手(三)布局
如何成为自定义高手(四)触摸反馈,事件分发机制
如何成为自定义高手(五)多点触摸
如何成为自定义高手(六)滑动和拖拽
如何成为自定义高手(七)滑动冲突