当前位置: 首页 > news >正文

自定义View学习记录 plinko游戏View

plinko

1.创建自定义View属性

在res的value中的attrs.xml中添加如下属性

    <declare-styleable name="MyNailView"><attr name="nailBackground" format="reference|color"/>  //钉子的背景<attr name="ballIcon" format="reference|color"/>  //小球的icon<attr name="row" format="integer"/>  //行数<attr name="column" format="integer"/>  //列数</declare-styleable>

2.创建自定义View

package com.example.test.ui.widgetimport android.animation.ValueAnimator
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.animation.LinearInterpolator
import android.widget.FrameLayout
import kotlin.math.min
import kotlin.math.sqrt// 数据类:钉子(圆心的x坐标,圆心的y坐标,行数,列数)
data class MyNailBean(val x: Float, val y: Float, val row: Int = -1, val column: Int = -1)class MyNailView @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyle: Int = 0
) : FrameLayout(context, attrs, defStyle) {private var dropAnim: ValueAnimator? = nullprivate var viewWidth = dp2px(343f)  //控件宽度private var viewHeight = dp2px(500f) //控件高度//钉子相关private var nailRow = 6 //钉子行数private var nailColumn = 5 //钉子列数private val nailRadius = dp2px(10f) //钉子半径private var nailDistance = viewWidth / nailColumn.toFloat()  //钉子间距private val nailBeanList = mutableListOf<MyNailBean>()private var nailBmp: Bitmap? = null //钉子图标private val nailPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {color = Color.YELLOWstyle = Paint.Style.FILL}//球相关private var ballRadius = dp2px(10f).toFloat() //默认球半径private var ballX = 0f //球圆心X坐标private var ballY = 0f //球圆心Y坐标private var ballVx = 0.1f // 初始水平速度private var ballVy = 10f // 初始垂直速度private val gravity = 1.0f // 重力加速度 (调整)private val damping = 0.6f // 碰撞阻尼 (调整)private val minStopSpeed = 1f // 速度停止阈值 (调整)private var ballBmp: Bitmap? = nullprivate val ballPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {color = Color.BLUEstyle = Paint.Style.FILL}init {ballRadius = if (ballBmp != null) ballBmp!!.width / 2f  else ballRadiusballX = viewWidth / 2fballY = ballRadiusnailDistance = viewWidth / nailColumn.toFloat()// 加载自定义属性val typedArray = context.obtainStyledAttributes(attrs,R.styleable.MyNailView,defStyle,0)try {val nailResId = typedArray.getResourceId(R.styleable.MyNailView_nailBackground, -1)if (nailResId != -1) {// 情况1:是资源引用(@drawable/xxx 或 @mipmap/xxx)nailBmp = when (resources.getResourceTypeName(nailResId)) {"drawable", "mipmap" -> BitmapFactory.decodeResource(resources, nailResId)else -> null}} else {// 情况2:可能是颜色值或未设置val color = typedArray.getColor(R.styleable.MyNailView_nailBackground,Color.YELLOW // 默认颜色)nailPaint.color = color}val ballResId = typedArray.getResourceId(R.styleable.MyNailView_ballIcon, -1)if (ballResId != -1) {ballBmp = when (resources.getResourceTypeName(ballResId)) {"drawable", "mipmap" -> BitmapFactory.decodeResource(resources, ballResId)else -> null}} else {// 情况2:可能是颜色值或未设置val color = typedArray.getColor(R.styleable.MyNailView_ballIcon,Color.BLUE // 默认颜色)ballPaint.color = color}// 获取行数和列数nailRow = typedArray.getInt(R.styleable.MyNailView_row, 6)nailColumn = typedArray.getInt(R.styleable.MyNailView_column, 7)} finally {typedArray.recycle()}}private fun updateBallInfo() {//应用重力ballVy += gravity//更新小球速度ballX += ballVxballY += ballVy//检测小球是否触底val bottomLine = viewHeight - ballRadius * 2 - 20fif (ballY >= bottomLine) {// 1. 计算当前速度大小val speed = sqrt(ballVx * ballVx + ballVy * ballVy)// 2. 如果速度小于阈值,则停止if (speed < minStopSpeed) {ballY = bottomLinedropAnim?.cancel()return}// 3. 应用阻尼ballVx *= dampingballVy = damping(-ballVy)}//检测小球是否与钉子碰撞for (nail in nailBeanList) {val dx = ballX - nail.xval dy = ballY - nail.yval distance = sqrt(dx * dx + dy * dy)if (distance <= nailRadius + ballRadius) {// 1. 计算碰撞法线方向(从钉子指向小球)val nx = dx / distanceval ny = dy / distance// 2. 计算入射速度在法线方向上的投影val dotProduct = ballVx * nx + ballVy * ny// 3. 计算反弹后的速度ballVx -= 2 * dotProduct * nxballVy -= 2 * dotProduct * ny// 4.应用阻尼ballVx *= dampingballVy *= damping// 5.避免小球陷入钉子内部ballX = nail.x + (ballRadius + nailRadius) * nxballY = nail.y + (ballRadius + nailRadius) * ny}}//检测小球与两侧墙壁的碰撞if (ballX <= ballRadius) {ballVx = damping(-ballVx)ballX = ballRadius}if (ballX >= width - ballRadius) {ballVx = damping(-ballVx)ballX = width - ballRadius}LogUtil.d("Nail", "位置:$ballX,$ballY")}fun startAnimator(clickX: Float) {initBallInfo(clickX)dropAnim?.cancel()if (dropAnim == null) {dropAnim = ValueAnimator.ofFloat(0f, 1f).apply {duration = 5000repeatCount = 1interpolator = LinearInterpolator()addUpdateListener {updateBallInfo()invalidate()}}}dropAnim?.start()}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)// 绘制钉子nailBeanList.forEach { nail ->if (nailBmp != null) {canvas.drawBitmap(nailBmp!!,nail.x - nailBmp!!.width / 2,nail.y - nailBmp!!.height / 2,null)} else {canvas.drawCircle(nail.x, nail.y, nailRadius.toFloat(), nailPaint)}}// 绘制球if (ballBmp != null) {canvas.drawBitmap(ballBmp!!, ballX - ballRadius, ballY - ballRadius, null)} else {canvas.drawCircle(ballX, ballY, ballRadius, ballPaint)}}private fun createNailList() {nailBeanList.clear()val offsetX = nailDistance / 2 //奇数行 钉子距离两侧增加的偏移距离var offsetY = nailDistancefor (i in 0 until nailRow) {for (j in 0 until nailColumn) {val x =if (i % 2 == 0 && j == nailColumn - 1) continueelse if (i % 2 == 0) (j + 1) * nailDistanceelse offsetX + j * nailDistancenailBeanList.add(MyNailBean(x, offsetY, i, j))}offsetY += nailDistance}}private fun initBallInfo(inputBallX: Float = viewWidth / 2f) {ballX = setInputBallX(inputBallX)ballY = ballRadius + 10fballVx = 0fballVy = 10fballRadius = if (ballBmp != null) ballBmp!!.width / 2f  else ballRadius}private fun damping(num: Float): Float {return num * damping}override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)viewWidth = wviewHeight = hnailDistance = viewWidth / nailColumn.toFloat()createNailList()invalidate()}private fun setInputBallX(inputBallX: Float): Float {return when {inputBallX < nailDistance -> nailDistance + 1finputBallX > viewWidth - nailDistance -> viewWidth - nailDistance - 1felse -> inputBallX}}override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {// 默认宽度(如果未指定具体值)val defaultWidth = dp2px(343f).toInt()// 计算期望高度(基于钉子行数和球半径)val defaultHeight = calculateWrapContentHeight()// 处理宽度测量val width = when (MeasureSpec.getMode(widthMeasureSpec)) {MeasureSpec.EXACTLY -> MeasureSpec.getSize(widthMeasureSpec) // 精确值MeasureSpec.AT_MOST -> min(defaultWidth, MeasureSpec.getSize(widthMeasureSpec)) // 最大不超过else -> defaultWidth // wrap_content 或其他情况}// 处理高度测量val height = when (MeasureSpec.getMode(heightMeasureSpec)) {MeasureSpec.EXACTLY -> MeasureSpec.getSize(heightMeasureSpec) // 精确值MeasureSpec.AT_MOST -> min(defaultHeight,MeasureSpec.getSize(heightMeasureSpec)) // 最大不超过else -> defaultHeight // wrap_content}setMeasuredDimension(width, height)// 更新视图尺寸相关变量viewWidth = widthviewHeight = heightnailDistance = viewWidth / nailColumn.toFloat()createNailList()}/** 计算 wrap_content 时的合适高度 */private fun calculateWrapContentHeight(): Int {// 钉子区域高度 = (行数 + 1) * 钉子间距(+1 给顶部留空间)val nailsHeight = nailDistance * (nailRow + 1)// 球运动区域高度 = 球半径 * 4(保证下落和弹跳空间)val ballMovementHeight = ballRadius * 4return (nailsHeight + ballMovementHeight).toInt()}override fun performClick(): Boolean {super.performClick()return true}override fun onDetachedFromWindow() {dropAnim?.let {it.cancel()          // 停止动画it.removeAllListeners() // 移除所有监听器it.removeAllUpdateListeners()}dropAnim = nullif (ballBmp?.isRecycled == false) {ballBmp?.recycle()  // 回收位图内存}ballBmp = null// 清空数据集合nailBeanList.clear()super.onDetachedFromWindow()}
}

