自定义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()
        }
相关推荐
阿巴斯甜8 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker8 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95279 小时前
Andorid Google 登录接入文档
android
黄林晴10 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android