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切换的流畅效果 比自己写动画简单很多.

相关推荐
dalancon16 分钟前
VSYNC 信号流程分析 (Android 14)
android
dalancon25 分钟前
VSYNC 信号完整流程2
android
dalancon27 分钟前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户69371750013841 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android2 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才3 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶3 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle
汪海游龙4 小时前
开源项目 Trending AI 招募 Google Play 内测人员(12 名)
android·github
qq_283720054 小时前
MySQL技巧(四): EXPLAIN 关键参数详细解释
android·adb
没有了遇见5 小时前
Android 架构之网络框架多域名配置<三>
android