Android 音乐播放器之MotionLayout实现View流畅变换

最近在玩耍一个音乐播放器项目,顺带着将MD这几年的更新库耍一把,后续会陆续放出代码.

第一把刚上了MotionLayout虽然写的时候挺费劲,写完看着效果还行.

效果:

悬浮框-->矩形工具栏-->全屏展示

注意:

  • MotionLayout中嵌套两层会出现 apha和显示隐藏不生效问题
  • 角度控制需要代码控制

1: view_music_play.xml

ini 复制代码
<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="bottom"
    android:background="@color/transparent"
    app:layoutDescription="@xml/player_scene">
    <!-- 背景层 -->
    <com.google.android.material.imageview.ShapeableImageView
        android:id="@+id/cl_music_bg"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="@color/color_white"
        app:shapeAppearanceOverlay="@style/circleImageStyle" />
    <!-- 悬浮圆盘 -->
    <com.google.android.material.imageview.ShapeableImageView
        android:id="@+id/iv_cover"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="@mipmap/ic_launcher_c"
        android:clickable="false"
        android:scaleType="centerCrop"
        app:shapeAppearanceOverlay="@style/circleImageStyle" />

    <TextView
        android:id="@+id/tv_music"
        android:layout_width="0dp"
        android:gravity="start|center_vertical"
        android:layout_height="wrap_content"

        android:ellipsize="middle"
        android:lines="1"
        android:text="11111"
        android:textColor="@color/color_black"
        android:textSize="18sp"
        android:visibility="invisible" />


    <ImageButton
        android:id="@+id/iv_pre"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:src="@drawable/media3_icon_previous"
        android:visibility="invisible" />

    <ImageButton
        android:id="@+id/iv_rewind"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:src="@drawable/media3_icon_rewind"
        />


    <ImageButton
        android:id="@+id/iv_play"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:src="@drawable/media3_icon_play"
        />

    <ImageButton
        android:id="@+id/iv_forward"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:src="@drawable/media3_icon_fast_forward"
        />

    <ImageButton
        android:id="@+id/iv_next"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:src="@drawable/media3_icon_next"
        />

    <ImageButton
        android:id="@+id/iv_mode"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:src="@drawable/media3_icon_repeat_all"
        />

    <ImageButton
        android:id="@+id/iv_quality"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:src="@drawable/media3_icon_quality"
        />

    <ImageButton
        android:id="@+id/iv_more"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:src="@drawable/media3_icon_settings"
        />


</androidx.constraintlayout.motion.widget.MotionLayout>

2. player_scene.xml

ini 复制代码
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:motion="http://schemas.android.com/tools">

    <!-- 收起状态 -->
    <ConstraintSet android:id="@+id/cycle">

        <Constraint
            android:id="@id/cl_music_bg"
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:alpha="1"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

        <Constraint
            android:id="@id/iv_cover"
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:alpha="1"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toEndOf="@+id/cl_music_bg"
            app:layout_constraintStart_toStartOf="@+id/cl_music_bg"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />


        <Constraint
            android:id="@id/tv_music"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:alpha="0"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toStartOf="@id/iv_pre"
            app:layout_constraintStart_toEndOf="@id/iv_cover"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />

        <Constraint
            android:id="@id/iv_pre"
            android:alpha="0"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toStartOf="@+id/iv_rewind"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg"
            />

        <Constraint
            android:id="@id/iv_rewind"
            android:alpha="0"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toStartOf="@+id/iv_play"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />



        <Constraint
            android:id="@id/iv_play"
            android:alpha="0"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toStartOf="@+id/iv_forward"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />


        <Constraint
            android:id="@id/iv_forward"
            android:alpha="0"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toStartOf="@+id/iv_next"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />


        <Constraint
            android:id="@id/iv_next"
            android:alpha="0"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toStartOf="@+id/iv_mode"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />

        <Constraint
            android:id="@id/iv_mode"
            android:alpha="0"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toStartOf="@+id/iv_quality"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />
        <Constraint
            android:id="@id/iv_quality"
            android:alpha="0"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toStartOf="@+id/iv_more"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />


        <Constraint
            android:id="@id/iv_more"
            android:alpha="0"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toEndOf="@+id/cl_music_bg"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />



    </ConstraintSet>

    <!-- 展开状态 -->
    <ConstraintSet android:id="@+id/rounded">
        <Constraint
            android:id="@id/cl_music_bg"
            android:layout_width="0dp"
            android:layout_height="80dp"
            android:layout_marginStart="10dp"
            android:layout_marginEnd="10dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />

        <Constraint
            android:id="@id/iv_cover"
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:layout_marginStart="10dp"
            android:alpha="1"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintStart_toStartOf="@+id/cl_music_bg"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />

        <Constraint
            android:id="@id/tv_music"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="10dp"
            android:layout_marginEnd="10dp"
            android:alpha="1"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toStartOf="@id/iv_pre"
            app:layout_constraintStart_toEndOf="@id/iv_cover"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />
        <Constraint
            android:id="@id/iv_pre"
            android:alpha="0"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toStartOf="@+id/iv_rewind"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg"
            />

        <Constraint
            android:id="@id/iv_rewind"
            android:alpha="0"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toStartOf="@+id/iv_play"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />



        <Constraint
            android:id="@id/iv_play"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_marginEnd="10dp"

            android:alpha="1"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toStartOf="@+id/iv_forward"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />


        <Constraint
            android:id="@id/iv_forward"
            android:alpha="0"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toStartOf="@+id/iv_next"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />


        <Constraint
            android:id="@id/iv_next"
            android:alpha="0"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toStartOf="@+id/iv_mode"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />

        <Constraint
            android:id="@id/iv_mode"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_marginEnd="10dp"
            android:alpha="1"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toStartOf="@+id/iv_quality"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />
        <Constraint
            android:id="@id/iv_quality"
            android:alpha="0"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toStartOf="@+id/iv_more"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />


        <Constraint
            android:id="@id/iv_more"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_marginEnd="10dp"
            android:alpha="1"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toEndOf="@+id/cl_music_bg"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />



    </ConstraintSet>

