附件图像是应用程序UI的要求-由一组需要旋转的图标组成,就像旧的旋转电话一样。可以用手指拖动圆圈上的四个图标以旋转所有图标(一起),放开时,它们会以最靠近底部的图标沉降,单击到该底部位置,选中该图标,并在该文本下方总结该部分。即,当不拖动UI时,它只能处于四个位置(钟面上的12 pm、3pm、6pm、9pm)。
我以前没有实现过可拖动的UI。我最好怎么做?我应该尝试使用MotionLayout还是监视触摸事件,更改“视图”图标的旋转位置,然后在向上事件上,将旋转动画设置为“点击”,底部带有最近的图标?
答案 0 :(得分:0)
我记得ConstraintLayout v1.1 +具有圆形位置约束,这使动画非常简单。并不是那么简单的是处理单击,因为我找不到任何使它们传递到可拖动覆盖视图下方的ImageView的方法,因此必须计算单击了哪个单击。对于想要实现类似功能的其他人,这里有一些代码(请注意,它使用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>