
最近在玩耍一个音乐播放器项目,顺带着将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切换的流畅效果 比自己写动画简单很多.