自定义View和动画学习记录 抓娃娃机View

抓娃娃

目录

[1. 首先准备几张钩子的相关图片](#1. 首先准备几张钩子的相关图片)

[2.自定义View 的布局文件 my_view_catch.xml](#2.自定义View 的布局文件 my_view_catch.xml)

3.自定义View的代码

4.具体使用


1. 首先准备几张钩子的相关图片

从前往后分别命名为:ic_tongs_top , ic_tongs_center , ic_tongs_open , ic_tongs_close ,ic_catch_coin

2.自定义View 的布局文件 my_view_catch.xml

XML 复制代码
<?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="500dp"
    tools:background="@color/white">

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/bubbleView0"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:tag="0"
        android:layout_marginBottom="20dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/bubbleView1"
        app:layout_constraintStart_toStartOf="parent"
        app:srcCompat="@drawable/ic_catch_coin"/>

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/bubbleView1"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:tag="1"
        android:layout_marginBottom="20dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/bubbleView2"
        app:layout_constraintStart_toEndOf="@+id/bubbleView0"
        app:srcCompat="@drawable/ic_catch_coin"/>
    
    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/bubbleView2"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:tag="2"
        android:layout_marginBottom="20dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/bubbleView1"
        app:srcCompat="@drawable/ic_catch_coin"/>

    <androidx.appcompat.widget.LinearLayoutCompat
        android:id="@+id/tongsView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.appcompat.widget.AppCompatImageView
            android:layout_width="92dp"
            android:layout_height="9dp"
            android:layout_gravity="center_horizontal"
            android:adjustViewBounds="true"
            app:srcCompat="@drawable/ic_tongs_top" />

        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/tongsCenterView"
            android:layout_width="8dp"
            android:layout_height="3dp"
            android:layout_gravity="center_horizontal"
            android:adjustViewBounds="true"
            android:scaleType="fitXY"
            app:srcCompat="@drawable/ic_tongs_center" />

        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/tongsBottomView"
            android:layout_width="92dp"
            android:layout_height="49dp"
            android:layout_gravity="center_horizontal"
            android:adjustViewBounds="true"
            android:scaleType="fitStart"
            app:srcCompat="@drawable/ic_tongs_close" />

    </androidx.appcompat.widget.LinearLayoutCompat>


</androidx.constraintlayout.widget.ConstraintLayout>

3.自定义View的代码

Kotlin 复制代码
package com.example.test.ui.widget

import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.Animation
import android.widget.FrameLayout
import android.widget.Toast
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.animation.doOnEnd
import androidx.core.animation.doOnStart
import androidx.core.view.doOnNextLayout
import androidx.core.view.updateLayoutParams


class MyCatchView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    private val binding = MyViewCatchBinding.inflate(LayoutInflater.from(context), this, true)

    private var horizontalAnimator: ValueAnimator? = null  //钩爪左右移动的动画
    private var horizontalMoveAnimator: ValueAnimator? = null //钩爪开始抓取时移动到指定位置的动画
    private var moveDownAnimator: ValueAnimator? = null //钩爪抓取时向下移动的动画
    private var moveUpAnimator: ValueAnimator? = null //钩爪抓取时向上返回的动画
    private var captureAnimatorSet: AnimatorSet? = null //抓取动画集合

    private var bubbleViewList: MutableList<AppCompatImageView> = mutableListOf()

    private lateinit var targetView: AppCompatImageView  //要抓取的目标视图

    private var viewWidth = 0f
    private var viewHeight = 0f

    private var onCapture = false

    init {
        initBubbleView()
    }

    private fun startHorizontalAnimator() {
        binding.tongsView.post {
            //在tongsView绘制完成之后才能获取宽高
            if (horizontalAnimator == null) {
                horizontalAnimator =
                    ValueAnimator.ofFloat(0f, viewWidth - binding.tongsView.width)
                        .apply {
                            duration = 3000
                            repeatCount = ValueAnimator.INFINITE
                            repeatMode = ValueAnimator.REVERSE
                            addUpdateListener {
                                binding.tongsView.translationX = it.animatedValue as Float
                            }
                        }
            }
            LogUtil.d("CatchView", "tongsX: ${viewWidth - binding.tongsView.width}")
            horizontalAnimator?.cancel()
            horizontalAnimator?.start()
        }
    }

    //开始抓取
    fun startCapture() {

        if (bubbleViewList.isEmpty()) {
            initBubbleView()
            Toast.makeText(context, "奖池重置!", Toast.LENGTH_SHORT).show()
            return
        }

        if (onCapture) return

        onCapture = true

        Toast.makeText(context, "$onCapture", Toast.LENGTH_SHORT).show()

        horizontalAnimator?.cancel()
        captureAnimatorSet?.cancel()

        targetView = bubbleViewList.random()

        val startY = binding.tongsBottomView.y
        val startX = binding.tongsView.x

        val targetX = targetView.x + (targetView.width - binding.tongsView.width) / 2
        val targetY = targetView.y + targetView.height / 2f - binding.tongsBottomView.height

        captureAnimatorSet = AnimatorSet().apply {
            // 设置动画序列
            playSequentially(
                getHorizontalMoveAnimator(startX, targetX),
                getMoveDownAnimator(startY, targetY),
                getMoveUpAnimator(targetY, startY),
                getHorizontalMoveAnimator(binding.tongsView.x, 0f, 1000)
            )
            // 动画结束回调
            doOnEnd {
                onCapture = false
                startHorizontalAnimator()
                Toast.makeText(context, "恭喜获得tag:${targetView.tag}", Toast.LENGTH_SHORT).show()
            }
        }

        captureAnimatorSet?.start()
    }

    //将钩爪移动到指定水平位置
    private fun getHorizontalMoveAnimator(
        startX: Float,
        targetX: Float,
        delay: Long = 0,
        endCallBack: () -> Unit = {}
    ): ValueAnimator {
        horizontalMoveAnimator = ValueAnimator.ofFloat(startX, targetX).apply {
            duration = 1000
            startDelay = delay
            interpolator = AccelerateDecelerateInterpolator()
            addUpdateListener {
                binding.tongsView.translationX = it.animatedValue as Float
            }
            doOnEnd { endCallBack.invoke() }
        }
        return horizontalMoveAnimator!!

    }

    //钩爪向下移动
    private fun getMoveDownAnimator(startY: Float, endY: Float): ValueAnimator {
        val startHeight = binding.tongsCenterView.height
        val openTongsY = startY + (endY - startY) / 4 * 3
        var setOpenSrc = false
        moveDownAnimator = ValueAnimator.ofFloat(startY, endY).apply {
            duration = 1000
            interpolator = AccelerateDecelerateInterpolator()
            addUpdateListener {
                //并不是钩爪向下移动,而是通过增加杆View的高度来把钩爪挤下去
                val animatedValue = it.animatedValue as Float
                binding.tongsCenterView.apply {
                    val params = layoutParams
                    params.height = startHeight + (animatedValue - startY).toInt()
                    layoutParams = params
                }
                if (!setOpenSrc && animatedValue >= openTongsY) {
                    binding.tongsBottomView.setImageResource(R.drawable.ic_tongs_open)
                    !setOpenSrc
                }
            }
        }
        return moveDownAnimator!!
    }

    //钩爪向上移动
    private fun getMoveUpAnimator(startY: Float, endY: Float): ValueAnimator {
        var startHeight = 0
        val targetViewStartY = targetView.y
        moveUpAnimator = ValueAnimator.ofFloat(startY, endY).apply {
            duration = 1000
            startDelay = 150
            interpolator = AccelerateDecelerateInterpolator()
            doOnStart {
                //必须在动画开始的时候再获取
                startHeight = binding.tongsCenterView.height
            }
            addUpdateListener {
                val animatedValue = animatedValue as Float
                val s = (animatedValue - startY).toInt()  //滑动距离
                binding.tongsCenterView.apply {
                    val params = layoutParams
                    params.height = startHeight + s
                    layoutParams = params
                }
                targetView.y = targetViewStartY + s
            }
            doOnEnd {
                binding.tongsBottomView.setImageResource(R.drawable.ic_tongs_close)
                targetView.visibility = View.INVISIBLE
                bubbleViewList.remove(targetView)
            }
        }
        return moveUpAnimator!!
    }

    private fun initBubbleView() {
        bubbleViewList.clear()
        bubbleViewList.apply {
            add(binding.bubbleView0)
            add(binding.bubbleView1)
            add(binding.bubbleView2)
        }
        bubbleViewList.forEach {
            it.visibility = View.VISIBLE
            it.y = viewHeight - it.height - dp2px(20f)
        }
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        viewWidth = w.toFloat()
        viewHeight = h.toFloat()
        startHorizontalAnimator()
    }

    override fun onDetachedFromWindow() {

        horizontalAnimator?.cancel()
        horizontalAnimator = null

        horizontalMoveAnimator?.cancel()
        horizontalMoveAnimator = null

        moveDownAnimator?.cancel()
        moveDownAnimator = null

        moveUpAnimator?.cancel()
        moveUpAnimator = null

        captureAnimatorSet?.cancel()
        captureAnimatorSet = null

        super.onDetachedFromWindow()
    }

}

4.具体使用

XML 复制代码
  <com.example.test.ui.widget.MyCatchView
        android:id="@+id/catchView"
        android:layout_width="match_parent"
        android:layout_height="500dp"/>
Kotlin 复制代码
        binding.catchView.setOnClickListener {
            binding.catchView.startCapture()
        }
相关推荐
bytebeats2 小时前
移动开发中特性开关的最佳实践
android·firebase
bytebeats3 小时前
Android 开发者的 Jetpack Compose 学习路线图
android·android jetpack
bytebeats3 小时前
Android 的未来: 为什么 `Navigation 3` 是行业变革者!
android·android jetpack
bytebeats3 小时前
如何在 Jetpack Compose 中创建逐字动画
android·android jetpack
Billy_Zuo3 小时前
Android调用python库和方法的实现
android·开发语言·python
烈焰晴天4 小时前
一款基于 ReactNative 最新发布的`Android/iOS` 新架构文档预览开源库
android·react native·ios
2501_915909064 小时前
iOS电池寿命与App能耗监测实战 构建完整性能监控系统
android·ios·小程序·https·uni-app·iphone·webview
_祝你今天愉快5 小时前
NDK-参数加密和签名校验
android
一起搞IT吧7 小时前
Camera相机人脸识别系列专题分析之十九:MTK ISP6S平台FDNode传递三方FFD到APP流程解析
android·图像处理·人工智能·数码相机·计算机视觉
wyjcxyyy7 小时前
打靶日记-RCE-labs
android