Я вспомнил, что 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>