一. View基础知识
- view的位置参数
- MotionEvent和TouchSlop
- Velocity,GentureDetecor和Scroller
二. 什么是View
View是Android中所有的组件的基类,包括系统提供的控件,如:Button,TextView,Relativelayout和Listview还是自定义控件他们的共同基类都是view,所以说.View是一种界面层的控件的一种抽象,它代表了一个控件.除了View,还有ViewGroup,从名字来看,它可以翻译为控件组,ViewGroup里面可以包括多个控件,即一组View,在Android的设计中,ViewGroup也继承View,View本身就可以是单个控件也可以是多个控件组成的一组控件,这种关系就View树的结构.
例如:Button显然是一个View,但LinearLayout不但是一个View而且还是一个ViewGroup,而ViewGroup内部可以有子View的,这个子View可能还是View复制代码
三. View的位置参数
- top 左上角纵坐标
- left 左上角横坐标
- right 右下角横坐标
- bottom 右下角纵坐标 这些坐标都是以父容器为参考系的,因此它是一种相对坐标
width = right - leftheight = bottom - top复制代码
那么如何得到这四个参数呢?
Left = getLeft()Right =getRight()Top = getTop()Bottom = getBottom()复制代码
从 Android 3.0 开始增加了这几个额外的参数 x,y.translationX 和 teanslationY , 其中 x, y 是 View 左上角的坐标.而 translationX 和 teanslationY 是 View 左上角相对于的偏移量. 这几个参数也是相对于父容器的偏移量. translationX 和 teanslationY 默认值是 0 , View 也为 他们提供默认的set/get 方法
x = left + translationXy = top + teanslationY复制代码
四. MotionEvent和TouchSlop
①.MotionEvent
- 手指触摸屏幕后产生一系列事件
- ACTION_DOWN -- 手指刚接触屏幕
- ACTION_MOVE -- 手指在屏幕上移动
- ACTION_UP -- 手指从屏幕松开的一瞬间
- 正常情况,一次手指触摸屏幕的行为会触发一系列的点击事件
- 点击屏幕后离开松开 事件序列 DOWN --> UP
- 点击屏幕滑动一会再松开 DOWN --> MOVE --> .. --> MOVE --> UP
- 通过 MotionEvent 对象我们可以获取 点击事件发生的 x 和 y 坐标,为此系统提供了 两组方法:
- getX / getY (当前View 左上角 x 坐标 和 y坐标)
- getRawX / getRawY (屏幕左上角 x 坐标 和 y 坐标)
②.TouchSlop
TouchSlop 是系统所能识别出被认为是滑动的最小距离 ,当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统不认为他是滑动的,这个常量值和设备有关,不同的设备这个值可能有所差异 如何回去这个常量呢? ViewConfiguration.get(getContext()).getScaledTouchTop()
五. Velocity,GentureDetecor和Scroller
①.Velocity
速度追踪,用来追踪手指滑动过程中的速度,包括水平速度和垂直速度,他的使用过程很简单
- 在View 的 onTouchEvent方法中追踪当前点击事件的速度
final VelocityTracker obtain = VelocityTracker.obtain();obtain.addMovement(event);复制代码
- 获取当前滑动速度
obtain.computeCurrentVelocity(1000); final int xVelocity = (int) obtain.getXVelocity(); final int yVelocity = (int) obtain.getYVelocity();复制代码
当不需要的时候,注意重置回收
obtain.recycle(); obtain.clear();复制代码
②.GentureDetecor
手势检测,用户辅助检测用户单击,滑动,长按,双击行为
- 创建一个GentureDetecor对象并实现OnGestureListener接口,根据需要我们还可以实现 OnDoubleTapListener 从而能够监听双击行为
final GestureDetector detector = new GestureDetector(this); // 解决长按屏幕无法拖动的现象 detector.setIsLongpressEnabled(false);复制代码
- 接管目标 View 的 onTouchEvent 方法,在待监听 View 的 onTouchEvent 方法中添加如下实现
final boolean consume = detector.onTouchEvent(event); return consume;复制代码
方法名 | 描述 | 所属接口 |
---|---|---|
onDown(触摸放开) | 手指轻轻触摸屏幕一瞬间,由 1 个 ACTION_DOWN 触发 | OnGestureListener |
onShowPress(触摸未松动) | 手指轻轻触摸屏幕,尚未松动或拖动,由1个 ACTION_DOWN 触发 * 注意 和 onDown() 区别是,强调的是没有松开或拖动的状态 | OnGestureListener |
onSingleTapUp(单击) | 手指松开,伴随着1个 MotionEvent ACTION_UP 而触发,这是单击行为 | OnGestureListener |
onLongPress (长按) | 用户长久地按着屏幕不放 | OnGestureListener |
onFling(快速滑动) | 用户按下触摸屏,快速滑动松开,由1个 ACTION_DOWN ,多个 ACTION_MOVE 和 ACTION_UP触发,这就是快速滑动行为 | OnGestureListener |
OnDoubleTab(双击) | 双击,由两次单击组成,它不可能和 OnSingleTabConfirm 共存 | OnDoubleTabListener |
OnSingleTabConfirm(严格单击行为) | 严格单击行为,只响应一次 | OnDoubleTabListener |
OnDoubleTabEvent (双击) | 双击行为 | OnDoubleTabListener |
onScroll(拖动) | 手指按下屏幕并拖动,由1个ACTION_DOWN,多个ACTION_MOVE触发,这就是拖动行为 | OnGestureListener |
③.Scroller
弹性滑动对象
final Scroller scroller = new Scroller(this);复制代码
六. View的滑动
如何实现View的滑动
①. scrollTo / scrollBy
public void scrollTo(int x, int y) { if (mScrollX != x || mScrollY != y) { 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); }复制代码
-
View 施加平滑效果实现View的滑动
-
改变View的LayoutParam 使得 View重新布局, 从而实现滑动
②.使用动画
复制代码
为了兼容3.0以下版本我们需要引入 nineoldAndroid,但是注意一点的是View动画只能改变View的影像,并不能改变View的布局参数
③.改变布局参数
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mMore.getLayoutParams(); params.width += 100; params.leftMargin += 100; mMore.requestLayout(); //或者 mMore.setLayoutParams(params);复制代码
③.各种滑动方式对比
- scrollTo / scrollBy
适合对View内容的滑动
- 动画
适用于没有交互的 View 和 实现复杂的动画效果
- 改变布局
操作复杂,适用于交互的View
@Override public boolean onTouchEvent(MotionEvent event) { final int x = (int) event.getRawX(); final int y = (int) event.getRawY(); switch (event.getAction()){ case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: int deltaX = x - mLastX; int deltaY = y - mLastY; final int translationX = (int)ViewHelper.getTranslationX(this) + deltaX; final int translationY = (int)ViewHelper.getTranslationX(this) + deltaY; ViewHelper.setTranslationX(this,translationX); ViewHelper.setTranslationY(this,translationY); break; case MotionEvent.ACTION_UP: break; default: break; } mLastX = x; mLastY = y; return true; }复制代码
④.弹性滑动
如何实现弹性滑动?
将一次大的滑动分成若干的小滑动,并在一定的时间内完成
⑤.使用Scroller
public void startScroll(int startX, int startY, int dx, int dy, int duration) { mMode = SCROLL_MODE; mFinished = false; mDuration = duration; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartX = startX; mStartY = startY; mFinalX = startX + dx; mFinalY = startY + dy; mDeltaX = dx; mDeltaY = dy; mDurationReciprocal = 1.0f / (float) mDuration; }复制代码
- Scroller 如何让 View 滑动的?
public boolean computeScrollOffset() { if (mFinished) { return false; } int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); if (timePassed < mDuration) { switch (mMode) { case SCROLL_MODE: final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); mCurrX = mStartX + Math.round(x * mDeltaX); mCurrY = mStartY + Math.round(x * mDeltaY); break; case FLING_MODE: final float t = (float) timePassed / mDuration; final int index = (int) (NB_SAMPLES * t); float distanceCoef = 1.f; float velocityCoef = 0.f; if (index < NB_SAMPLES) { final float t_inf = (float) index / NB_SAMPLES; final float t_sup = (float) (index + 1) / NB_SAMPLES; final float d_inf = SPLINE_POSITION[index]; final float d_sup = SPLINE_POSITION[index + 1]; velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); distanceCoef = d_inf + (t - t_inf) * velocityCoef; } mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f; mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX)); // Pin to mMinX <= mCurrX <= mMaxX mCurrX = Math.min(mCurrX, mMaxX); mCurrX = Math.max(mCurrX, mMinX); mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY)); // Pin to mMinY <= mCurrY <= mMaxY mCurrY = Math.min(mCurrY, mMaxY); mCurrY = Math.max(mCurrY, mMinY); if (mCurrX == mFinalX && mCurrY == mFinalY) { mFinished = true; } break; } } else { mCurrX = mFinalX; mCurrY = mFinalY; mFinished = true; } return true; }复制代码
⑥.通过动画
TODO: 博主暂时也没有弄明白
ObjectAnimator.ofFloat(view, "translationX", 0, 100).setDuration(100).start(); final int startX = 0; final int startY = 100; final ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) @Override public void onAnimationUpdate(ValueAnimator animation) { final float fraction = animator.getAnimatedFraction(); mHome.scrollTo(startX + (deltaX * fraction),0); } }); animator.start();复制代码
⑦.使用延时策略
通过发送一系列的延时消息从而达到一种渐进式的效果
private static final int MESSAGE_SCROLL_TO = 1; private static final int FRAME_COUNT = 30; private static final int DELAYED_TIME = 33; private int mCount = 0; protected Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); int what = msg.what; switch (what) { case MESSAGE_SCROLL_TO: mCount++; if (mCount <= FRAME_COUNT) { final float fraction = mCount / (float) FRAME_COUNT; final int scrollX = (int) (fraction * 100); mAutoLogin.scrollTo(scrollX,0); mHandler.sendMessageDelayed(MESSAGE_SCROLL_TO,DELAYED_TIME); } break;}}复制代码
七. View的事件分发机制
①. 点击事件的传递规则
点击事件传递过程中涉及一个很重要的API,就是MotionEvent.所谓的点击事件的事件分发就是对MotionEvent事件的分发过程,当一个MotionEvent产生后,系统需要将这个事件传递给具体的View,而这个传递的过程就是分发的过程. 中间风阀过程涉及三个重点的方法:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { return super.dispatchTouchEvent(ev); }复制代码
进行事件分发,如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的TouchEvent和下级View的dispatchTouchEvent影响,表示正在消耗当前事件
public boolean onIntercrptTouchEvent(MotionEvent ev) {}复制代码
用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一事件序列中,此方法不会被调用
@Override public boolean onTouchEvent(MotionEvent event) { boolean consume = false; if (onInterceptTouchEvent(event)){ consume = onTouchEvent(event); }else { consume = child.dispatchTouchEvent(event); } return consume; }复制代码
对于一个根View而言,当点击事件发生以后,它的dispatchevent就会被调用,如果onInterceptTouchEvent方法返回true,就表示拦截此事件,接着事件就会交给ViewGroup处理,即他的TouchEvent会被调用,如果这个ViewGrop返回false,那么表示不拦截这个事件,这时,当前事件就会传递给他的子元素,接着子元素的dispatchEvent方法就会被调用,如此反复直至事件被消耗完毕 当一个View处理事件时,它设置了onTouchListener,那么onTouchListener中的onTouch方法就会被调用,这时事件如何处理还要看onTouch的返回值,如果返回false,则当前view的ontouchEvent方法会被调用,如果返回为true,那么onTouchEvent方法将不会被调用.由此可见onTouchListener的优先级比 onTouchEvent还高,在 onTouchEvent 方法中 , 如果当前设置的有onClickListener,那么 onClick 方法会被调用,平时我们常用的 onClickListerner 优先级最低 点击事件的传递顺序在ui层的优先顺序表现为 Activity -> Window -> View
- ViewGroup 默认不拦截任何事件, Android源码中 ViewGrop 默认返回 false
public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.isFromSource(InputDevice.SOURCE_MOUSE) && ev.getAction() == MotionEvent.ACTION_DOWN && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY) && isOnScrollbarThumb(ev.getX(), ev.getY())) { return true; } return false; }复制代码
- View 没有 oninterceptEvent 方法,一旦点击事件传递给它,那么它的 onTouchEvent 就会被调用
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return clickable; } if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; if ((viewFlags & TOOLTIP) == TOOLTIP) { handleTooltipUp(); } if (!clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; } boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // take focus if we don't have it already and we should in // touch mode. boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { // The button is being released before we actually // showed it as pressed. Make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. setPressed(true, x, y); } if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // This is a tap, so remove the longpress check removeLongPressCallback(); // Only perform take click actions if we were in the pressed state if (!focusTaken) { // Use a Runnable and post this rather than calling // performClick directly. This lets other visual state // of the view update before click actions start. if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_DOWN: if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) { mPrivateFlags3 |= PFLAG3_FINGER_DOWN; } mHasPerformedLongPress = false; if (!clickable) { checkForLongClick(0, x, y); break; } if (performButtonActionOnTouchDown(event)) { break; } // Walk up the hierarchy to determine if we're inside a scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true, x, y); checkForLongClick(0, x, y); } break; case MotionEvent.ACTION_CANCEL: if (clickable) { setPressed(false); } removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; break; case MotionEvent.ACTION_MOVE: if (clickable) { drawableHotspotChanged(x, y); } // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button // Remove any future long press/tap checks removeTapCallback(); removeLongPressCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; } break; } return true; } return false; }复制代码
- 一个事件序列所有的事件都只能由一个 View 完成,也就是谁当一个View决定拦截一个事件后,那么系统会将所有的事件方法分配给它处理,因此不会再调用这个 View 的 onInterceptEvent 去询问他是否要拦截了
- 如果同序列事件传递给一个View处理,那么它就必须消耗掉
- View 的 onTouchEvent 默认都会消耗事件,除非他是不可点击的
- View 的 longClickable 属性是 false; Button clickable 是 true , TextView 的 clickable 默认是 false
- 事件的传递都是由外向内传递的,即事件总是先传递给父元素,然后再由父元素分发给子View.通过 requestDisallowInterceptTouchEvent 方法 可以在 子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 除外
@Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { // We're already in this state, assume our ancestors are too return; } if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // Pass it up to our parent if (mParent != null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); } }复制代码
②. 事件分发源码分析
点击事件 用 MotionEvent 表示,当一个点击事件发生以后,事件先传递给 Activity,由Activity 的 dispatchEvent 进行事件派发,具体的工作由Activity的 window 完成,window 将事件传递给 dector view,dector view 一般就是当前界面的底层容器(即 setContentView 所设置的 View的父容器),通过Activity.getWindow. getDectorView()可以获得
i.Activty对点击事件的分发过程
public boolean dispatchTouchEvent(MotionEvent event) { if (event.isTargetAccessibilityFocus()) { if (!isAccessibilityFocusedViewOrHost()) { return false; } event.setTargetAccessibilityFocus(false); } boolean result = false; if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { stopNestedScroll(); } if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; } } if (!result && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && !result)) { stopNestedScroll(); } return result; }复制代码
ii.window将事件传递给Activity的过程
public abstract boolean superDispatchTouchEvent(MotionEvent event);复制代码
iii.顶级View对点击事件的分发过程
TODO: 难度系数太高,参考<<Android艺术与探索>> page146 ~ 151
iv.view对点击事件的处理过程
TODO: 难度系数太高,参考<<Android艺术与探索>> page151 ~ 154
八.View的滑动冲突
- 外部滑动方向和内部滑动方向的不一致 ViewPager 和 Fragment 配合使用所组成的页面滑动效果,主流应用几乎都会使用这个效果.在这种效果中,可以左右滑动来切换页面,而每个页面往往又是一个ListView.本来这种情况下是有滑动冲突的,ViewPager 内部处理了这种冲突,因此采用Viewpager时我们无须关注这个问题,如果我们采用不是ViewPager 而是 ScroolView 等,那就必须处理滑动冲突了,否则造成的后果就是内外两层只能有一层能够滑动,这是滑动因为,这是因为两者之间的滑动事件有冲突,除了这两种情况,还存在其他情况,比如外部上下滑动,内部左右滑动等,它们属于同类一类滑动冲突
- 外部滑动方向和内部滑动方向的一致性 当手指滑动用户无法知道到底是想让哪一层滑动,所以当手指滑动就会出现问题,系统不知道用户到底是王哪一层滑动,所以当手指滑动就会出现一种问题,要么只有一层滑动,要么就是两层滑动就会很卡顿
- 上述两种情况均存在 如:外部有一个SlideMenu效果,然后内部有一个ViewPage,ViewPage的每一个页面中又是一个ListView,但是他是几个单一的滑动事件的总合
九.常见的滑动冲突场景
TODO: 难度系数太高,参考<<Android艺术与探索>> page155 ~ 156
①滑动冲突的处理规则
TODO: 难度系数太高,参考<<Android艺术与探索>> page156 ~ 157
②滑动冲突的解决方式
TODO: 难度系数太高,参考<<Android艺术与探索>> page158 ~ 159
i.外部拦截法
ii.内部拦截法
TODO: 难度系数太高,参考<<Android艺术与探索>> page159 ~ 173