绘制界面只是创建自定义视图的一部分。您还需要让视图以与模拟的真实世界操作密切相似的方式响应用户输入。
让应用中的对象行为类似于现实世界中的对象。例如,不要让应用中的图片突然消失又在其他地方重新出现,因为现实世界中的对象不会那样做。相反,将您的图片从一个地方移动到另一个地方。
用户可以感知界面中即使是微妙的行为或感受,并且对模仿真实世界的细微之处反应最佳。例如,当用户快速滑动界面对象时,应使其在开始时具有惯性感,延迟移动。在移动结束时,应使其具有动量感,使对象超出快速滑动范围。
本页面演示了如何使用 Android 框架的功能将这些真实世界的行为添加到您的自定义视图中。
处理输入手势
与许多其他界面框架一样,Android 支持输入事件模型。用户操作会转化为事件,这些事件会触发回调,您可以覆盖这些回调以自定义应用如何响应用户。Android 系统中最常见的输入事件是 触摸,它会触发 onTouchEvent(android.view.MotionEvent)
。如下所示,覆盖此方法来处理事件:
Kotlin
override fun onTouchEvent(event: MotionEvent): Boolean { return super.onTouchEvent(event) }
Java
@Override public boolean onTouchEvent(MotionEvent event) { return super.onTouchEvent(event); }
仅靠触摸事件本身并不是特别有用。现代触摸界面根据手势(如轻触、拉动、推动、快速滑动和缩放)定义交互。要将原始触摸事件转换为手势,Android 提供了 GestureDetector
。
通过传递实现 GestureDetector.OnGestureListener
类的实例来构造 GestureDetector
。如果您只想处理少数手势,可以扩展 GestureDetector.SimpleOnGestureListener
,而不是实现 GestureDetector.OnGestureListener
接口。例如,此代码创建一个扩展 GestureDetector.SimpleOnGestureListener
并覆盖 onDown(MotionEvent)
的类。
Kotlin
private val myListener = object : GestureDetector.SimpleOnGestureListener() { override fun onDown(e: MotionEvent): Boolean { return true } } private val detector: GestureDetector = GestureDetector(context, myListener)
Java
class MyListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDown(MotionEvent e) { return true; } } detector = new GestureDetector(getContext(), new MyListener());
无论您是否使用 GestureDetector.SimpleOnGestureListener
,都要始终实现返回 true
的 onDown()
方法。这是必需的,因为所有手势都始于 onDown()
消息。如果您从 onDown()
返回 false
(就像 GestureDetector.SimpleOnGestureListener
那样),系统会假定您想忽略其余手势,并且不会调用 GestureDetector.OnGestureListener
的其他方法。只有在您想忽略整个手势时才从 onDown()
返回 false
。
实现 GestureDetector.OnGestureListener
并创建 GestureDetector
实例后,您可以使用 GestureDetector
来解释您在 onTouchEvent()
中接收到的触摸事件。
Kotlin
override fun onTouchEvent(event: MotionEvent): Boolean { return detector.onTouchEvent(event).let { result -> if (!result) { if (event.action == MotionEvent.ACTION_UP) { stopScrolling() true } else false } else true } }
Java
@Override public boolean onTouchEvent(MotionEvent event) { boolean result = detector.onTouchEvent(event); if (!result) { if (event.getAction() == MotionEvent.ACTION_UP) { stopScrolling(); result = true; } } return result; }
当您将 onTouchEvent()
传递给不识别为手势一部分的触摸事件时,它会返回 false
。然后您可以运行自己的自定义手势检测代码。
创建物理可信的运动
手势是控制触摸屏设备的强大方式,但除非产生物理上可信的结果,否则它们可能反直觉且难以记住。
例如,假设您想实现一个水平快速滑动的手势,使视图中绘制的项目围绕其垂直轴旋转。如果界面通过快速向快速滑动方向移动然后减速来响应,那么这个手势是合理的,就好像用户推动飞轮使其旋转一样。
关于如何为滚动手势添加动画效果的文档详细解释了如何实现自己的滚动行为。但是,模拟飞轮的感觉并非易事。要使飞轮模型正常工作需要大量的物理和数学知识。幸运的是,Android 提供了辅助类来模拟此行为和其他行为。Scroller
类是处理飞轮式快速滑动手势的基础。
要开始快速滑动,调用 fling()
,传入起始速度以及快速滑动的最小和最大 x 和 y 值。对于速度值,您可以使用由 GestureDetector
计算的值。
Kotlin
fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean { scroller.fling( currentX, currentY, (velocityX / SCALE).toInt(), (velocityY / SCALE).toInt(), minX, minY, maxX, maxY ) postInvalidate() return true }
Java
@Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { scroller.fling(currentX, currentY, velocityX / SCALE, velocityY / SCALE, minX, minY, maxX, maxY); postInvalidate(); return true; }
调用 fling()
会设置快速滑动手势的物理模型。之后,定期调用 Scroller.computeScrollOffset()
来更新 Scroller
。 computeScrollOffset()
通过读取当前时间并使用物理模型计算此时的 x 和 y 位置来更新 Scroller
对象的内部状态。调用 getCurrX()
和 getCurrY()
来检索这些值。
大多数视图将 Scroller
对象的 x 和 y 位置直接传递给 scrollTo()
。此示例有所不同:它使用当前滚动 x 位置来设置视图的旋转角度。
Kotlin
scroller.apply { if (!isFinished) { computeScrollOffset() setItemRotation(currX) } }
Java
if (!scroller.isFinished()) { scroller.computeScrollOffset(); setItemRotation(scroller.getCurrX()); }
Scroller
类会为您计算滚动位置,但不会自动将这些位置应用于您的视图。经常应用新的坐标以使滚动动画看起来流畅。有两种方法可以做到这一点:
- 调用
fling()
后,通过调用postInvalidate()
强制重绘。此技术要求您在onDraw()
中计算滚动偏移量,并在每次滚动偏移量变化时调用postInvalidate()
。 - 设置
ValueAnimator
,在快速滑动期间为其添加动画效果,并通过调用addUpdateListener()
添加监听器来处理动画更新。此技术可让您为View
的属性添加动画效果。
使过渡平滑
用户期望现代界面能够在状态之间平滑过渡:界面元素淡入淡出而不是突然出现和消失,以及运动平稳开始和结束而不是突然启动和停止。Android 属性动画框架使平滑过渡更容易。
要使用动画系统,无论何时属性更改影响您的视图外观时,都不要直接更改属性。相反,使用 ValueAnimator
进行更改。在以下示例中,修改视图中选中的子组件会使整个渲染视图旋转,从而使选择指针居中。ValueAnimator
会在几百毫秒的时间内更改旋转角度,而不是立即设置新的旋转值。
Kotlin
autoCenterAnimator = ObjectAnimator.ofInt(this, "Rotation", 0).apply { setIntValues(targetAngle) duration = AUTOCENTER_ANIM_DURATION start() }
Java
autoCenterAnimator = ObjectAnimator.ofInt(this, "Rotation", 0); autoCenterAnimator.setIntValues(targetAngle); autoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION); autoCenterAnimator.start();
如果您要更改的值是基础 View
属性之一,进行动画效果会更容易,因为视图内置了 ViewPropertyAnimator
,它针对同时为多个属性添加动画效果进行了优化,如下例所示:
Kotlin
animate() .rotation(targetAngle) .duration = ANIM_DURATION .start()
Java
animate().rotation(targetAngle).setDuration(ANIM_DURATION).start();