一、 关于View所需要掌握的基本功
1、 View的概念
- 在android中View为所有控件的基类,简单的控件和VIewGroup都继承自View。
- 一个View占用了屏幕上的一个矩形区域并且负责界面绘制和事件处理。
- View是一个宽泛的概念,既可以指复杂的ViewGroup(控件组,诸如RelativeLayout,LinearLayout),也可以是简单的控件(诸如TextView,Button)。
- View树,由于View既可以做简单控件,也可为ViewGroup,故而View体系组成了View树,View的显示和事件处理,都是依赖于这个View树。绘制和事件处理的起始点,都是从根View开始一级一级的往下传递。我们从任意一层发起绘制,都将反馈到根View,然后再从上往下传递。
2、 View坐标参数
android坐标系中的view
四个参数
View的四个顶点决定了其显示的位置,分别为left、top、right、bottom(分别对应着view的左上顶点的横坐标、左上顶点的纵坐标、右下顶点的横坐标、右下顶点的纵坐标),
获取方式
分别通过getLeft()、getTop()、getRight()、getBottom()方法获取(相对于ViewGroup)
View的宽高计算
width=getRight()-getLeft();
height=getBottom()-getTop();
3、 MotionEvent触摸事件
常见触摸事件
从手指触摸屏幕开始到手指离开屏幕,触发了一系列事件,常见动作及触发时机
MotionEvent.ACTION_DOWN ,触摸屏幕时触发
MotionEvent.ACTION_MOVE,滑动时触发
MotionEvent.ACTION_UP,离开屏幕时触发
MotionEvent.ACTION_CANCEL,当焦点滑动到控件之外时触发
MotionEvent对象获取事件发生时的坐标
getX() / getY() , 获取相对于View自身的左上角的x/y坐标
getRawX() / getRawY() , 获取相对于屏幕左上角的x/y坐标
4、 TouchSlop – 触摸or滑动
TouchSlop含义
Distance in pixels a touch can wander before we think the user is scrolling,即可以视为触摸而非滑动事件的最大距离,大于这个值一般可视为滑动。其值为常量且与设备有关,不同设备值可能不同。
获取TouchSlop
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
5、 GestureDetector与ScaleGestureDetector 手势识别
GestureDetector
GestureDetector的使用
作用:手势检测,如单击,滑动,长按,双击等
使用:
实现OnGesturerListener接口,注册事件监听
接管目标View滑动事件onTouchEvent方法。在onTouchEvent方法中
boolean isConsume = mGestureDetector.onTouchEvent(event);
return isConsume;
在对应的方法中执行相应的操作
OnGestureListener接口中的方法
-
onDown Notified when a tap occurs with the down MotionEvent that triggered it.
-
onFling Notified of a fling event when it occurs with the initial on down MotionEvent and the matching up MotionEvent.//手指以某个速率滑离屏幕时
-
onLongPress Notified when a long press occurs with the initial on down MotionEvent that trigged it.
-
onScroll Notified when a scroll occurs with the initial on down MotionEvent and the current move MotionEvent.
-
onShowPress The user has performed a down MotionEvent and not performed a move or up yet.
-
onSingleTapUp Notified when a tap occurs with the up MotionEvent that triggered it.
-
onSingleTapConfirmed(MotionEvent e); Notified when a single-tap occurs. Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this will only be called after the detector is confident that the user’s first tap is not followed by a second tap leading to a double-tap gesture.// 严格的单击事件,不可能为双击事件的一击
ScaleGestureDetector
ScaleGestureDetector的使用
作用:识别缩放手势
使用:
实现OnScaleGestureListener接口,注册事件监听
接管目标View滑动事件onTouchEvent方法。在onTouchEvent方法中
boolean isConsume = mScaleGestureDetector.onTouchEvent(event);
return isConsume;
在对应的方法中执行相应的操作
// 获取缩放因子
float scaleFactor = detector.getScaleFactor();
mMatrix.postScale(scaleFactor,scaleFactor,detector.getFocusX(), detector.getFocusY()); //手势中点
setImageMatrix(mMatrix);
onScaleBegin,onScale返回值为true时才会有缩放效果
OnScaleGestureListener接口方法
-
onScale Responds to scaling events for a gesture in progress.缩放时
-
onScaleBegin Responds to the beginning of a scaling gesture. 缩放开始
-
onScaleEnd Responds to the end of a scale gesture. 缩放结束
6、 Scroller - 弹性滑动承载者
弹性滑动对象,本身并不能产生弹性滑动,需要配合computeScroll () 与scrollTo()方法才能实现view的弹性滑动,示例、
/**
* 弹性滑动
* @param startX
* @param startY
* @param dX ,x方向的偏移量,>0往左滑 ,<0往右滑
* @param dY ,y方向的偏移量,>0往上滑 ,<0往下滑
* @param duration , 持续时长
*/
private void smoothScroll(int startX, int startY, int dX, int dY, int duration) {
if (Math.abs(duration) > 800) duration = 800;
mScroller.startScroll(startX, startY, dX, dY, Math.abs(duration));
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) { // computeScrollOffset返回值为true则滑动还没结束
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
7、 VelocityTracker - 速度追踪利器
作用:滑动速度追踪,分为x方向(getXVelocity)与y方向(getYVelocity)
计算:( 终点位置横坐标(纵坐标)-起点位置横坐标(纵坐标)) / 耗时
当从左往右滑getXVelocity>0 , 反之getXVelocity<0
当从上往下滑getXVelocity>0 , 反之getXVelocity<0
使用步骤
-
获取VelocityTracker
if (velocityTracker == null) { velocityTracker = VelocityTracker.obtain(); }
-
添加用户的movement到tracker
velocityTracker.addMovement(ev); // ACTION_DOWN与ACTION_MOVE都要
-
设置时间间隔计算速度
velocityTracker.computeCurrentVelocity(50); // 计算当前速率,按每50毫秒
-
获取x与y方向速度
final float yVelocity = velocityTracker.getYVelocity(); final float xVelocity = velocityTracker.getXVelocity();
-
回收资源
velocityTracker.clear(); // 重置到初始状态 当velocityTracker不再使用时,调用velocityTracker.recycle();回收资源,以便其他使用
二、View滑动那些事
1. View的几种常见滑动
使用scrollTo/scrollBy实现
特点:瞬间完成,scrollTo绝对移动,scrollBy相对移动scrollTo是滑动到指定位置,scrollBy是相对自身的位置滑动指定距离
scrollBy基于scrollTo实现
/**
* @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);
}
getScrollX与getScrollY的意义
getScrollX获取的是view内容左边缘相对于view边界左边缘的偏移量,值=view边界左边缘 x- view内容左边缘x
getScrollY获取的是view内容上边缘相对于view边界上边缘的偏移量,值=view边界上边缘y - view内容上边缘y
由上可总结如下,
view内容左边缘滑动到view左边缘左边时,getScrollX>0,反之,右边时,getScrollX<0
view内容上边缘滑动到view上边缘上边时,getScrollY>0,反之,下边时,getScrollY<0
使用动画实现
- 补间动画
包括AlphaAnimation、TranslateAnimation、ScaleAnimation、RotateAnimation
特点:不改变view的位置属性,移动的只是view的副本
设置setFillAfter为true,保持最后状态
- 属性动画
包括ObjectAnimator,ValueAnimator
特点:真实改变View的属性
Android2.3以下使用nineoldandroids兼容库
示例
ObjectAnimator.ofFloat(targetView,"alpha",0.0f,1.0f);
改变布局参数策略
通过改变View的自身的位置参数实现滑动效果
在目标view旁置一个空view,通过改变此空view的位置参数来间接改变目标view位置,
获取并修改位置参数
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) textView.getLayoutParams();
mlp.leftMargin += 10;
mlp.width += 10;
textView.setLayoutParams(mlp); // 或textView.requestLayout();
layout方法
int contentLeft = centerContent.getLeft() + dX;
centerContent.layout(contentLeft,centerContent.getTop(),contentLeft+centerContent.getMeasuredWidth(), centerContent.getBottom());
offsetLeftAndRight与offsetTopAndBottom
offsetLeftAndRight : 通过特定的pixels对view进行水平方向偏移
Offset this view's horizontal location by the specified amount of pixels.
offsetTopAndBottom : 通过特定的pixels对view进行竖直方向偏移
Offset this view's vertical location by the specified number of pixels.
ViewDragHelper
常用方法
-
create(forParent, callBack) —创建
forParent - Parent view to monitor 要监控的父view callBack - Callback to provide information and receive events 回调
-
tryCaptureView (View child, int pointerId) —决定哪个view可以被捕获
child - Child the user is attempting to capture 要捕获的view pointerId - ID of the pointer attempting the capture touch的id
-
clampViewPositionHorizontal (View child, int left, int dx) —限制被拖拽view的水平方向滑动
child - Child view being dragged 被拖拽的view left - Attempted motion along the X axis x方向尝试滑动的left dx - Proposed change in position for left x方向的增量
-
clampViewPositionVertical —限制被拖拽view的水竖直向滑动
-
onViewPositionChanged(View changedView, int left, int top, int dx, int dy) —当view位置改变时
param left 此view的左边界的x param top 此view的上边界的y param dx 与上次相比的x位置增量 param dy 与上次相比的y位置增量
-
onViewReleased(View releasedChild, float xvel, float yvel) — 释放拖动后调用
param releasedChild 被释放的view param xvel x方向的速度 param yvel y方向的速度
-
getViewHorizontalDragRange与getViewVerticalDragRange 返回view的水平与竖直拖动范围,设置为0则不可拖动
-
onEdgeDragStarted 在边界拖动时回调
实现弹性滑动
mViewDragHelper.smoothSlideViewTo(View child,int finalLeft, int finalTop) 开启滑动
ViewCompat. postInvalidateOnAnimation(View view) 引导重绘
computeScroll中continueSettling判断是否完成
简单总结
- scrollTo/scrollBy : 操作简单,适用于对view内容的滑动
- 动画 : 适用于没有交互性的操作及实现复杂的动画
- 改变布局参数 : 操作繁琐,适用于有交互性的滑动
- layout方法:对ViewGroup内部View重新布局
- 使用ViewDragHelper:功能强大,可以用于制作复杂的滑动效果
2. View的弹性滑动
Scroller实现弹性滑动(示例见view的基本功6)
注意事项:
1. 滑动的是view的内容而不是view本身位置改变
2. mScroller.getCurrX() / mScroller.getCurrY()获得的值是根据时间流逝比例计算所得,为scrollTo将要滑动到的位置
3. mScroller.computeScrollOffset() 返回值为true表示滑动还未结束
4. Scroller内部只是保存了我们传递的参数,没有实现滑动
5. mScroller.startScroll并不会开始滑动,调用invalidate()才会导致view重绘,并开始滑动
6. Scroller本身需要配合computeScroll方法和scrollTo方法才能实现弹性滑动,通过不断的view重绘和小步位移完成view的滑动效果
7. 如果滑动过程执行在非UI线程,在computeScroll方法中应调用postInvalidate()进行view重绘,否则会导致异常产生
使用动画实现
/**
* @param targetView 目标view
* @param dx , 偏移量
* @param duration
*/
private void smoothScroll(final View targetView, final int dx, int duration) {
final int left = targetView.getLeft();
final int right = targetView.getRight();
final ValueAnimator animator = ObjectAnimator.ofFloat(0, 1);
animator.setInterpolator(new AccelerateDecelerateInterpolator(mContext,null));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
final float fraction = animation.getAnimatedFraction();
targetView.layout((int) (left + fraction * dx), targetView.getTop(), (int) (right + fraction * dx), targetView.getBottom());
}
});
animator.start();
}
使用延时
- 使用handler的postDelayed实现
- 使用view自身的postDelayed实现
- 使用线程
/**
* @param targetView 目标view
* @param dx , 偏移量
*/
private void smoothScrollRunnable(final View targetView, final int dx) {
targetView.postDelayed(new ScrollRunnable(targetView, dx),10);
}
class ScrollRunnable implements Runnable {
private View targetView;
private int dx;
private int left, right; // targetView初始的左右位置
private float fraction; //位移比率
public ScrollRunnable(View targetView, int dx) {
this.targetView = targetView;
this.dx = dx;
left = targetView.getLeft();
right = targetView.getRight();
}
@Override
public void run() {
fraction += 0.04;
if (fraction < 1.0f) {
int delta = (int) (fraction * dx);
Log.e("", delta+"-------------- ");
targetView.layout(left + delta, targetView.getTop(), right + delta, targetView.getBottom());
postDelayed(this, 5);
}else {
targetView.layout(left + dx, targetView.getTop(), right + dx, targetView.getBottom());
}
}
}
三、 事件的分发机制浅析
1. 点击事件的传递规则
事件分发的三个重要方法及作用
public boolean dispatchTouchEvent(MotionEvent ev) 事件分发
public boolean onInterceptTouchEvent(MotionEvent ev)(ViewGroup) 事件拦截
public boolean onTouchEvent (MotionEvent ev) 事件处理
分发细节
- 如果view能够接受到点击事件,则dispatchTouchEvent一定会调用
- 在dispatchTouchEvent方法中onInterceptTouchEvent会调用
- 如果onInterceptTouchEvent返回值为true,则表示此view将消耗此事件,其onTouchEvent会被调用
- 如果onInterceptTouchEvent返回值为false,则此view不消耗此事件,其子view的dispatchTouchEvent方法将被调用,重复上述过程直到事件被处理
注意:只有ViewGroup才有onInterceptTouchEvent方法,简单的view不具备onInterceptTouchEvent方法
2. View事件的传递顺序
- 事件总是先传给Activity ,然后会走Activity -> Window(实现类PhoneWindow) -> View(从顶级decorview开始) -> ViewGroup -> View
- 一个view的onTouchEvent方法返回false,则其父view的onTouchEvent方法会被调用
- 若所有的view的onTouchEvent都返回false,则事件会往回传递,最终Activity的onTouchEvent方法会被调用
3. 优先级
onTouch > onTouchEvent > onClick
#四、 滑动冲突解决办法
###1. 滑动冲突处理规则
根据滑动的方向来判断该由谁来拦截事件
2. 两种解决方式
外部拦截法
- 点击事件都先由父容器进行拦截处理,父容器进行选择性拦截
- 如果父容器需要则拦截事件,在onInterceptTouchEvent方法中返回true,否则返回false
- 父容器的onInterceptTouchEvent方法的ACTION_DOWN必须返回false,否则子view收不到点击事件,因为如果返回true,则后续事件都会交由父容器处理
- 若父容器将事件交由子view处理,如果父容器ACTION_UP返回为true,则使得子view的ACTION_UP无法触发,导致子view的onClick无法触发
- 父容器一旦决定拦截事件,则后续事件都交由它处理,即使其onInterceptTouchEvent方法的ACTION_UP返回为false
在父容器中onInterceptTouchEvent的处理细节
boolean intercepted=false;
if(父容器要消耗的事件){
intercepted=true;
}else{
intercepted=false;
}
return intercepted;
内部拦截法
- 父容器不拦截任何事件,所有事件都交由子元素处理,如果子元素需要则消耗事件,否则交由父容器处理
- 需要配合requestDisallowInterceptTouchEvent()方法,父容器如果需要处理事件,则给此方法传递false
- 父元素默认拦截除了ACTION_DOWN的所有事件
注:此篇为总结篇,在前人的基础上,增加部分内容,作了自我梳理