Как реализовать вращающийся перетаскиваемый интерфейс иконок? - PullRequest
0 голосов
/ 31 января 2019

Прикрепленное изображение является требованием для пользовательского интерфейса приложения, состоящего из группы значков, которые должны вращаться, как старый поворотный телефон.Четыре значка на круге можно перетаскивать пальцем, чтобы повернуть все значки (вместе), и когда они отпущены, они сопоставляются с иконкой, ближайшей к нижней части, щелкая в этом нижнем положении, с выбранным им и текстом под сводкой этого раздела.,т. е. когда пользовательский интерфейс не перетаскивается, на циферблате часов может быть только четыре позиции (12, 15, 18, 21:00).

Я не реализовывал перетаскиваемый пользовательский интерфейс, подобный этому ранее.Как бы я лучше пойти об этом?Стоит ли пытаться использовать MotionLayout или отслеживать сенсорные события, изменять положение вращения значка «Виды», а затем на событии «вверх» анимировать поворот на «щелчок» с ближайшим значком внизу?

1 Ответ

0 голосов
/ 05 февраля 2019

Я вспомнил, что ConstraintLayout v1.1 + имеет круговое позиционное ограничение, которое сделало анимацию довольно простой.Что было не так просто, так это обрабатывать щелчки, так как я не мог найти способа заставить их пройти к ImageViews под перетаскиваемым оверлейным представлением, поэтому пришлось подсчитать, на каком из них щелкнули.Для тех, кто хочет реализовать что-то подобное, вот код (обратите внимание, что он использует привязку данных Android).Макет пользовательского интерфейса отвечает за размеры экрана и изменяет размеры значков как% пользовательского интерфейса просмотра.

Этот код не поддерживает броски или поворотный диск для «блокировки» в определенных положениях, но оба могутбыть добавлены с анимациями, которые начинаются после окончания перетаскивания.

RotaryView.kt:

import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintLayout
import timber.log.Timber

/**
 * Displays a circle of icons that rotate and can be selected (if at the bottom position)
 * or clicked
 */
class RotaryView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {

    private var binding: ViewRotaryBinding = ViewRotaryBinding.inflate(LayoutInflater.from(context), this, true)
    private var callback: RotaryListener? = null

    fun setUp(callback: RotaryListener) {
        this.callback = callback
        callback.onNutritionSelected() // Default selection
        binding.dragOverlay.setOnTouchListener(DragListener())
    }

    override fun onWindowFocusChanged(hasFocus: Boolean) {
        setConstraintRadius(binding.dashboardMind)
        setConstraintRadius(binding.dashboardFitness)
        setConstraintRadius(binding.dashboardNutrition)
        setConstraintRadius(binding.dashboardVirtualWorld)
    }

    /// Private methods

    private fun setConstraintRadius(view: View) {
        val layoutParams = view.layoutParams as ConstraintLayout.LayoutParams
        layoutParams.circleRadius = width / 3
        view.layoutParams = layoutParams
    }

    private fun rotateDialer(angleDelta: Float) {
        setIconAngle(binding.dashboardMind, angleDelta) { callback?.onMindSelected() }
        setIconAngle(binding.dashboardFitness, angleDelta)  { callback?.onFitnessSelected() }
        setIconAngle(binding.dashboardNutrition, angleDelta)  { callback?.onNutritionSelected() }
        setIconAngle(binding.dashboardVirtualWorld, angleDelta)  { callback?.onVirtualWorldSelected() }
    }

    private fun setIconAngle(imageView: ImageView, angleDelta: Float, showSummary: ()->Unit) {
        val layoutParams = imageView.layoutParams as ConstraintLayout.LayoutParams
        val newAngle = normaliseAngle(layoutParams.circleAngle.toInt() + angleDelta.toInt())
        if (newAngle in 136..224) showSummary() // Bottom quadrant
        layoutParams.circleAngle = newAngle.toFloat()
        imageView.layoutParams = layoutParams
    }

    private fun handleClick(angle: Int) {
        val clickAngle0to360 = normaliseAngle(90 - angle)
        val layoutParams = binding.dashboardMind.layoutParams as ConstraintLayout.LayoutParams
        val iconsAngle0to360 = normaliseAngle(layoutParams.circleAngle.toInt())
        val correctedAngle = normaliseAngle(clickAngle0to360 - iconsAngle0to360)
        when {
            (correctedAngle > (360-45) || correctedAngle < 45) -> callback?.onMindClicked()
            ((45) .. (90 + 45)).contains(correctedAngle) -> callback?.onFitnessClicked()
            ((180 - 45) .. (180 + 45)).contains(correctedAngle) -> callback?.onNutritionClicked()
            ((270 - 45) .. (270 + 45)).contains(correctedAngle) -> callback?.onVirtualWorldClicked()
            else -> Timber.e("Impossible state")
        }
    }

