创建交互式自定义视图

尝试 Compose 方法
Jetpack Compose 是 Android 推荐的 UI 工具包。了解如何在 Compose 中使用布局。

绘制 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,始终实现一个返回trueonDown() 方法。这是必要的,因为所有手势都以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 类是处理飞轮式轻弹手势的基础。

要开始轻弹,请使用起始速度以及轻弹的最小和最大xy 值调用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() 来更新ScrollercomputeScrollOffset() 通过读取当前时间并使用物理模型计算该时间点的xy 位置来更新Scroller 对象的内部状态。调用getCurrX()getCurrY() 以检索这些值。

大多数视图将Scroller 对象的xy 位置直接传递给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 的属性设置动画。

使您的过渡流畅

用户期望现代 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();