每日一问:到底为什么属性动画后 View 在新位置还能响应事件

在 Android 开发中,我们难免会使用动画来处理各种各样的动画效果,以满足 UI 的高逼格设计。对于比较复杂的动画效果,我们通常会采用著名的开源库:lottie-android,或许你会对 lottie 的原理充满好奇,但这并不在我们这篇文章的讨论范围,感兴趣的自行 Google 吧~

属性动画和补间动画的基本编写方式

我一度在论坛上看到人使用了 TranslateAnimation 对控件做了移动操作,然后发现在 View 的新位置点击并没有响应自己的点击事件,反倒是之前的位置能够响应。实际上,补间动画仅仅是对 View 在视觉效果上做了移动、缩放、旋转和淡入淡出的效果,其实并没有真正改变 View 的属性。但我们大多数情况下肯定希望 View 在经过动效后响应触摸事件的位置和视觉效果相同,所以在 Android 3.0 之后引入了属性动画,彻底解决了这个难题。

可能还有一些小伙伴不明白怎样的代码是属性动画,怎样的代码是补间动画。下面针对 View 向右平移 500 px 做一下简单的演示。

对于属性动画,你可以用下面的两种方式。

ObjectAnimator.ofFloat(tv1, "translationX", 0f, 500f)
                    .setDuration(1000)
                    .start()
// 或者像这样
tv1.animate().setDuration(1000).translationX(500f)

但用补间动画,并且你想达到同样的效果的话

val anim = TranslateAnimation(0f, 500f, 0f, 0f)
anim.duration = 1000
anim.fillAfter = true    // 设置保留动画后的状态
tv1.startAnimation(anim)

属性动画的使用注意点

对于属性动画来说,尤其需要注意的是操作的属性需要有 set 和 get 方法,不然你的 ObjectAnimator 操作就不会生效。比如水平平移,我们知道,View 的 translationX 属性设置方法接受的是 float 值,所以你把上面的操作编写为 ofInt 就不会生效,比如:

ObjectAnimator.ofInt(tv1, "translationX", 0, 500)
                    .setDuration(1000)
                    .start()

对于我们需要用到但又没有写好的属性,比如我们自定义一个进度条 View,我们需要实时展示进度,这时候我们就可以自己定义一个属性,并让它支持 set 和 get,那么在外面就可以对这个自定义的 View 做属性动画操作了。

属性动画和补间动画工作原理

属性动画

属性动画的工作原理很简单,其实就是在一定的时间间隔内,通过不断地对值进行改变,并不断将该值赋给对象的属性,从而实现该对象在属性上的动画效果。

这个属性可以是任意对象的属性。

从上述工作原理可以看出属性动画有两个非常重要的类:ValueAnimator 类 & ObjectAnimator 类,二者的区别在于:
ValueAnimator 类是先改变值,然后 手动赋值 给对象的属性从而实现动画;是 间接 对对象属性进行操作;而 ValueAnimator 类本质上是一种 改变值 的操作机制。

ObjectAnimator 类是先改变值,然后 自动赋值 给对象的属性从而实现动画;是 直接 对对象属性进行操作;可以理解为:ObjectAnimator 更加智能、自动化程度更高。

补间动画

而对于补间动画,我们不妨跟进源码,看看到底做了什么操作。

/**
 * Start the specified animation now.
 *
 * @param animation the animation to start now
 */
public void startAnimation(Animation animation) {
    animation.setStartTime(Animation.START_ON_FIRST_FRAME);
    setAnimation(animation);
    invalidateParentCaches();
    invalidate(true);
}

看到了非常明显 invalidate() 方法,很明显,补间动画在执行的时候,直接导致了 View 执行 onDraw() 方法。总的来说,补间动画的核心本质就是在一定的持续时间内,不断改变 Matrix 变换,并且不断刷新的过程。

为什么属性动画移动一个 View 后,目标位置还可以响应触摸事件呢?

这个问题来自 wanandroid,在此前,我一直认为既然 View 的属性得到了改变,那么经过属性动画后的控件应该所有属性都等同于直接设置在动画后的位置的控件。

看完「陈小缘」的回答后,我突然才想到,虽然 View 做了属性上的改变,但其实并没有更改 View 的 leftrighttopbottom 这些属性,而这些属性恰恰决定了 ViewGroup 的触摸区域判断。