    private fun normaliseAngle(angle: Int) : Int {
        return (angle + 360).rem(360)
    }

    private inner class DragListener : OnTouchListener {

        private var startAngle: Double = 0.toDouble()
        private var shouldClick = true

        override fun onTouch(v: View, event: MotionEvent): Boolean {
            when (event.action) {

                MotionEvent.ACTION_DOWN -> {
                    shouldClick = true
                    startAngle = getAngle(event.x.toDouble(), event.y.toDouble())
                }

                MotionEvent.ACTION_MOVE -> {
                    val currentAngle = getAngle(event.x.toDouble(), event.y.toDouble())
                    rotateDialer((startAngle - currentAngle).toFloat())
                    startAngle = currentAngle
                    shouldClick = false
                    v.performClick() // Just here to avoid IDE warnings
                }

                MotionEvent.ACTION_UP -> {
                    if (shouldClick) {
                        val angle = getAngle(event.x.toDouble(), event.y.toDouble())
                        handleClick(angle.toInt())
                    }
                }
            }

            return true
        }

        private fun getAngle(xTouch: Double, yTouch: Double): Double {
            val x = xTouch - width / 2.0
            val y = height - yTouch - height / 2.0
            return when (getQuadrant(x, y)) {
                1 -> Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI
                2 -> 180 - Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI
                3 -> 180 + -1.0 * Math.asin(y / Math.hypot(x, y)) * 180.0 / Math.PI
                4 -> 360 + Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI
                else -> 0.0
            }
        }

        private fun getQuadrant(x: Double, y: Double): Int {
            return if (x >= 0) {
                if (y >= 0) 1 else 4
            } else {
                if (y >= 0) 2 else 3
            }
        }

    }

    interface RotaryListener {
        fun onMindClicked()
        fun onMindSelected()
        fun onFitnessClicked()
        fun onFitnessSelected()
        fun onNutritionClicked()
        fun onNutritionSelected()
        fun onVirtualWorldClicked()
        fun onVirtualWorldSelected()
    }

}

view_rotary.xml:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        >

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            >

        <ImageView
                android:id="@+id/dashboard_circle"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:src="@drawable/ic_dashboard_circle"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintWidth_percent="0.9"
                app:layout_constraintHeight_percent="0.9"
                tools:ignore="ContentDescription"
                />

        <ImageView
                android:id="@+id/dashboard_mind"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:src="@drawable/ic_dashboard_mind"
                app:layout_constraintCircle="@+id/dashboard_circle"
                app:layout_constraintCircleRadius="120dp"
                app:layout_constraintCircleAngle="0"
                app:layout_constraintWidth_percent="0.3"
                app:layout_constraintHeight_percent="0.3"
                android:contentDescription="@string/dash_board_mind_content_description"
                />

        <ImageView
                android:id="@+id/dashboard_virtual_world"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:src="@drawable/ic_dashboard_virtual_world"
                app:layout_constraintCircle="@+id/dashboard_circle"
                app:layout_constraintCircleRadius="120dp"
                app:layout_constraintCircleAngle="270"
                app:layout_constraintWidth_percent="0.3"
                app:layout_constraintHeight_percent="0.3"
                android:contentDescription="@string/dashboard_virtual_world_content_description"
                />

        <ImageView
                android:id="@+id/dashboard_fitness"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:src="@drawable/ic_dashboard_fitness"
                app:layout_constraintCircle="@+id/dashboard_circle"
                app:layout_constraintCircleRadius="120dp"
                app:layout_constraintCircleAngle="90"
                app:layout_constraintWidth_percent="0.3"
                app:layout_constraintHeight_percent="0.3"
                android:contentDescription="@string/dashboard_fitness_content_description"
                />

        <ImageView
                android:id="@+id/dashboard_nutrition"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:src="@drawable/ic_dashboard_nutrition"
                app:layout_constraintCircle="@+id/dashboard_circle"
                app:layout_constraintCircleRadius="120dp"
                app:layout_constraintCircleAngle="180"
                app:layout_constraintWidth_percent="0.3"
                app:layout_constraintHeight_percent="0.3"
                android:contentDescription="@string/dashboard_nutrition_content_description"
                />

        <View
                android:id="@+id/dragOverlay"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:clickable="true"
                android:focusable="true"
                app:layout_constraintStart_toStartOf="@id/dashboard_circle"
                app:layout_constraintEnd_toEndOf="@id/dashboard_circle"
                app:layout_constraintTop_toTopOf="@+id/dashboard_circle"
                app:layout_constraintBottom_toBottomOf="@id/dashboard_circle"
                />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>
...