Android动画转场的本质

我们进行转场动画最常用的就是Translation。我们查看源代码知道Translation的父类是Visibility。我们在自定义转场动画的时候只需要实现onAppear() 和 onDisappear() 方法。只需要在这里面去创建对应的Animator对象,实现各自的动画就好了。现有的实现类有Fade,Slide,Explode

View有自带的做缩放平移旋转…..的api:

​ 这些动画的底层实现都是交给了RenderNode来做。

1
2
3
4
5
6
public void setScaleY(float scaleY) {
mRenderNode.setScaleY(scaleY);
}
public void setRotationY(float rotationY) {
mRenderNode.setRotationY(rotationY);
}

通过上面这个方法修改view的属性,他的点击事件也会跟随view走。

补间动画不能改变触摸事件的区域,但是属性动画可以。但是为什么可以呢?

通过查看属性动画的源码得知:

1
2
3
4
5
6
public float getX() {
return mLeft + getTranslationX();
}
public float getY() {
return mTop + getTranslationY();
}

我们获取x 和 y的时候都分别加上了对应的Translation,而属性动画更新的就是这个值。所以通过getX getY可以获取到正确的值。

那问题又来了,如果说坐标指的是left,top,right,bottom,那就不对了。通过上面分析可以看到,我们在做动画的时候只更改了getX 和 getY的值,而并没有去修改这四个值。我们可以打开开发者选项里的显示布局边界:

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
mView.setOnTouchListener(new View.OnTouchListener() {

int lastX, lastY;
Toast toast = Toast.makeText(TestActivity.this, "", Toast.LENGTH_SHORT);

@Override
public boolean onTouch(View v, MotionEvent event) {
int x = (int) event.getRawX();
int y = (int) event.getRawY();

if (event.getAction() == MotionEvent.ACTION_MOVE) {
//Toolbar和状态栏的高度
int toolbarHeight = (getWindow().getDecorView().getHeight() - findViewById(R.id.root_view).getHeight());
int widthOffset = mView.getWidth() / 2;
int heightOffset = mView.getHeight() / 2;

mView.setTranslationX(x - mView.getLeft() - widthOffset);
mView.setTranslationY(y - mView.getTop() - heightOffset - toolbarHeight);

toast.setText(String.format("left: %d, top: %d, right: %d, bottom: %d",
mView.getLeft(), mView.getTop(), mView.getRight(), mView.getBottom()));
toast.show();
}
lastX = x;
lastY = y;
return true;
}
});
img1

可以发现,当view移动的时候,哪个框框并没有移动,打印的left值也没变。

那我们直接改layout:

1
2
3
4
5
6
7
8
9
10
11
@Override
public boolean onTouch(View v, MotionEvent event) {
...
if (event.getAction() == MotionEvent.ACTION_MOVE) {
...
mView.layout(x - widthOffset, y - heightOffset - toolbarHeight,
x + widthOffset, y + heightOffset - toolbarHeight);
...
}
return true;
}
img1

可以发现用layout移动view,那个框框也是会动的。

还有一个实现改变view的位置的方法:这个方法也不是正真改变控件的位置。

1
2
view.offsetTopAndBottom(offectY)
view.offsetLeftAndRight(offectX)

那现在问题来了。既然setTranslation没有改变正真的坐标,那为什么触摸的区域确会跟着移动呢?

Android实现圆弧滑动效果之ArcSlidingHelper篇_陈小缘的博客-CSDN博客

这里我们就需要看事件分发的代码了:

事件传递顺序:

img1

先从Activity传到PhonwWindow才传到Decorview,DecorView是继承自ViewGroup,然后就走到了ViewGroup的dispatchTouchEvent(Event e)方法。

进入viewGroup首先判断这个view是不是可见 and 有没有被遮挡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//ViewGroup 判断这个view是不是可见 and 有没有被遮挡:
if (onFilterTouchEventForSecurity(ev)) {
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 重置整个调用链子的状态 ViewGroup里面有一个参数 mFirstTouchTarget 保存了整个调用链的数据,在第一down事件的时候他为null
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 可以看到调用 onInterceptTouchEvent方法
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;
}
}

// 判断点击的点是不是在这个控件范围l
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempLocationF();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}

手指按下屏幕的时候,首先进入viewgroup的dispatchTouchEvent(),在onInterceptTouchEvent方法里面判断是否拦截,如果不拦截,开始到序的方式遍历viewGroup里面的全部子view。遍历的过程中,依次判断手指按下的坐标,是否在当前子view的区间里面,如果不在里面,就continue,开始下一个子view的判断。当找到合适的时候,执行dispatchTransformedTouchEvent()方法,看看子view是否处理这个事件,在里面执行了child.dispatchTouchEvent(event);方法。如果这个child是一个viewgroup方法,那么会再次执行上面这一串逻辑,如果是view,那么我们进入view的dispatchTouchEvent方法。在进入view的dispatchTouchEvent方法我们可以看到如果这个view设置了setOnTouchListener方法,那么点击事件就会传给这个回调里面,就不会走setOnClickListener设置的点击事件,如果没设置setOnTouchListener,那么就会接着执行view的onTouchEvent方法。

这个child.dispatchTouchEvent(event)返回true的时候代表这次的down事件有人处理了。

然后方法继续回到viewGroup的dispatchTouchEvent方法,给newTouchTargets附上当前这个view的值,然后mFirstTouchTarget赋值 = newTouchTargets,然后告诉这次的事件找到了执行者 alreadyDispatchedToNewTouchTarget = true