绘制界面只是创建自定义视图的一个部分。您还需要让视图以与自己模仿的真实操作非常相似的方式响应用户输入。对象的行为方式应始终与真实物体相同。例如,图片不应瞬间消失后重新出现在其他地方,因为现实世界中的物体不会这样。相反,图片应从一个位置移动到另一个位置。
用户还能感受到界面中的细微行为变化或者给人带来的细微感觉变化,并对细微之处做出最佳反应来模仿现实世界。例如,当用户快滑某个界面对象时,开始快滑时要感受到导致运动延迟的摩擦力,结束时也要感受到使运动超出快滑范围的动量。
本课介绍了如何使用 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(PieChart.this.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; }
注意:虽然 GestureDetector
计算出的速度在物理上是准确的,但许多开发者认为使用此值会导致快滑动画速度过快。常见的做法是用 x 和 y 速度除以 4 到 8 之间的系数。
调用 fling()
将设置快滑手势的物理学模型。之后,您需要定期调用 Scroller.computeScrollOffset()
以更新 Scroller
。computeScrollOffset()
通过读取当前时间并使用物理模型计算当时的 x 和 y 位置,以此更新 Scroller
对象的内部状态。调用 getCurrX()
和 getCurrY()
可以检索这些值。
大多数视图会将 Scroller
对象的 x 和 y 位置直接传递到 scrollTo()
。PieChart 示例略有不同:它使用当前的滚动 y 位置设置图表的旋转角度。
Kotlin
scroller.apply { if (!isFinished) { computeScrollOffset() setPieRotation(currY) } }
Java
if (!scroller.isFinished()) { scroller.computeScrollOffset(); setPieRotation(scroller.getCurrY()); }
Scroller
类会为您计算滚动位置,但不会自动将这些位置应用到视图中。您要负责确保以足够高的频率获取和应用新坐标,使滚动动画看起来流畅。您可以采用下列两种方法:
- 调用
fling()
后再调用postInvalidate()
,以强制重新绘制。此方法要求您在onDraw()
中计算滚动偏移,并在每次滚动偏移更改时调用postInvalidate()
。 - 设置
ValueAnimator
为快滑过程添加动画,并通过调用addUpdateListener()
添加监听器以处理动画更新。
PieChart 示例采用的是第二种方法。此方法的设置稍微复杂一些,但可以更紧密地配合动画系统工作,并且不需要进行不必要的视图失效操作。缺点是 ValueAnimator
不适用于 API 级别 11 之前的版本,因此该技术无法在运行的 Android 版本低于 3.0 的设备上使用。
注意:您可以在面向较低 API 级别的应用中使用 ValueAnimator
。只需确保在运行时检查当前 API 级别,如果当前级别低于 11,则忽略对视图动画系统的调用。
Kotlin
private val scroller = Scroller(context, null, true) private val scrollAnimator = ValueAnimator.ofFloat(0f, 1f).apply { addUpdateListener { if (scroller.isFinished) { scroller.computeScrollOffset() setPieRotation(scroller.currY) } else { cancel() onScrollFinished() } } }
Java
scroller = new Scroller(getContext(), null, true); scrollAnimator = ValueAnimator.ofFloat(0,1); scrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { if (!scroller.isFinished()) { scroller.computeScrollOffset(); setPieRotation(scroller.getCurrY()); } else { scrollAnimator.cancel(); onScrollFinished(); } } });
实现流畅切换
用户希望现代界面能够在不同的状态之间流畅地切换。界面元素应淡入和淡出,而不是出现和消失。动作开始和结束都是平滑发生的,而不是突然开始和停止。Android 3.0 中引入的 Android 属性动画框架可以轻松实现平滑切换。
如需使用动画系统,每当属性更改会影响视图外观时,都不要直接更改属性,应使用 ValueAnimator
进行更改。在以下示例中,修改 PieChart 中当前选定的饼图分区会导致整个图表旋转,使选择指针在所选分区中居中。ValueAnimator
会用几百毫秒的时间更改旋转,而不是瞬间设定好新的旋转值。
Kotlin
autoCenterAnimator = ObjectAnimator.ofInt(this, "PieRotation", 0).apply { setIntValues(targetAngle) duration = AUTOCENTER_ANIM_DURATION start() }
Java
autoCenterAnimator = ObjectAnimator.ofInt(PieChart.this, "PieRotation", 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();