3.使用

<com.example.test.ui.widget.MyNailViewandroid:id="@+id/nailView"android:layout_margin="20dp"android:layout_width="match_parent"android:layout_height="wrap_content"android:background="@color/white"/>
        binding.nailView.setOnTouchListener { v, motionEvent ->if (motionEvent.action == MotionEvent.ACTION_DOWN) {binding.nailView.startAnimator(motionEvent.x)}v.performClick()}

http://www.dtcms.com/a/303330.html

相关文章:

  • 恒坤新材IPO被暂缓审议:收入确认法遭质疑,募资缩水约2亿元
  • 元宇宙经济与数字经济的异同:虚实交织下的经济范式对比
  • 基于Springboot的宠物救助管理系统的设计与实现
  • 【VUE3】搭建项目准备工作
  • 艾格文服装软件怎么用?
  • Windows中查看GPU和Cuda信息的DOS命令总结
  • AI产品经理手册(Ch1-2)AI Product Manager‘s Handbook学习笔记
  • uvm sequence Arbitration
  • AI 驱动、设施扩展、验证器强化、上线 EVM 测试网,Injective 近期动态全更新!
  • git stash apply 冲突合并方法解决
  • 希尔排序(缩小增量排序)面试专题解析
  • unisS5800XP-G交换机配置命令之登录篇
  • 洛谷 P10448 组合型枚举-普及-
  • Visual Studio Code使用
  • 25世界职业院校技能大赛国内赛区承办名单确定,各赛区需全力筹备
  • 【Spring Boot 快速入门】二、请求与响应
  • CGA围手术期:全周期保障老年手术安全
  • 基于深度学习的医学图像分析:使用YOLOv5实现细胞检测
  • TI 2025全国电赛猜题
  • 刘润探展科大讯飞WAIC,讯飞医疗AI该咋看?
  • 【重学数据结构】二叉搜索树 Binary Search Tree
  • LINUX 728 SHELL:grep;sort;diff
  • MOE 速览
  • python入门篇12-虚拟环境conda的安装与使用
  • 拷贝漫画网页入口 - Copymanga漫画官方网站及APP下载
  • 接⼝测试⾯试题汇总
  • YOLO目标检测总结
  • EXCEL怎么使用数据透视表批量生成工作表
  • 【深度学习】深入理解交叉熵损失函数 (Cross-Entropy Loss Function)
  • Lambda表达式Stream流-函数式编程-java8函数式编程(Lambda表达式,Optional,Stream流)从入门到精通-最通俗易懂