1.1使用Scroller
这里的滑动是指view内容的滑动。
当我们构造一个Scroller对象,并调用它的startScroll方法的时候,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;
}
主要靠startScroll下面的invalidate方法,重绘的时候会去调用computeScroll,使用scrollTo滑动的时候再去调用Scroller的scrollX和scrollY。
例子:
public class SmButton extends Button {
private Context mContext;
private Scroller mScroller;
public SmButton(Context context, AttributeSet attrs) {
super( context, attrs);
this. mContext = context;
mScroller = new Scroller( mContext);
}
@Override
public void computeScroll() {
if ( mScroller != null) {
if ( mScroller.computeScrollOffset()) {
scrollTo( mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate(); // So we draw again
// ViewCompat.postInvalidateOnAnimation(this);
}
}
}
public void smoothScrollTo( int destX, int destY) {
int scrollX = getScrollX();
int delta = destX - scrollX;
// 1000ms内滑向destX,效果就是慢慢滑动
mScroller.startScroll( scrollX, 0, delta, 0, 1000);
invalidate();
}
}
通过执行:
btn.smoothScrollTo(-200, 0);
之后:
所以我们需要紧记,这里的滑动指的是view内容的滑动,并且遵循前面说的
规律,从左向右滑动,mScrollX为负值,从上往下滑动mScrollY为负值。
1.2通过动画
通过以下代码让View的内容在100ms内,向左移动100像素。
ObjectAnimator.ofFloat(targetView, “translationX” , 0,100).setDuration(100).start();
模仿Scroller实现view的弹性滑动:
例子,实现上面的效果:
public void smoothScrollTo2( final int destX, final int destY) {
final ValueAnimator animator = ValueAnimator.ofFloat(0,1).setDuration(1000);
animator.addUpdateListener( new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animator.getAnimatedFraction();
scrollTo(( int)( destX* fraction), destY);
}
});
animator.start();
}
1.3使用延时策略
下面代码在一定的时间内让view的内容向左滑动100像素:
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELAYED_TIME = 33;
private View mButton;
private int mCount = 0;
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
switch ( msg. what) {
case MESSAGE_SCROLL_TO: {
mCount++;
if ( mCount <= FRAME_COUNT) {
float fraction = mCount / ( float) FRAME_COUNT;
int scrollX = ( int) ( fraction * 100);
mButton.scrollTo( scrollX, 0);
mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
}
break;
}
}
};
};
2.1点击事件的传递规则
http://www.cnblogs.com/sunzn/archive/2013/05/10/3064129.html
public boolean dispatchTouchEvent(MotionEvent ev)
进行事件的分发,如果事件能够传递给当前的view那么一定会调用这个
方法。返回的结果受当前view的onTouchEvent和下级view的dispat-
chTouchEvent方法影响,表示是否消耗当前事件。
public boolean onInterceptTouchEvent(MotionEvent ev)
在dispatchTouchEvent内部调用,用来判断是否拦截某个事件,如果当
前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方法返回true,表示它不需要
拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的
dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理;
当一个view需要处理事件时,如果它设置了onTouchListener,那么onTouch-
Listener中的onTouch方法就会被回调,如果onTouch返回false那么,onTou-
chEvent会被调用,如果返回true,那么onTouchEvent不会被调用;
onTouch比onTouchEvent优先级要高,在onTouchEvent里面会调用onClick
方法,平常我们使用的onClickListener,优先级最低,处于事件传递的底端。
事件传递的顺序:
Activity–>Window–>View;
事件总是优先传递给Activity,Activity再传递给Window,最后Window再
传递给顶级view,顶级view接收到事件之后就会按照事件分发机制去分发
事件。
如果view的onTouchEvent返回false,那么它的父容器的onTouchEvent会被
调用,如果所有的元素都不处理这个事件,那么这个事件最后会传递给Activity
去处理,即Activity的onTouchEvent方法会被调用。
一些结论:
2.1.1.同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻
结束,在这个过程中产生的一系列事件,这个事件序列以down事件开始,中间
含有数量不定的move事件,最终以up事件结束。
2.1.2.正常情况下,一个事件序列只能被一个view拦截且消耗,一旦一个元素拦截了
某此事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事
件序列中的事件不能分别由两个view同时处理,但是通过特殊手段可以做到,比如
一个view将本该自己处理的事件通过onTouchEvent强行传递给其他view处理。
2.1.3.某个view一旦决定拦截,那么这一个事件序列都只能由它来处理,并且它的
onInterceptTouchEvent不会再被调用。就是说当一个view决定拦截一个事件后,
那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不再调用
这个view的onInterceptTouchEvent去询问它是否要拦截了。
2.1.4.某个view一旦开始处理事件,如果它不消耗action_down事件(onTouchEvent
返回false),那么同一事件序列中的其他事件都不会再交给他来处理,并且事件
将重新交给它的父元素去处理,父元素的onTouchEvent会被调用。意思是事件一旦
交一个view处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交
给它来处理了。
2.1.5.如果view不消耗除action_down以外的其他事件,那么这个点击事件会消失,
此时父元素的onTouchEvent并不会被调用,并且当前view可以持续收到后续的
事件,最终这些消失的点击事件会传递给Activity处理。
2.1.6.ViewGroup默认不拦截任何事件,Android源码中的onInterceptTouchEvent
方法默认返回false。
2.1.7.view没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的
onTouchEvent方法就会被调用。
2.1.8.view的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的
(clickable和longclickable同时为false)。view的longclickable默认属性都为
fasle,clickable的属性要分情况,比如Button的clickable属性默认为true,而
TextView的clickable属性默认为false。
2.1.9.view的enable属性不影响onTouchEvent的默认返回值,哪怕一个view是
disable的,只有它是可点击的,那么它的onTouchEvent就返回true。
2.1.10.onClick会发生的前提是当前view是可点击的,并且它收到了down和up的
事件。
2.1.11.事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素
分发给子view,通过requestDisallowInterceptTouchEvent方法可以在子元素中
干预父元素的事件分发过程,但是action_down事件除外。
2.2事件分发的源码解析
1)Window的实现类为PhoneWindow。
2)获取Activity的contentView的方法((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);
2.3View的滑动冲突
2.3.1.外部拦截的经典写法:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent. ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent. ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (父容器需要当前的点击事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent. ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
针对不同的滑动冲突只需要修改父容器需要当前的点击事件的条件即可。
注意:在onInterceptTouchEvent的方法中,ACTION_DOWN这个事件必须
返回false,即不拦截ACTION_DOWN事件。
因为一旦拦截ACTION_DOWN事件,后续的事件都会直接交给父容器处理,
没办法传递给子元素了。
2.3.2内部拦截法:
@Override
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.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent. ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent( event);
}
父容器也需要拦截除了ACTION_DOWN以外的其他事件
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent. ACTION_DOWN) {
return false;
} else {
return true;
}
}