文章目录
1. 使View滑动
1.1 View基本知识
x并不是等于left,x与left的关系如下
- x = left + translationX
- y = top + translationY
1.2 使用scrollTo/scrollBy
此方法移动的是VIew的内容,并非VIew的实际位置
protected int mScrollX;
protected int mScrollY;
将view像左移动100像素,向上移动30像素,scrollTo的移动方向和我们熟知的x轴和y轴方向是相反的
mTextView.scrollTo(100, 30)
scrollBy本质来说也调用的是scrollTo,scrollTo是绝对移动,scrollBy是相对移动
1.3 使用动画来移动View
这种方式改变的是View的以下属性
translationX 和 translationY
public float getTranslationX() {
return mRenderNode.getTranslationX();
}
使用ObjectAnimator来移动View
ObjectAnimator.ofFloat(animaTextView, "translationX", a, 100f).apply {
duration = 100
start()
}
1.4 修改layoutParams来移动View
val marginLayoutParams = (layoutView.layoutParams as MarginLayoutParams)
marginLayoutParams.marginStart = 300
layoutView.layoutParams = marginLayoutParams
2. View的弹性滑动
如上1.View的滑动所示,这样移动VIew,有点像闪现,没有移动的过程,在现实使用中并不好看,所以为了有更好的移动效果,需要使用弹性滑动。
实现弹性滑动的方式也有三种,Scroller, 属性动画, 使用延时策略
2.1 Scroller
比如使用Scroller去弹性滑动移动一个TestView
private lateinit var scrollView: TextView
private lateinit var mScroller: Scroller
override fun onCreate(savedInstanceState: Bundle?){
val scrollBtn: Button = findViewById(R.id.smooth_scroller_btn);
scrollView = findViewById(R.id.ScrollerText)
scrollBtn.setOnClickListener {
scrollView.setScroller(mScroller)
smoothScrollerTo(-200, 0)
}
Log.d(TAG, "btn.scrollX: " + btn.scrollX + " btn.scrollY: " + btn.scrollY)
}
private fun smoothScrollerTo(destX: Int, destY: Int) {
val curX = scrollView.scrollX
val deltaX = destX - curX
mScroller.startScroll(curX, 0, deltaX, 0, 2000)
scrollView.invalidate()
}
Scroller弹性滑动原理
- 首先看看Scroller.startScroll方法, 这个方法并不是开始弹性滑动,仅仅是对进行弹性滑动所需要的参数进行赋值
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;
}
2.真正开始弹性滑动的方法为scrollView.invalidate
其中最终要的是computeScroll方法, 以TextView的为例
@Override
public void computeScroll() {
if (mScroller != null) {
if (mScroller.computeScrollOffset()) {
mScrollX = mScroller.getCurrX();
mScrollY = mScroller.getCurrY();
invalidateParentCaches();
postInvalidate(); // 再一次重绘View
}
}
}
大致逻辑就是不断地改变mScrollX和mScroll,然后重新drawView来实现弹性滑动,可以间接理解成不断的去调用scrollTo方法
那不断地drawView什么时候停止?
这就需要看mScroller.computeScrollOffset()方法,这个方法主要做了如下几个事情
public boolean computeScrollOffset() {
//如果动画已经完成,return false,从而中断computeScroller方法的调用
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;
}
2.2使用动画
像上面有使用ObjectAnimator去实现完全是可以的,这里我们换种方式,使用ValueAnimator去实现
val valueAnimatorBtn: Button = findViewById(R.id.smooth_animter_btn);
val valueAnimatorTextView: TextView = findViewById(R.id.smooth_text_btn)
val startX = 0
val deltaX = 400
valueAnimatorBtn.setOnClickListener {
ValueAnimator.ofInt(0, 1).apply {
duration = 2000
addUpdateListener {
val fraction = it.animatedFraction
valueAnimatorTextView.scrollTo(-(startX + (deltaX * fraction).toInt()), 0)
}
start()
}
}
2. 3 使用延时策略
/*
* 假设需要1000ms 移动 300px, 每次移动30像素
* 总共要移动 300 / 30 = 10次 delay时间 1000 / 10 = 100ms
* */
private var count = 0
private val mHandler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
count ++
when (msg.what) {
1 -> {
count++
if (count <= 10) {
handleView.scrollBy(-30, 0)
sendEmptyMessageDelayed(1, 100)
}
}
else -> {}
}
}
}
View的事件分发机制
MotionEvent是对应点击事件的封装类,常见的点击事件是down、up、move,分别对应按下,抬起,移动
如何把一个点击事件分发给某一个特定的VIew,这就需要了解Android的事件分发机制。
点击事件分发机制由3个重要方法组成
public boolean dispatchTouchEvent(MotionEvent ev)
//onInterceptTouchEvent方法存在于ViewGroup中,View没有
public boolean onInterceptTouchEvent(MotionEvent ev)
public boolean onTouchEvent(MotionEvent event)
这三个方法的关系是
通过dispatchTouchEvent方法去给VIew分发事件,VIew收到事件后再判断是否拦截该事件,如果要拦截,这个事件才真正的被这个View所消费,否则就将这个事件分发给子View
除了上述三个方法外,还有一个比较重要的Listener,OnTouchListener,可以通过View的setOnTouchListener设置
public interface OnTouchListener {
boolean onTouch(View v, MotionEvent event);
}
OnTouchListener的优先级比onTouchEvent高,如果onTouch的方法返回true。代表OnTouchListenr消费这个事件,即该View的onTouchEvent不会被调用,因为onClickListener再OnTouchEvent中被调用,所以该View的点击事件也会失效
最终的事件分发的总体框架如下图所示
关于View的事件分发有如下几个结论
- 从手指按压屏幕,移动,再到抬起,被称作一个事件序列
- 正常情况下,一个事件序列只能被同一个View所消费
- 一个ViewGroup一但决定拦截(onInterceptTouchEvent返回ture),那么这一个事件序列都有它来处理
- 某个VIew如果不消耗down事件,那么一个事件序列中的其他事件也不会让他来处理
- ViewGroup默认不消耗任何事件,因为它的onInterceptTouchEvent方法默认返回false
- View没有onInterceptTouchEvent方法,默认都消耗事件,除非View不可点击(clickable, longClickable同时为false)
- View的enbale不会改变onTouchEvent的返回值
事件分发源码解析
1.Activty事件分发的过程
当一个事件出现时,会以以下的顺序进行传递,Activity–> window ->顶层View(DecorView)
decorView是顶层View,我们Activity通过setContentView方法设置的VIew最终会设置到DecorView的子元素上(android.R.id.content),DecorVIew的大致结构如下
事件最终流传到了ViewGroup的dispatchTouchEvent方法中,接下来重点看看这个方法
2.ViewGroup事件分发
先看ViewGrop如何判断是否拦截某个事件
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;
//如果是down事件 或者 有子元素处理事件时
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
一个ViewGroup是否需要通过onInterceptTouchEvent方法来判断是否拦截事件,取决于两个因素,并且必须都满足
- 是否是down事件 || 或者有子View接受事件,mFirstTouchTarget 是接受事件的子View的引用
actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null
- FLAG_DISALLOW_INTERCEPT标记位,如果没有这个标记位,才会调用onInterceptTouchEvent方法
这个标记位可通过requestDisallowInterceptTouchEvent方法却设置,并且绝对不会影响down事件,因为resetTouchState方法会把这个标记位抹除
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
结论1:如果VIewGroup拦截down事件,那么其他事件也会被VIewGroup拦截,并且onInterceptTouchEvent方法只会再down事件判断一次,其他事件序列将不用onInterceptTouchEvent再次判断 原因如下:
接下来看看如果ViewGroup不拦截事件,事件是如何分发到子VIew的
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//此处省略一些源码
for (int i = childrenCount - 1; i >= 0; i--) {
//此处省略一些源码
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//此处省略一些源码
}
}
首先如上源码所示,在down事件时,遍历所有的子元素,判断子View是否有资格被分发事件,判断条件有2条,并且条件是或的关系
- 通过canReceivePointerEvents方法去判断 View是VISIBLE,或者正在播放动画
/**
* Returns whether this view can receive pointer events.
* @return {@code true} if this view can receive pointer events.
* @hide
*/
protected boolean canReceivePointerEvents() {
return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}
- 通过isTransformedTouchPointInView方法去判断 点击的是否是这个View的区域
这个方法就不详细讲了,大致实现方式就是通过的点击的x和y坐标结合View的长宽和位置去判断
判断完子View是否有资格被分发事件后就会走到下面的逻辑
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
如上源码所示其中最关键就是dispatchTransformedTouchEvent方法,在这个方法中回去调用子View的dispatchTouchEvent方法,大致伪代码如下所示
//如果子View为null,就会调用父类(View)的dispatchTouchEvent方法
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
//将事件分发给子View
handled = child.dispatchTouchEvent(event);
}
再刚开始分析源码时提到了mFirstTouchTarget成员,他会影响ViewGroup是否拦截某个事件的判断,该成员的赋值时机就在addTouchTarget方法中
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
根据衣裳代码会发现mFirstTouchTarget 本质是一个链表头,连接着每个被分发事件的子VIew,并采用头插法插入新元素,所以之前才会说mFirstTouchTarget != null代表着已经有事件成功分发给的子元素。当一个事件序列被分发完mFirstTouchTarget 链表就会被清空
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//该方法会便利链表全部置空
resetTouchState();
当然也会有这样一种情况,如果将事件分发给子元素,但是子元素也不消耗这个事件,即onTouchEvent方法返回false,如果时这种情况mFirstTouchTarget还是为null的状态,就会走到如下的逻辑中
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
dispatchTransformedTouchEvent方法的child参数为空,上面也分析过这个方法,如果child==null,就会调用super.dispatchTouchEvent方法,该方法最终又会调用ViewGroup的onTouchEvent方法
结论:如果一个子VIew不消费分过来的事件,即onTouchEvent返回false,最终事件会交给ViewGroup的onTouchEvent来处理, 即子元素不处理事件,父亲来处理
down事件时才会遍历子元素,才有机会给mFirstTouchTarget赋值,所以这就得出一个结论:
如果一个VIew不接受down事件,一个事件序列中后面的事件也一定不会给它了
3.View的事件分发
上面说到了如果子View不接受ViewGroup分发的事件,就会调用super.dispatchTouchEvent方法,也就是View的dispatchTouchEvent方法,我们接下来分析下这个方法
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
上面的源码也印证了上面之前说的一个结论:mOnTouchListener的优先级别比onTouchEvent高,如果mOnTouchListener的onTouch方法的返回值时true,那onTouchEvent就不是执行
接下来好好看看onTouchEvent方法
if ((viewFlags & ENABLED_MASK) == DISABLED
&& (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
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;
}
从上面源码中,我们又印证了一个结论一个View是否消费事件与是否enable无关,只与是否click有关
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
.................省略
switch (action) {
case MotionEvent.ACTION_UP:
performClickInternal();
.....................省略
}
.................省略
}
.................省略
}
如果VIew的可点击的,会在up事件时通过performClickInternal方法去调用onClickListener的onClick方法
onTouchEvent主要是View怎么处理事件的逻辑,这里不再细讲
总结