如何实现旋转的可拖动图标UI?

时间:2019-01-31 16:13:49

标签: android android-animation

附件图像是应用程序UI的要求-由一组需要旋转的图标组成,就像旧的旋转电话一样。可以用手指拖动圆圈上的四个图标以旋转所有图标(一起),放开时,它们会以最靠近底部的图标沉降,单击到该底部位置,选中该图标,并在该文本下方总结该部分。即,当不拖动UI时,它只能处于四个位置(钟面上的12 pm、3pm、6pm、9pm)。

我以前没有实现过可拖动的UI。我最好怎么做?我应该尝试使用MotionLayout还是监视触摸事件,更改“视图”图标的旋转位置,然后在向上事件上,将旋转动画设置为“点击”,底部带有最近的图标?

1 个答案:

答案 0 :(得分:0)

我记得ConstraintLayout v1.1 +具有圆形位置约束,这使动画非常简单。并不是那么简单的是处理单击,因为我找不到任何使它们传递到可拖动覆盖视图下方的Im​​ageView的方法,因此必须计算单击了哪个单击。对于想要实现类似功能的其他人,这里有一些代码(请注意,它使用Android数据绑定)。 UI布局负责屏幕尺寸,并将图标的大小调整为View UI的百分比。

此代码不支持猛击,也不支持旋转拨盘“锁定”到特定位置,但是都可以添加在拖动结束后开始的动画。

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>