
import android.animation.ValueAnimator
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.view.ViewGroup
import android.view.animation.LinearInterpolator
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.Toast
import androidx.core.animation.doOnEnd
import androidx.core.math.MathUtils
import com.sound.candy.riddle.box.stmh.R
import com.sound.candy.riddle.box.stmh.databinding.ActivityWhiteSongkBinding
import com.sound.candy.riddle.box.stmh.spf.SPFWhite
import com.sound.candy.riddle.box.stmh.utils.gone
import com.sound.candy.riddle.box.stmh.utils.hide
import com.sound.candy.riddle.box.stmh.utils.show
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.random.Random
class WhiteSongKActivity : WhiteBaseActivity<ActivityWhiteSongkBinding>() {
override fun provideViewBinding(): ActivityWhiteSongkBinding {
return ActivityWhiteSongkBinding.inflate(layoutInflater)
}
companion object {
@JvmStatic
fun start(context: Context, autoFinish: Boolean = false) {
val starter = Intent(context, WhiteSongKActivity::class.java)
context.startActivity(starter)
if (autoFinish && context is Activity) {
context.finish()
}
}
}
override fun initUI() {
binding.ivBack.setOnClickListener { finish() }
binding.ivStart.setOnClickListener {
binding.ivGasha.hide()
binding.layout.gone()
startAnimation()
}
}
override fun initData() {}
// 扭蛋资源列表
private val gashaList = listOf(
R.mipmap.gasha_01,
R.mipmap.gasha_02,
R.mipmap.gasha_03,
R.mipmap.gasha_04,
R.mipmap.gasha_05,
R.mipmap.gasha_06,
R.mipmap.gasha_07,
R.mipmap.gasha_08,
R.mipmap.gasha_09,
R.mipmap.gasha_10
)
fun startAnimation() {
binding.ivStart.isEnabled = false
val container = binding.container
val gashapons = mutableListOf<Gashapon>()
container.removeAllViews()
for (i in 0 until 10) {
val gashapon = Gashapon(this).apply {
setImageResource(gashaList[i])
layoutParams = FrameLayout.LayoutParams(120, 120).apply {
leftMargin = Random.nextInt(0, container.width - 120)
topMargin = Random.nextInt(0, container.height - 120)
}
xVelocity = Random.nextDouble(-10.0, 10.0)
yVelocity = Random.nextDouble(-10.0, 10.0)
rotationSpeed = Random.nextFloat() * 4f - 2f
rotation = Random.nextFloat() * 360 // 初始随机旋转角度
}
container.addView(gashapon)
gashapons.add(gashapon)
}
startGashaponAnimation(container, gashapons)
}
private fun startGashaponAnimation(container: FrameLayout, gashapons: List<Gashapon>) {
val animator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 8000
interpolator = LinearInterpolator()
repeatCount = ValueAnimator.INFINITE
addUpdateListener { animation ->
for (gashapon in gashapons) {
gashapon.x += gashapon.xVelocity.toFloat()
gashapon.y += gashapon.yVelocity.toFloat()
// 更新旋转角度
gashapon.rotation += gashapon.rotationSpeed
limitSpeed(gashapon)
checkCircularBoundaryCollision(gashapon, container)
checkGashaponCollisions(gashapons)
}
}
start()
}
Handler(Looper.getMainLooper()).postDelayed({
animator.cancel()
// 找到 Y 坐标最大的扭蛋(即最下方)
val selectedGashapon = gashapons.maxByOrNull { it.y } ?: return@postDelayed
explodeGashapon(selectedGashapon)
var prizeIndex = 0
for ((index, gashapon) in gashapons.withIndex()) {
if (gashapon === selectedGashapon) {
prizeIndex = index
break
}
}
binding.ivGasha.show()
binding.ivGasha.setImageResource(gashaList[prizeIndex])
SPFWhite.songK = gashaList[prizeIndex]
Toast.makeText(this, "恭喜抽中", Toast.LENGTH_SHORT).show()
binding.ivStart.isEnabled = true
}, 8000)
}
val maxSpeed = 8.0
fun limitSpeed(g: Gashapon) {
val speed = sqrt(g.xVelocity * g.xVelocity + g.yVelocity * g.yVelocity)
if (speed > maxSpeed) {
val scale = maxSpeed / speed
g.xVelocity *= scale
g.yVelocity *= scale
}
}
// 爆炸动画方法
private fun explodeGashapon(gashapon: Gashapon) {
val explosionAnimator = android.animation.ValueAnimator.ofFloat(1f, 3f).apply {
duration = 300
addUpdateListener { animation ->
val scale = animation.animatedValue as Float
gashapon.scaleX = scale
gashapon.scaleY = scale
gashapon.alpha = 1 - scale / 3 // 淡出
}
doOnEnd {
// 动画结束后移除扭蛋(可选)
(gashapon.parent as? ViewGroup)?.removeView(gashapon)
}
}
explosionAnimator.start()
}
// 检测与圆形边界的碰撞并反弹
private fun checkCircularBoundaryCollision(gashapon: Gashapon, container: FrameLayout) {
val centerX = container.width / 2f
val centerY = container.height / 2f
val radius = minOf(container.width, container.height) / 2f - 60 // 减去扭蛋的一半宽高
val dx = gashapon.x + 60 - centerX
val dy = gashapon.y + 60 - centerY
val distance = sqrt(dx * dx + dy * dy)
if (distance > radius) {
// 计算入射角度和反射角度
val angle = Math.atan2(dy.toDouble(), dx.toDouble())
val normalX = cos(angle)
val normalY = sin(angle)
// 计算速度投影到法线方向的分量
val dotProduct = gashapon.xVelocity * normalX + gashapon.yVelocity * normalY
// 反射速度公式
gashapon.xVelocity -= 2 * dotProduct * normalX
gashapon.yVelocity -= 2 * dotProduct * normalY
// 将扭蛋拉回边界内
gashapon.x = (centerX + radius * normalX - 60).toFloat()
gashapon.y = (centerY + radius * normalY - 60).toFloat()
}
}
private fun checkGashaponCollisions(gashapons: List<Gashapon>) {
val minVelocityThreshold = 0.5 // 最小速度阈值,防止低速抖动
for (i in gashapons.indices) {
for (j in i + 1 until gashapons.size) {
val g1 = gashapons[i]
val g2 = gashapons[j]
val dx = g1.x - g2.x
val dy = g1.y - g2.y
val distance = sqrt(dx * dx + dy * dy)
if (distance < 120) { // 假设直径为100
// 计算单位法线向量(从 g2 指向 g1)
val nx = dx / distance
val ny = dy / distance
// 计算两球中心之间的重叠距离
val overlap = 120 - distance
// 将两个扭蛋分开,各移动一半重叠距离
val separationX = nx * overlap / 2
val separationY = ny * overlap / 2
g1.x += separationX.toFloat()
g1.y += separationY.toFloat()
g2.x -= separationX.toFloat()
g2.y -= separationY.toFloat()
// 计算相对速度在法线方向上的投影
val dvx = g1.xVelocity - g2.xVelocity
val dvy = g1.yVelocity - g2.yVelocity
val dotProduct = dvx * nx + dvy * ny
// 如果两者正在接近,则进行反弹
if (dotProduct < 0) {
val restitution = 0.7 // 弹性系数(0~1)
// 应用冲量,交换速度(带弹性)
val impulse = -(1 + restitution) * dotProduct / 2
g1.xVelocity += impulse * nx
g1.yVelocity += impulse * ny
g2.xVelocity -= impulse * nx
g2.yVelocity -= impulse * ny
// 如果速度太小,冻结轻微震动
if (abs(g1.xVelocity) < minVelocityThreshold) g1.xVelocity = 0.0
if (abs(g1.yVelocity) < minVelocityThreshold) g1.yVelocity = 0.0
if (abs(g2.xVelocity) < minVelocityThreshold) g2.xVelocity = 0.0
if (abs(g2.yVelocity) < minVelocityThreshold) g2.yVelocity = 0.0
}
}
}
}
}
}
// 自定义扭蛋类,包含速度属性
class Gashapon(context: Context) : androidx.appcompat.widget.AppCompatImageView(context) {
var xVelocity: Double = 0.0
var yVelocity: Double = 0.0
var rotationSpeed: Float = Random.nextFloat() * 4f - 2f // 随机旋转速度(-2 ~ +2)
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFC5CA">
<View
android:layout_width="@dimen/dp_375"
android:layout_height="@dimen/dp_665"
android:background="@mipmap/bg_white"
app:layout_constraintBottom_toBottomOf="parent"
tools:ignore="MissingConstraints" />
<RelativeLayout
android:id="@+id/relativeLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_30"
android:gravity="center_vertical"
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/iv_back"
android:layout_width="@dimen/dp_50"
android:layout_height="@dimen/dp_50"
android:padding="@dimen/dp_13"
android:src="@drawable/icon_arrow_left" />
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="小助手"
android:textColor="@color/white"
android:textSize="@dimen/sp_22"
android:textStyle="bold" />
</RelativeLayout>
<TextView
android:layout_width="@dimen/dp_300"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="@dimen/dp_120"
android:background="@mipmap/tv_mhndj"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<RelativeLayout
android:id="@+id/rl_gasha"
android:layout_width="@dimen/dp_375"
android:layout_height="@dimen/dp_361"
android:layout_marginTop="@dimen/dp_220"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:id="@+id/layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerInParent="true"
android:gravity="center"
android:orientation="vertical">
<LinearLayout
android:id="@+id/layout1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_15">
<View
android:layout_width="@dimen/dp_60"
android:layout_height="@dimen/dp_60"
android:background="@mipmap/gasha_01" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_m_7">
<View
android:layout_width="@dimen/dp_60"
android:layout_height="@dimen/dp_60"
android:background="@mipmap/gasha_02" />
<View
android:layout_width="@dimen/dp_60"
android:layout_height="@dimen/dp_60"
android:background="@mipmap/gasha_03" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_m_7">
<View
android:layout_width="@dimen/dp_60"
android:layout_height="@dimen/dp_60"
android:background="@mipmap/gasha_03" />
<View
android:layout_width="@dimen/dp_60"
android:layout_height="@dimen/dp_60"
android:background="@mipmap/gasha_04" />
<View
android:layout_width="@dimen/dp_60"
android:layout_height="@dimen/dp_60"
android:background="@mipmap/gasha_05" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_m_7">
<View
android:layout_width="@dimen/dp_60"
android:layout_height="@dimen/dp_60"
android:background="@mipmap/gasha_06" />
<View
android:layout_width="@dimen/dp_60"
android:layout_height="@dimen/dp_60"
android:background="@mipmap/gasha_07" />
<View
android:layout_width="@dimen/dp_60"
android:layout_height="@dimen/dp_60"
android:background="@mipmap/gasha_08" />
<View
android:layout_width="@dimen/dp_60"
android:layout_height="@dimen/dp_60"
android:background="@mipmap/gasha_09" />
</LinearLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/container"
android:layout_width="@dimen/dp_300"
android:layout_height="@dimen/dp_300"
android:layout_alignParentTop="true"
android:layout_centerInParent="true"
android:clipChildren="true"
android:clipToPadding="true" />
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@mipmap/bg_ndj" />
<ImageView
android:id="@+id/iv_gasha"
android:layout_width="@dimen/dp_70"
android:layout_height="@dimen/dp_70"
android:layout_alignParentBottom="true"
android:layout_centerInParent="true"
android:layout_marginBottom="@dimen/dp_50" />
</RelativeLayout>
<ImageView
android:id="@+id/iv_start"
android:layout_width="@dimen/dp_214"
android:layout_height="@dimen/dp_74"
android:background="@mipmap/btn_kscj"
app:layout_constraintBottom_toBottomOf="@+id/rl_gasha"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/rl_gasha" />
</androidx.constraintlayout.widget.ConstraintLayout>