<!--全屏状态-->
    <ConstraintSet android:id="@+id/full">
        <Constraint
            android:id="@id/cl_music_bg"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
        <Constraint
            android:id="@id/iv_cover"
            android:layout_width="180dp"
            android:layout_height="180dp"
            android:alpha="1"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toEndOf="@+id/cl_music_bg"
            app:layout_constraintStart_toStartOf="@+id/cl_music_bg"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />


        <Constraint
            android:id="@id/tv_music"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="10dp"
            android:layout_marginEnd="10dp"
            android:alpha="1"
            android:visibility="visible"
            android:layout_marginTop="40dp"
            app:layout_constraintStart_toStartOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toEndOf="@+id/cl_music_bg"
            app:layout_constraintTop_toTopOf="@+id/cl_music_bg" />

        <Constraint
            android:id="@id/iv_pre"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_marginStart="5dp"
            android:layout_marginEnd="5dp"
            android:alpha="1"
            android:visibility="visible"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintBottom_toBottomOf="@+id/cl_music_bg"
            app:layout_constraintEnd_toStartOf="@+id/iv_rewind"
            app:layout_constraintTop_toBottomOf="@+id/iv_cover"
            />

        <Constraint
            android:id="@id/iv_rewind"
            android:layout_marginStart="5dp"
            android:layout_marginEnd="5dp"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:alpha="1"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/iv_pre"
            app:layout_constraintEnd_toStartOf="@+id/iv_play"
            app:layout_constraintTop_toTopOf="@+id/iv_pre" />



        <Constraint
            android:id="@id/iv_play"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_marginStart="5dp"
            android:layout_marginEnd="5dp"

            android:alpha="1"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/iv_pre"
            app:layout_constraintEnd_toStartOf="@+id/iv_forward"
            app:layout_constraintTop_toTopOf="@+id/iv_pre" />


        <Constraint
            android:id="@id/iv_forward"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_marginStart="5dp"
            android:layout_marginEnd="5dp"
            android:alpha="1"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/iv_pre"
            app:layout_constraintEnd_toStartOf="@+id/iv_next"
            app:layout_constraintTop_toTopOf="@+id/iv_pre" />


        <Constraint
            android:id="@id/iv_next"
            android:layout_width="50dp"
            android:layout_marginStart="5dp"
            android:layout_marginEnd="5dp"
            android:layout_height="50dp"
            android:alpha="1"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/iv_pre"
            app:layout_constraintEnd_toStartOf="@+id/iv_mode"
            app:layout_constraintTop_toTopOf="@+id/iv_pre" />

        <Constraint
            android:id="@id/iv_mode"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_marginStart="5dp"
            android:layout_marginEnd="5dp"
            android:alpha="1"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/iv_pre"
            app:layout_constraintEnd_toStartOf="@+id/iv_quality"
            app:layout_constraintTop_toTopOf="@+id/iv_pre" />
        <Constraint
            android:id="@id/iv_quality"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:alpha="1"
            android:layout_marginStart="5dp"
            android:layout_marginEnd="5dp"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="@+id/iv_pre"
            app:layout_constraintEnd_toStartOf="@+id/iv_more"
            app:layout_constraintTop_toTopOf="@+id/iv_pre" />


        <Constraint
            android:id="@id/iv_more"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:alpha="0"
            android:visibility="visible"
            android:layout_marginEnd="0dp"

            app:layout_constraintBottom_toBottomOf="@+id/iv_pre"
            app:layout_constraintEnd_toEndOf="@+id/cl_music_bg"
            app:layout_constraintTop_toTopOf="@+id/iv_pre" />

    </ConstraintSet>

    <!-- 过渡动画 -->
    <Transition
        android:id="@+id/expandTransition"
        app:constraintSetEnd="@id/rounded"
        app:constraintSetStart="@id/cycle"
        app:duration="400">

        <KeyFrameSet>

        </KeyFrameSet>
    </Transition>

