绘制 UI 只是创建自定义视图的一部分。您还需要使视图以与您模拟的现实世界操作非常相似的方式响应用户输入。
使应用中的对象像真实对象一样运作。例如,不要让应用中的图像突然消失然后出现在其他地方,因为现实世界中的物体不会这样做。相反,将图像从一个地方移动到另一个地方。
用户会感知界面中细微的行为或感觉,并且对模拟现实世界的细微之处反应最好。例如,当用户轻弹 UI 对象时,请给他们一种初始的惯性感,从而延迟运动。在运动结束时,给他们一种动量感,使物体超越轻弹。
此页面演示如何使用 Android 框架的功能来向您的自定义视图添加这些现实世界行为。
处理输入手势
与许多其他 UI 框架一样,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); }
仅触摸事件本身并没有特别有用。现代触摸 UI 根据手势(例如点击、拉动、推动、轻弹和缩放)定义交互。为了将原始触摸事件转换为手势,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
。然后,您可以运行您自己的自定义手势检测代码。
创建物理上合理的运动
手势是控制触摸屏设备的强大方法,但是除非它们产生物理上合理的成果,否则它们可能会违反直觉并且难以记住。
例如,假设您想实现一个水平轻弹手势,该手势设置视图中绘制的项目围绕其垂直轴旋转。如果 UI 通过在轻弹方向快速移动然后减速来响应,则此手势是有意义的,就像用户推动飞轮并使其旋转一样。
关于如何动画滚动手势的文档详细解释了如何实现您自己的滚动行为。但是模拟飞轮的感觉并非易事。需要大量的物理和数学知识才能使飞轮模型正常工作。幸运的是,Android 提供了辅助类来模拟此行为和其他行为。Scroller
类是处理飞轮式轻弹手势的基础。
要开始轻弹,请使用起始速度以及轻弹的最小和最大x和y值调用fling()
。对于速度值,您可以使用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
类为您计算滚动位置,但它不会自动将这些位置应用于您的视图。经常应用新坐标以使滚动动画看起来流畅。有两种方法可以做到这一点
- 调用
postInvalidate()
后调用fling()
强制重绘。此技术要求您在onDraw()
中计算滚动偏移量,并在每次滚动偏移量更改时调用postInvalidate()
。 - 设置
ValueAnimator
以轻弹持续时间进行动画处理,并添加一个侦听器以通过调用addUpdateListener()
来处理动画更新。此技术允许您设置View
的属性动画。
使您的过渡流畅
用户期望现代 UI 在状态之间平滑过渡:UI 元素淡入淡出而不是出现和消失,以及运动平滑地开始和结束而不是突然启动和停止。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();