Android 扭蛋机抽奖

在这里插入图片描述


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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

魑魅魍魉9527

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值