カスタムビューをインタラクティブにする

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 を実装するクラスのインスタンスを渡して、GestureDetector.OnGestureListener を構築します。ごく少数のジェスチャーだけを処理する場合は、GestureDetector.OnGestureListener インターフェースを実装するのではなく、GestureDetector.SimpleOnGestureListener を拡張します。たとえば、次のコードでは、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() メッセージで始まるため、この手順は必須です。GestureDetector.SimpleOnGestureListener のように onDown() から false を返すと、システムはジェスチャーの残りを無視すると見なして、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 クラスはスクロール位置を計算しますが、その位置をビューに自動的に適用することはありません。スクロール アニメーションがスムーズに見えるように、新しい座標を頻繁に適用します。これを行うには、次の 2 つの方法があります。

  • fling() を呼び出した後に postInvalidate() を呼び出して、再描画を強制します。この手法の場合、スクロール オフセットが変更するたびに、 onDraw() でスクロール オフセットを計算し、postInvalidate() を呼び出す必要があります。
  • をセットアップして、フリングが継続している間、アニメーション化し、 を呼び出すことで、 アニメーションの更新を処理するリスナーを追加します。 ValueAnimatoraddUpdateListener()この手法を使用すると、 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 プロパティの 1 つである場合、さらに簡単にアニメーションを実行できます。ビューには、次の例のように、複数のプロパティを同時にアニメーション化するように最適化されたビルトイン ViewPropertyAnimator が用意されています。

Kotlin

animate()
    .rotation(targetAngle)
    .duration = ANIM_DURATION
    .start()

Java

animate().rotation(targetAngle).setDuration(ANIM_DURATION).start();