</MotionScene>

3.自定义ViewGroup

kotlin 复制代码
package com.wkq.main.ui.view

import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.animation.LinearInterpolator
import android.widget.FrameLayout
import androidx.constraintlayout.motion.widget.MotionLayout
import com.wkq.main.R
import com.wkq.main.databinding.ViewMusicPlayBinding
import com.wkq.main.util.RotationHelper


/**
 *
 *@Author: wkq
 *
 *@Time: 2025/12/19 14:34
 *
 *@Desc: 播放器 播放控制器
 */
class MusicPlayView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    private val binding: ViewMusicPlayBinding
    private val motionLayout: MotionLayout


    init {
        binding = ViewMusicPlayBinding.inflate(LayoutInflater.from(context), this, true)
        motionLayout = binding.motionLayout
        initView()
    }

    private fun startDiscRotation() {
        ObjectAnimator.ofFloat(binding.ivCover, "rotation", 0f, 360f).apply {
            duration = 5000
            repeatCount = ValueAnimator.INFINITE
            interpolator = LinearInterpolator()
            start()
        }
    }

    var isExpanded = false
    var isFullScreen = false
    private lateinit var rotationHelper: RotationHelper

    private fun initView() {
        // 初始化工具类
        rotationHelper = RotationHelper(binding.ivCover)
        rotationHelper.start()
        binding.ivCover.setOnClickListener {
            if (isExpanded) {
                // 展开 -> 收起
                binding.motionLayout.transitionToState(R.id.cycle)
            } else {
                // 收起 -> 展开
                binding.motionLayout.transitionToState(R.id.rounded)
            }

        }


        binding.ivMore.setOnClickListener {
            if (!isFullScreen) {
                // rounded -> full
                binding.motionLayout.transitionToState(R.id.full)
            } else {
                // full -> rounded
                binding.motionLayout.transitionToState(R.id.rounded)
            }
        }
        binding.motionLayout.setTransitionListener(object : MotionLayout.TransitionListener {
            override fun onTransitionStarted(
                motionLayout: MotionLayout, startId: Int, endId: Int
            ) {
            }

            override fun onTransitionChange(
                motionLayout: MotionLayout, startId: Int, endId: Int, progress: Float
            ) {
                val width = binding.clMusicBg.width.toFloat()
                val height = binding.clMusicBg.height.toFloat()
                val minSide = minOf(width, height)

                // 定义每个状态对应的 cornerRadius
                val startRadius = when (startId) {
                    R.id.cycle -> minSide / 2f        // 完全圆形
                    R.id.rounded -> 16f               // 圆角矩形
                    R.id.full -> 0f                    // 方形
                    else -> 0f
                }

                val endRadius = when (endId) {
                    R.id.cycle -> minSide / 2f
                    R.id.rounded -> 16f
                    R.id.full -> 0f
                    else -> 0f
                }

                val radius = startRadius + (endRadius - startRadius) * progress

                binding.clMusicBg.shapeAppearanceModel =
                    binding.clMusicBg.shapeAppearanceModel.toBuilder().setAllCornerSizes(radius)
                        .build()
            }

            override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) {
                isExpanded = currentId == R.id.rounded
                isFullScreen = currentId == R.id.full

            }

            override fun onTransitionTrigger(
                motionLayout: MotionLayout, triggerId: Int, positive: Boolean, progress: Float
            ) {
            }
        })

    }
}

总结

简单实现了这个结果,核心是MotionLayout 搭配过度动画实现 View切换的流畅效果 比自己写动画简单很多.

相关推荐
TheNextByte12 小时前
在 PC 和Android之间同步音乐的 4 种方法
android
君莫啸ོ3 小时前
Android基础-Activity属性 android:configChanges
android
TimeFine3 小时前
Android AI解放生产力(七):更丰富的AI运用前瞻
android
保持低旋律节奏3 小时前
linux——进程状态
android·linux·php
明川3 小时前
Android Gradle - ASM + AsmClassVisitorFactory插桩使用
android·前端·gradle
csdn12259873364 小时前
Android将应用添加到默认打开方式
android
百锦再4 小时前
京东云鼎入驻方案解读——通往协同的“高架桥”与“快速路”
android·java·python·rust·django·restful·京东云
成都大菠萝5 小时前
1-2-3 Kotlin与C++基础-JNI原理与使用
android
TimeFine5 小时前
Android AI解放生产力(六)实战:解放页面开发前的繁琐工作
android·架构