一. 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 - left
height = 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 + translationX
y = 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重新布局, 从而实现滑动
②.使用动画
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:fromAlpha="1.0"
android:toAlpha="1.0"
android:duration="550"/>
</set>
复制代码
为了兼容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