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

问题描述 投票:-1回答:1

附加的图像是应用程序的用户界面的需求 - 包括的一组需要旋转,像一个老旋转电话图标。圆上的四个图标可以用手指来转动所有的图标(一起)和释放时,他们与图标定居最接近底部,点击进入该底部位置,它选择下面总结这部分的正文被拖进。即,UI不被拖动时,仅存在四个位置也可以是在(下午12点,下午3点,6点,下午9点上的时钟面)。

我以前没实现这样的拖动UI。如何将我最好的做呢?我应该尝试使用MotionLayout,或监控触摸事件,更改图标视图的旋转位置,然后就到了事件,动画旋转,以“点击”在底部最近的图标?

android android-animation
1个回答
0
投票

我记得ConstraintLayout 1.1版+具有取得的动画相当简单的圆形的位置约束。什么是不那么简单被处理的点击,因为我找不到任何办法让他们通过下面的拖动覆盖查看ImageViews,所以不得不计算已经点击了其中一个。对于任何人都希望实现这样的事情,这里的一些代码(注意:它采用了Android数据绑定)。 UI布局负责的屏幕尺寸并调整图标的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>
© www.soinside.com 2019 - 2024. All rights reserved.