tv1.animate().setDuration(1000).translationX(500f)

那么,假定我们的 View 经过了上面的平移操作后,为什么点击新的位置能够响应到这个点击事件呢?

看了「陈小缘」的回答,我顺便深入了一波源码,想想必须在这分享给大家。

我们知道,在 ViewGroup 没有重写 onInterceptTouchEvent() 方法进行事件拦截的时候,我们一定会通过其 dispatchTouchEvent() 方法进行事件分发,而决定我们哪一个子 View 响应我们的触摸事件的条件又是 我们手指的位置必须在这个子 View 的边界范围内,也就是 leftrighttopbottom 这四个属性形成的矩形区域。

那么,如果我们的 View 已经进行了属性动画后,现在手指响应的触摸位置区域肯定不是 View 自己的leftrighttopbottom 这四个属性形成的区域了,但这个 View 却神奇的响应了我们的点击事件。

/**
 * Returns a MotionEvent that's been transformed into the child's local coordinates.
 *
 * It's the responsibility of the caller to recycle it once they're finished with it.
 * @param event The event to transform.
 * @param child The view whose coordinate space is to be used.
 * @return A copy of the the given MotionEvent, transformed into the given View's coordinate
 *         space.
 */
private MotionEvent getTransformedMotionEvent(MotionEvent event, View child) {
    final float offsetX = mScrollX - child.mLeft;
    final float offsetY = mScrollY - child.mTop;
    final MotionEvent transformedEvent = MotionEvent.obtain(event);
    transformedEvent.offsetLocation(offsetX, offsetY);
    if (!child.hasIdentityMatrix()) {
        transformedEvent.transform(child.getInverseMatrix());
    }
    return transformedEvent;
}

/**
 * Returns true if the transform matrix is the identity matrix.
 * Recomputes the matrix if necessary.
 *
 * @return True if the transform matrix is the identity matrix, false otherwise.
 */
final boolean hasIdentityMatrix() {
    return mRenderNode.hasIdentityMatrix();
}

/**
 * Utility method to retrieve the inverse of the current mMatrix property.
 * We cache the matrix to avoid recalculating it when transform properties
 * have not changed.
 *
 * @return The inverse of the current matrix of this view.
 * @hide
 */
public final Matrix getInverseMatrix() {
    ensureTransformationInfo();
    if (mTransformationInfo.mInverseMatrix == null) {
        mTransformationInfo.mInverseMatrix = new Matrix();
    }
    final Matrix matrix = mTransformationInfo.mInverseMatrix;
    mRenderNode.getInverseMatrix(matrix);
    return matrix;
}   

答案:

原来,ViewGroup 在 getTransformedMotionEvent() 方法中会通过子 View 的 hasIdentityMatrix() 方法来判断子 View 是否应用过位移、缩放、旋转之类的属性动画。如果应用过的话,那还会调用子 View 的 getInverseMatrix() 做「反平移」操作,然后再去判断处理后的触摸点是否在子 View 的边界范围内。

作者:南尘

https://www.cnblogs.com/liushilin/p/11094002.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。
Android动画会改变View位置和大小,如果不正确处理,会导致View点击事件位置也改变。 解决方法: 1. 使用属性动画(ObjectAnimator)替代补间动画(TranslateAnimation、ScaleAnimation等),属性动画不会改变View位置和大小。 2. 在补间动画结束后,重设置View位置和大小。 3. 在View的onTouchEvent方法中,使用getRawX和getRawY获取触摸点的坐标,而不是getX和getY获取相对于View的坐标。这样可以避免动画导致的位置变化。 示例代码: 使用属性动画: ```java ObjectAnimator animator = ObjectAnimator.ofFloat(view, "translationX", 0, 100); animator.setDuration(1000); animator.start(); ``` 在补间动画结束后重设置View位置和大小: ```java TranslateAnimation animation = new TranslateAnimation(0, 100, 0, 0); animation.setDuration(1000); animation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { view.setTranslationX(100); } @Override public void onAnimationRepeat(Animation animation) { } }); view.startAnimation(animation); ``` 在View的onTouchEvent方法中使用getRawX和getRawY获取触摸点的坐标: ```java @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { float x = event.getRawX(); float y = event.getRawY(); // 处理点击事件 return true; } return super.onTouchEvent(event); } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值