Veiw 是Android中所有控件的基类。View是一种界面层的控件的抽象。
ViewGroup也是继承之View,翻译为控件组。ViewGroup内部包含了许多控件。所以View本身可以是单个控件也可以是由多个控件组成的一个控件。
例:
LinearLayout不但是一个View还是一个ViewGroup,而ViewGroup内部可以有子View的,这个子View同样还可以是ViewGroup。
View的位置由四个顶点来决定,分别对应于四个属性:top,left,right,bottom。这四个参数都是相对于Veiw的父容器来说的,是相对坐标。
· ACTION_DOWN 手指刚接触屏幕
· ACTION_MOVE 手指在屏幕上移动
· ACTION_UP 手指从屏幕上松开的一瞬间
通过MotionEvent对象可以得到点击事件发生的x 和 y坐标。系统提供了两组方法:
getX/getY 和 getRawX/getRawY
区别:
getX/getY返回的是相对于当前View的左上角的x和y坐标;
getRawX/getRawY返回的是相对于屏幕的左上角的x和y的坐标
系统所能识别出的被认为是滑动的最小距离。也就是说,当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,
系统会认为这不是一次滑动。需要注意的是,不同的
设备这个常量值可能是不相同的。可通过如下的方式获取这个常量值:
ViewConfiguration.get(getContext()).getScaledTouchSlop()
速度跟踪,用于追踪手指在滑动过程中的速度,包括水平和垂直方向的速度。
首先,在View的onTouchEvent方法中追踪当前的点击事件的速度:
>
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracler.addMovement(event);
接着,
>
velocityTracker.computeCurrentVelocity(1000);
int xVelocity= (int)velocityTracker.getXVelocity();
int yVelocity= (int)velocityTracker.getYVelocity();
这里需要注意两点:
第一: 获取速度之前必须先计算速度,即必须先调用computeCurrentVelocity(1000)之后是getXVelocity(),getYVelocity()
第二: 这里的速度是在指定时间内划过的像素数
例:将时间间隔设为1s,在1s内,手指在水平方向从左向右滑动100像素,水平速度就是100;
速度也可以为负数。当手指从右向左滑过100像素,水平方向的速度就是负值。
公式表示:
速度 = (终点位置-起始位置)/ 时间段
最后,当不需要的时候,需要调用clear方法重置并回收内存。
>
velocityTracker.clear();
velocityTracker.recycle();
手势检测,用于辅助检测用户单击、滑动、长按和双击等行为。(如果只是监听滑动相关,建议在onTouchEvent;
如果是双击,需要使用GestureDetector)
首先,需要创建一个GestureDetector对象实现onGestureListener接口,
这里根据需要还可以实现OnDoubleTapListener 实现双击监听
>
GestureDetector mGestureDetector = new GestureDetector(this);
//解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);
接着,接管目标的View的OnTouchEvent方法,在待监听的View的OnTouchEvent中添加:
>
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
然后,可以有选择地实现OnGestureListener 和 OnDoubleTapListener中的方法
Socroller本身无法让View弹性滑动,需要和View的computeScroll配合使用
>
Scroller scroller = new Scroller(this);
//缓慢滑到指定位置
private void smoothScrollTo(int destX, int destY){
int scrollX = getScrollX();
int delta= destX - getScrollX();
//1000ms内滑向destX,效果就是慢慢滑动
mScroller.startScroll(scrollX,0 , delta,0 , 1000);
invalidata();
}
@Override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postiInvalidate();
}
}
一般View的滑动通过三种方式实现:
第一种通过View本身的提供的scrollTo/scrollBy方法实现
第二种通过动画给View施加平移效果实现滑动
第三种通过改变View的LayoutParams使得View重新布局从而实现滑动
public void scrollTo(int x,int y){
if( mScrollX != x && mScrollY != null){
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX,,mScrollY,oldX, oldY);
if( !awakenScrollBars()){
postInvalidateOnAnimation();
}
}
}
/*************************************/
public void scrollBy(int x , int y){
scrollTo(mScrollX + x, mScrollY + y);
}
scrollBy()调用scrollTo()方法。scrollTBy()基于当前位置的相对滑动,scrollTo()实现了基于
所传递参数的绝对滑动。
* 注意:*
使用scrollTo()和scrollBy()来实现View的滑动,只能将View上的内容进行移动,
并不能将View本身进行移动。
无论怎么滑动都不可能将当前的View滑动到附近的View所在区域。
使用动画主要是操作View的transklationX和transklationY属性
动画代码,此动画在100ms内将一个View从原始位置向右下角移动100个像素。
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:zAdjustment="normal">
<translate
adnroid:duration = "100"
android:fromXDelta = "0"
android:fromYDelta = "0"
android:interpolator = "@android:anim/linear_interpolator"
android:toXDelta = "100"
android:toYDelta = "100" />
</set>
属性动画代码
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
View的动画是对View的影像做操作,并不能真正改变View的位置参数,包括宽/高,
如果希望保留动画后的状态,将
fillAfter设置为true;
MarginLayoutParams params = (MarginLayoutParams)mButtion1.getLayoutParams();
params.width += 100;
params.leftMargin += 100;0
mButtion1.requestLayout();
//或者mButton1.setLayoutParams(params)
-scrollTo/scrollBy : 操作简单,适合对View内容的滑动
-动画 : 操作简单,适合用于没有交互的View和实现复杂的动画效果
-改变布局参数 : 操作稍微复杂了点,适用于有交互的View
Scroller源码:
Scroller scroller = new Scroller(mContext);
//缓慢滚动到指定位置
private void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int deltaX = destX - srollX;
//1000ms内滑向destX,效果就是慢慢滑动
mScroller.startScroll(scrollX,0,deltaX,1,1000);
invalidate();
}
@Override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
* public boolean dispatchTouchEvent(MotionEvent ev) *
如果事件能够传递给当前View,此方法一定会被调用,返回结果受当前View的oTouchEvent 和
下级View的dispatchTouchEvent 方法的影响,表示是否消耗当前事件
* public boolean onInterceptTouchEvent(MotionEvent event) *
在上述内部调用,用来判断是否拦截某个事件,如果当前View拦截某个事件,
那么同一事件序列当中,此方法不会再调用,返回结果表示是否拦截当前事件
* public boolean onTouchEvent(MotionEvent event) *
在dispatchTouchEvent 方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,
如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false ;
if(onInterceptTouchEvent (ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
传递规则:
对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个
ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即
它的onTouchEvent方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示不拦截当前事件,
这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会调用,如此反复,直到事件被处理。
当一个View需要处理事件时,如果它设置了onTouchListener,onTouchListener中的onTouch方法会被回调。这件事如何处理还要
看onTouch的返回值,如果返回false。则当前View的onTouch方法会被调用;如果返回true,那么onTouchEvent方法将不会被调用。
由此可见,给View设置onTouchListener,其优先级比onTouchEvent要高。在onTouch方法中,如果设置的有onClickListener,那么
onClick方法会被调用。
点击事件的传递顺序: activity --> window --> View
点击事件用MotionEvent来表示,当一个点击操作发生时,事件最先传递给当前的Activicity,由Activity的disaptchTouchEvent
来进行事件的派发,具体的工作是由Activcity内部的Window来完成的。Window会将事件传递给decor view, decor view一般就是当前
界面的底层容器,通过Activity.getWindow.getDecorView()获得
源码:Activity#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev){
if(ev.getAction() == MotionEvent.ACTION_DOWN){
onUserInteraction();
}
if(getWindow().superDispatchTouchEvent (ev)){
return true;
}
return onTouchEvent(ev);
}
事件开始交给Acitivity所属的Winow进行分布,如果返回true,整个事件循环就结束,返回false
意味着事件没人处理,所有View的onTouchEvent都返回了false,Activity的onTouchEvent会调用。Window
将事件传递给ViewGroup的。
Window是个抽象类,Window的dispatchTouchEvent是个抽象方法,实现类是PhoneWindow。
源码:PhoneWindow#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent ev){
return mDecor.superDispatchTouchEvent(ev);
}
PhoneWindow将事件直接传递给了DecorView。
通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)
获得Activity所设置的View,这个mDecor就是getWindow().getDecorView()返回的View,通过setContentView
设置的View就是mDecor的子View。目前事件传递到了这里。
View事件分发回顾:
点击事件达到顶级View(一般是一个ViewGroup)以后,会调用ViewGroup的dispatchTouchEvent方法;
此时,
· 如果顶级ViewGroup拦截事件onInterceptTouchEvent返回 true,则事件由ViewGroup本身处理。
这时如果ViewGroup的mOnTouchListener被设置,则OnTouch被调用,否则OnTouchEvent会被调
用。也就是说如果能提供的话,onTouch会屏蔽掉onToucheEvent。在onTouchEvent中,如果设
置了mOnClickListener,则onClick会被调用。
· 如果顶级ViewGroup不拦截事件,则事件会传递到它所在的点击事件链上的子View,这个时候子
View的dispatchTouchEvent会被调用。
* 分发过程 和 处理过程第一遍没看懂*
View的setClickable 和 setOnLongClickListener会自动将View的 CLICKABLE 和 LONG_CLICKABLE属性
设置为true。
常见的滑动冲突场景,简单分为三种:
* 针对场景1的解决方式 *
所谓外部拦截是指事情都经过父容器的拦截处理,如果父容器需要此事件就拦截;
如果不需要就不拦截。外部拦截法需要重写父容器的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;
}
在onInterceptTouchEvent方法中,首先是在ACTION_DOWN,父容器必须返回false,即不拦截
ACTION_DOWN,ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,这个时候事件没法再传递
子元素了;其次是ACTION_MOVE事件,这个事件可以根据需求决定是否需要拦截,如果父容器需要
拦截就返回true,否则返回false;最后是ACTION_UP事件,必须要返回false,因为ACTION_UP时事
件本身没有啥意义。
内部拦截法指父容器不拦截任何事件,所有的事件都传给子元素,如果子元素需要此事件就
直接消耗掉,若不需要,就交由父容器进行处理,这种方法和Andorid中的事件分发机制不一致,
需要配合requestDisallowInterceptTouchEvent方法才能正常工作m,使用起来外部拦截稍显复杂。
伪代码如下:
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.requestDisalowInterceptTouchEvent(false)方法时,父元素才能拦截所需的
事件。
为什么父容器不能拦截ACTION_DOWN 事件呢?那是因为ACTION_DOWN 事件并不受FLAG_DISALLOW_INTERCEPT这个标记的控制,所以一旦父容器拦截ACTION_DOWN事件,那么所有的事件都无法传递到子元素中去,这样内部拦截就起不了作用,所以父元素做下面的改动。
public boolean onInterceptTouchEvent(MotionEvent event){
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN){
return false;
}else{
return true;
}
}