[Now Playing] Replaced old lyrics with LrcView, this replaces Album Cover with LrcView when enabled

[Now Playing] Replaced old lyrics with LrcView, this replaces Album Cover with LrcView when enabled
This commit is contained in:
Prathamesh More 2021-12-15 15:05:45 +05:30
parent 9806e2119a
commit e4a309af66
7 changed files with 818 additions and 172 deletions

View file

@ -134,12 +134,11 @@ class UserInfoFragment : Fragment() {
private fun loadProfile() { private fun loadProfile() {
binding.bannerImage.let { binding.bannerImage.let {
GlideApp.with(this) GlideApp.with(this)
.asBitmap()
.load(RetroGlideExtension.getBannerModel()) .load(RetroGlideExtension.getBannerModel())
.profileBannerOptions(RetroGlideExtension.getBannerModel()) .profileBannerOptions(RetroGlideExtension.getBannerModel())
.into(it) .into(it)
} }
GlideApp.with(this).asBitmap() GlideApp.with(this)
.load(RetroGlideExtension.getUserModel()) .load(RetroGlideExtension.getUserModel())
.userProfileOptions(RetroGlideExtension.getUserModel()) .userProfileOptions(RetroGlideExtension.getUserModel())
.into(binding.userImage) .into(binding.userImage)

View file

@ -14,42 +14,36 @@
*/ */
package code.name.monkey.retromusic.fragments.player package code.name.monkey.retromusic.fragments.player
import android.annotation.SuppressLint
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils
import android.view.View import android.view.View
import android.widget.FrameLayout import androidx.core.view.isInvisible
import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.viewpager.widget.ViewPager import androidx.viewpager.widget.ViewPager
import code.name.monkey.appthemehelper.util.MaterialValueHelper
import code.name.monkey.retromusic.R import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.SHOW_LYRICS import code.name.monkey.retromusic.SHOW_LYRICS
import code.name.monkey.retromusic.adapter.album.AlbumCoverPagerAdapter import code.name.monkey.retromusic.adapter.album.AlbumCoverPagerAdapter
import code.name.monkey.retromusic.adapter.album.AlbumCoverPagerAdapter.AlbumCoverFragment import code.name.monkey.retromusic.adapter.album.AlbumCoverPagerAdapter.AlbumCoverFragment
import code.name.monkey.retromusic.databinding.FragmentPlayerAlbumCoverBinding import code.name.monkey.retromusic.databinding.FragmentPlayerAlbumCoverBinding
import code.name.monkey.retromusic.extensions.isColorLight
import code.name.monkey.retromusic.extensions.surfaceColor
import code.name.monkey.retromusic.fragments.NowPlayingScreen.* import code.name.monkey.retromusic.fragments.NowPlayingScreen.*
import code.name.monkey.retromusic.fragments.base.AbsMusicServiceFragment import code.name.monkey.retromusic.fragments.base.AbsMusicServiceFragment
import code.name.monkey.retromusic.fragments.base.AbsPlayerFragment import code.name.monkey.retromusic.fragments.base.AbsPlayerFragment
import code.name.monkey.retromusic.fragments.base.goToLyrics import code.name.monkey.retromusic.fragments.base.goToLyrics
import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.helper.MusicPlayerRemote
import code.name.monkey.retromusic.helper.MusicProgressViewUpdateHelper import code.name.monkey.retromusic.helper.MusicProgressViewUpdateHelper
import code.name.monkey.retromusic.model.lyrics.AbsSynchronizedLyrics import code.name.monkey.retromusic.lyrics.CoverLrcView
import code.name.monkey.retromusic.model.lyrics.Lyrics import code.name.monkey.retromusic.model.lyrics.Lyrics
import code.name.monkey.retromusic.transform.CarousalPagerTransformer import code.name.monkey.retromusic.transform.CarousalPagerTransformer
import code.name.monkey.retromusic.transform.ParallaxPagerTransformer import code.name.monkey.retromusic.transform.ParallaxPagerTransformer
import code.name.monkey.retromusic.util.LyricUtil import code.name.monkey.retromusic.util.LyricUtil
import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.PreferenceUtil
import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jaudiotagger.audio.AudioFileIO
import org.jaudiotagger.audio.exceptions.CannotReadException
import org.jaudiotagger.tag.FieldKey
import java.io.File
import java.io.FileNotFoundException
class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_player_album_cover), class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_player_album_cover),
ViewPager.OnPageChangeListener, MusicProgressViewUpdateHelper.Callback, ViewPager.OnPageChangeListener, MusicProgressViewUpdateHelper.Callback,
@ -70,9 +64,7 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe
} }
private var progressViewUpdateHelper: MusicProgressViewUpdateHelper? = null private var progressViewUpdateHelper: MusicProgressViewUpdateHelper? = null
private val lyricsLayout: FrameLayout get() = binding.playerLyrics private val lrcView: CoverLrcView get() = binding.lyricsView
private val lyricsLine1: TextView get() = binding.playerLyricsLine1
private val lyricsLine2: TextView get() = binding.playerLyricsLine2
var lyrics: Lyrics? = null var lyrics: Lyrics? = null
@ -82,102 +74,28 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe
} }
private fun updateLyrics() { private fun updateLyrics() {
lyrics = null binding.lyricsView.setLabel("Empty")
lifecycleScope.launch(Dispatchers.IO) {
val song = MusicPlayerRemote.currentSong val song = MusicPlayerRemote.currentSong
val lyrics = try { when {
var lrcFile: File? = null LyricUtil.isLrcOriginalFileExist(song.data) -> {
if (LyricUtil.isLrcOriginalFileExist(song.data)) { LyricUtil.getLocalLyricOriginalFile(song.data)
lrcFile = LyricUtil.getLocalLyricOriginalFile(song.data) ?.let { binding.lyricsView.loadLrc(it) }
} else if (LyricUtil.isLrcFileExist(song.title, song.artistName)) {
lrcFile = LyricUtil.getLocalLyricFile(song.title, song.artistName)
} }
val data: String = LyricUtil.getStringFromLrc(lrcFile) LyricUtil.isLrcFileExist(song.title, song.artistName) -> {
if (!TextUtils.isEmpty(data)) { LyricUtil.getLocalLyricFile(song.title, song.artistName)
Lyrics.parse(song, data) ?.let { binding.lyricsView.loadLrc(it) }
} else {
// Get Embedded Lyrics and check if they are Synchronized
val embeddedLyrics = try{
AudioFileIO.read(File(song.data)).tagOrCreateDefault.getFirst(FieldKey.LYRICS)
} catch(e: Exception){
null
} }
if (AbsSynchronizedLyrics.isSynchronized(embeddedLyrics)) { else -> {
Lyrics.parse(song, embeddedLyrics) binding.lyricsView.reset()
} else {
null
}
}
} catch (err: FileNotFoundException) {
null
} catch (e: CannotReadException){
null
}
withContext(Dispatchers.Main) {
this@PlayerAlbumCoverFragment.lyrics = lyrics
} }
} }
} }
override fun onUpdateProgressViews(progress: Int, total: Int) { override fun onUpdateProgressViews(progress: Int, total: Int) {
if (_binding == null) return binding.lyricsView.updateTime(progress.toLong())
if (!isLyricsLayoutVisible()) {
hideLyricsLayout()
return
}
if (lyrics !is AbsSynchronizedLyrics) return
val synchronizedLyrics = lyrics as AbsSynchronizedLyrics
lyricsLayout.visibility = View.VISIBLE
lyricsLayout.alpha = 1f
val oldLine = lyricsLine2.text.toString()
val line = synchronizedLyrics.getLine(progress)
if (oldLine != line || oldLine.isEmpty()) {
lyricsLine1.text = oldLine
lyricsLine2.text = line
lyricsLine1.visibility = View.VISIBLE
lyricsLine2.visibility = View.VISIBLE
lyricsLine2.measure(
View.MeasureSpec.makeMeasureSpec(
lyricsLine2.measuredWidth,
View.MeasureSpec.EXACTLY
),
View.MeasureSpec.UNSPECIFIED
)
val h: Float = lyricsLine2.measuredHeight.toFloat()
lyricsLine1.alpha = 1f
lyricsLine1.translationY = 0f
lyricsLine1.animate().alpha(0f).translationY(-h).duration =
AbsPlayerFragment.VISIBILITY_ANIM_DURATION
lyricsLine2.alpha = 0f
lyricsLine2.translationY = h
lyricsLine2.animate().alpha(1f).translationY(0f).duration =
AbsPlayerFragment.VISIBILITY_ANIM_DURATION
}
}
private fun isLyricsLayoutVisible(): Boolean {
return lyrics != null && lyrics!!.isSynchronized && lyrics!!.isValid
}
private fun hideLyricsLayout() {
lyricsLayout.animate().alpha(0f).setDuration(AbsPlayerFragment.VISIBILITY_ANIM_DURATION)
.withEndAction {
if (_binding == null) return@withEndAction
lyricsLayout.visibility = View.GONE
lyricsLine1.text = null
lyricsLine2.text = null
}
} }
@SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
_binding = FragmentPlayerAlbumCoverBinding.bind(view) _binding = FragmentPlayerAlbumCoverBinding.bind(view)
@ -210,14 +128,25 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe
progressViewUpdateHelper = MusicProgressViewUpdateHelper(this, 500, 1000) progressViewUpdateHelper = MusicProgressViewUpdateHelper(this, 500, 1000)
// Don't show lyrics container for below conditions // Don't show lyrics container for below conditions
if (!(nps == Circle || nps == Peak || nps == Tiny || !PreferenceUtil.showLyrics)) { if (!(nps == Circle || nps == Peak || nps == Tiny || !PreferenceUtil.showLyrics)) {
lyricsLayout.isVisible = false lrcView.isVisible = false
viewPager.isInvisible = false
progressViewUpdateHelper?.stop() progressViewUpdateHelper?.stop()
} else { } else {
lyricsLayout.isVisible = true lrcView.isVisible = true
viewPager.isInvisible = true
progressViewUpdateHelper?.start() progressViewUpdateHelper?.start()
} }
lrcView.apply {
setDraggable(true, object : CoverLrcView.OnPlayClickListener {
override fun onPlayClick(time: Long): Boolean {
MusicPlayerRemote.seekTo(time.toInt())
MusicPlayerRemote.resumePlaying()
return true
}
})
}
// Go to lyrics activity when clicked lyrics // Go to lyrics activity when clicked lyrics
binding.playerLyricsLine2.setOnClickListener { lrcView.setOnClickListener {
goToLyrics(requireActivity()) goToLyrics(requireActivity())
} }
} }
@ -227,10 +156,12 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe
val nps = PreferenceUtil.nowPlayingScreen val nps = PreferenceUtil.nowPlayingScreen
// Don't show lyrics container for below conditions // Don't show lyrics container for below conditions
if (nps == Circle || nps == Peak || nps == Tiny || !PreferenceUtil.showLyrics) { if (nps == Circle || nps == Peak || nps == Tiny || !PreferenceUtil.showLyrics) {
lyricsLayout.isVisible = false lrcView.isVisible = false
viewPager.isInvisible = false
progressViewUpdateHelper?.stop() progressViewUpdateHelper?.stop()
} else { } else {
lyricsLayout.isVisible = true lrcView.isVisible = true
viewPager.isInvisible = true
progressViewUpdateHelper?.start() progressViewUpdateHelper?.start()
} }
PreferenceManager.getDefaultSharedPreferences(requireContext()) PreferenceManager.getDefaultSharedPreferences(requireContext())
@ -266,27 +197,39 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe
val nps = PreferenceUtil.nowPlayingScreen val nps = PreferenceUtil.nowPlayingScreen
// Don't show lyrics container for below conditions // Don't show lyrics container for below conditions
if (!(nps == Circle || nps == Peak || nps == Tiny || !PreferenceUtil.showLyrics)) { if (!(nps == Circle || nps == Peak || nps == Tiny || !PreferenceUtil.showLyrics)) {
lyricsLayout.isVisible = false lrcView.isVisible = true
progressViewUpdateHelper?.stop() viewPager.isInvisible = true
} else {
lyricsLayout.isVisible = true
progressViewUpdateHelper?.start() progressViewUpdateHelper?.start()
lyricsLayout.animate().alpha(1f).duration = lrcView.animate().alpha(1f).duration =
AbsPlayerFragment.VISIBILITY_ANIM_DURATION AbsPlayerFragment.VISIBILITY_ANIM_DURATION
binding.playerLyrics.isVisible = true } else {
lrcView.isVisible = false
viewPager.isInvisible = false
progressViewUpdateHelper?.stop()
} }
} else { } else {
lrcView.isVisible = false
viewPager.isInvisible = false
progressViewUpdateHelper?.stop() progressViewUpdateHelper?.stop()
lyricsLayout.animate().alpha(0f)
.setDuration(AbsPlayerFragment.VISIBILITY_ANIM_DURATION)
.withEndAction {
if (_binding != null) {
binding.playerLyrics.isVisible = false
lyricsLine1.text = null
lyricsLine2.text = null
} }
} }
} }
private fun setLRCViewColors(backgroundColor: Int) {
val primaryColor = MaterialValueHelper.getPrimaryTextColor(
requireContext(),
backgroundColor.isColorLight
)
val secondaryColor = MaterialValueHelper.getSecondaryDisabledTextColor(
requireContext(),
backgroundColor.isColorLight
)
lrcView.apply {
setCurrentColor(primaryColor)
setTimeTextColor(primaryColor)
setTimelineColor(primaryColor)
setNormalColor(secondaryColor)
setTimelineTextColor(primaryColor)
} }
} }
@ -321,6 +264,18 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe
private fun notifyColorChange(color: MediaNotificationProcessor) { private fun notifyColorChange(color: MediaNotificationProcessor) {
callbacks?.onColorChanged(color) callbacks?.onColorChanged(color)
setLRCViewColors(
when (PreferenceUtil.nowPlayingScreen) {
Adaptive, Fit, Plain, Simple -> surfaceColor()
Flat, Normal -> if (PreferenceUtil.isAdaptiveColor) {
color.backgroundColor
} else {
surfaceColor()
}
Color ->color.backgroundColor
Blur -> Color.BLACK
else -> color.backgroundColor
})
} }
fun setCallbacks(listener: Callbacks) { fun setCallbacks(listener: Callbacks) {

View file

@ -47,7 +47,7 @@ class AdaptiveFragment : AbsPlayerFragment(R.layout.fragment_adaptive_player) {
_binding = FragmentAdaptivePlayerBinding.bind(view) _binding = FragmentAdaptivePlayerBinding.bind(view)
setUpSubFragments() setUpSubFragments()
setUpPlayerToolbar() setUpPlayerToolbar()
binding.root.drawAboveSystemBars() binding.playbackControlsFragment.drawAboveSystemBars()
} }
private fun setUpSubFragments() { private fun setUpSubFragments() {

View file

@ -0,0 +1,718 @@
/*
* Copyright (C) 2017 wangchenyan
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package code.name.monkey.retromusic.lyrics
import android.annotation.SuppressLint
import kotlin.jvm.JvmOverloads
import android.text.TextPaint
import android.graphics.drawable.Drawable
import android.animation.ValueAnimator
import android.view.GestureDetector
import android.widget.Scroller
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.MotionEvent
import code.name.monkey.retromusic.R
import android.text.TextUtils
import android.os.AsyncTask
import android.text.StaticLayout
import android.view.animation.LinearInterpolator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.os.Looper
import android.text.Layout
import android.text.format.DateUtils
import android.util.AttributeSet
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import code.name.monkey.retromusic.BuildConfig
import java.io.File
import java.lang.StringBuilder
import java.util.*
import kotlin.math.abs
/**
* 歌词 Created by wcy on 2015/11/9.
*/
@SuppressLint("StaticFieldLeak")
class CoverLrcView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val mLrcEntryList: MutableList<LrcEntry> = ArrayList()
private val mLrcPaint = TextPaint()
private val mTimePaint = TextPaint()
private var mTimeFontMetrics: Paint.FontMetrics? = null
private var mPlayDrawable: Drawable? = null
private var mDividerHeight = 0f
private var mAnimationDuration: Long = 0
private var mNormalTextColor = 0
private var mNormalTextSize = 0f
private var mCurrentTextColor = 0
private var mCurrentTextSize = 0f
private var mTimelineTextColor = 0
private var mTimelineColor = 0
private var mTimeTextColor = 0
private var mDrawableWidth = 0
private var mTimeTextWidth = 0
private var mDefaultLabel: String? = null
private var mLrcPadding = 0f
private var mOnPlayClickListener: OnPlayClickListener? = null
private var mAnimator: ValueAnimator? = null
private var mGestureDetector: GestureDetector? = null
private var mScroller: Scroller? = null
private var mOffset = 0f
private var mCurrentLine = 0
private var flag: Any? = null
private var isShowTimeline = false
private var isTouching = false
private var isFling = false
private var mTextGravity // 歌词显示位置,靠左/居中/靠右
= 0
private val hideTimelineRunnable = Runnable {
if (hasLrc() && isShowTimeline) {
isShowTimeline = false
smoothScrollTo(mCurrentLine)
}
}
/**
* 手势监听器
*/
private val mSimpleOnGestureListener: SimpleOnGestureListener =
object : SimpleOnGestureListener() {
override fun onDown(e: MotionEvent): Boolean {
if (mOffset != getOffset(0)) {
parent.requestDisallowInterceptTouchEvent(true)
}
if (hasLrc() && mOnPlayClickListener != null) {
mScroller!!.forceFinished(true)
removeCallbacks(hideTimelineRunnable)
isTouching = true
isShowTimeline = true
invalidate()
return true
}
return super.onDown(e)
}
override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (mOffset == getOffset(0) && distanceY < 0F) {
return super.onScroll(e1, e2, distanceX, distanceY)
}
if (hasLrc()) {
mOffset += -distanceY
mOffset = mOffset.coerceAtMost(getOffset(0))
mOffset = mOffset.coerceAtLeast(getOffset(mLrcEntryList.size - 1))
invalidate()
parent.requestDisallowInterceptTouchEvent(true)
return true
}
return super.onScroll(e1, e2, distanceX, distanceY)
}
override fun onFling(
e1: MotionEvent,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
if (hasLrc()) {
mScroller!!.fling(
0,
mOffset.toInt(),
0,
velocityY.toInt(),
0,
0,
getOffset(mLrcEntryList.size - 1).toInt(),
getOffset(0).toInt()
)
isFling = true
return true
}
return super.onFling(e1, e2, velocityX, velocityY)
}
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (hasLrc()
&& isShowTimeline
&& mPlayDrawable!!.bounds.contains(e.x.toInt(), e.y.toInt())
) {
val centerLine = centerLine
val centerLineTime = mLrcEntryList[centerLine].time
// onPlayClick 消费了才更新 UI
if (mOnPlayClickListener != null && mOnPlayClickListener!!.onPlayClick(
centerLineTime
)
) {
isShowTimeline = false
removeCallbacks(hideTimelineRunnable)
mCurrentLine = centerLine
invalidate()
return true
}
}
return super.onSingleTapConfirmed(e)
}
}
private fun init(attrs: AttributeSet?) {
val ta = context.obtainStyledAttributes(attrs, R.styleable.LrcView)
mCurrentTextSize = ta.getDimension(
R.styleable.LrcView_lrcTextSize, resources.getDimension(R.dimen.lrc_text_size)
)
mNormalTextSize = ta.getDimension(
R.styleable.LrcView_lrcNormalTextSize,
resources.getDimension(R.dimen.lrc_text_size)
)
if (mNormalTextSize == 0f) {
mNormalTextSize = mCurrentTextSize
}
mDividerHeight = ta.getDimension(
R.styleable.LrcView_lrcDividerHeight,
resources.getDimension(R.dimen.lrc_divider_height)
)
val defDuration = resources.getInteger(R.integer.lrc_animation_duration)
mAnimationDuration =
ta.getInt(R.styleable.LrcView_lrcAnimationDuration, defDuration).toLong()
mAnimationDuration =
if (mAnimationDuration < 0) defDuration.toLong() else mAnimationDuration
mNormalTextColor = ta.getColor(
R.styleable.LrcView_lrcNormalTextColor,
ContextCompat.getColor(context, R.color.lrc_normal_text_color)
)
mCurrentTextColor = ta.getColor(
R.styleable.LrcView_lrcCurrentTextColor,
ContextCompat.getColor(context, R.color.lrc_current_text_color)
)
mTimelineTextColor = ta.getColor(
R.styleable.LrcView_lrcTimelineTextColor,
ContextCompat.getColor(context, R.color.lrc_timeline_text_color)
)
mDefaultLabel = ta.getString(R.styleable.LrcView_lrcLabel)
mDefaultLabel =
if (TextUtils.isEmpty(mDefaultLabel)) context.getString(R.string.empty) else mDefaultLabel
mLrcPadding = ta.getDimension(R.styleable.LrcView_lrcPadding, 0f)
mTimelineColor = ta.getColor(
R.styleable.LrcView_lrcTimelineColor,
ContextCompat.getColor(context, R.color.lrc_timeline_color)
)
val timelineHeight = ta.getDimension(
R.styleable.LrcView_lrcTimelineHeight,
resources.getDimension(R.dimen.lrc_timeline_height)
)
mPlayDrawable = ta.getDrawable(R.styleable.LrcView_lrcPlayDrawable)
mPlayDrawable =
if (mPlayDrawable == null) ContextCompat.getDrawable(
context,
R.drawable.ic_play_arrow
) else mPlayDrawable
mTimeTextColor = ta.getColor(
R.styleable.LrcView_lrcTimeTextColor,
ContextCompat.getColor(context, R.color.lrc_time_text_color)
)
val timeTextSize = ta.getDimension(
R.styleable.LrcView_lrcTimeTextSize,
resources.getDimension(R.dimen.lrc_time_text_size)
)
mTextGravity = ta.getInteger(R.styleable.LrcView_lrcTextGravity, LrcEntry.GRAVITY_CENTER)
ta.recycle()
mDrawableWidth = resources.getDimension(R.dimen.lrc_drawable_width).toInt()
mTimeTextWidth = resources.getDimension(R.dimen.lrc_time_width).toInt()
mLrcPaint.isAntiAlias = true
mLrcPaint.textSize = mCurrentTextSize
mLrcPaint.textAlign = Paint.Align.LEFT
mTimePaint.isAntiAlias = true
mTimePaint.textSize = timeTextSize
mTimePaint.textAlign = Paint.Align.CENTER
mTimePaint.strokeWidth = timelineHeight
mTimePaint.strokeCap = Paint.Cap.ROUND
mTimeFontMetrics = mTimePaint.fontMetrics
mGestureDetector = GestureDetector(context, mSimpleOnGestureListener)
mGestureDetector!!.setIsLongpressEnabled(false)
mScroller = Scroller(context)
}
/** 设置非当前行歌词字体颜色 */
fun setNormalColor(normalColor: Int) {
mNormalTextColor = normalColor
postInvalidate()
}
/** 普通歌词文本字体大小 */
fun setNormalTextSize(size: Float) {
mNormalTextSize = size
}
/** 当前歌词文本字体大小 */
fun setCurrentTextSize(size: Float) {
mCurrentTextSize = size
}
/** 设置当前行歌词的字体颜色 */
fun setCurrentColor(currentColor: Int) {
mCurrentTextColor = currentColor
postInvalidate()
}
/** 设置拖动歌词时选中歌词的字体颜色 */
fun setTimelineTextColor(timelineTextColor: Int) {
mTimelineTextColor = timelineTextColor
postInvalidate()
}
/** 设置拖动歌词时时间线的颜色 */
fun setTimelineColor(timelineColor: Int) {
mTimelineColor = timelineColor
postInvalidate()
}
/** 设置拖动歌词时右侧时间字体颜色 */
fun setTimeTextColor(timeTextColor: Int) {
mTimeTextColor = timeTextColor
postInvalidate()
}
/**
* 设置歌词是否允许拖动
*
* @param draggable 是否允许拖动
* @param onPlayClickListener 设置歌词拖动后播放按钮点击监听器如果允许拖动则不能为 null
*/
fun setDraggable(draggable: Boolean, onPlayClickListener: OnPlayClickListener?) {
mOnPlayClickListener = if (draggable) {
requireNotNull(onPlayClickListener) { "if draggable == true, onPlayClickListener must not be null" }
onPlayClickListener
} else {
null
}
}
/**
* 设置播放按钮点击监听器
*
* @param onPlayClickListener 如果为非 null 则激活歌词拖动功能否则将将禁用歌词拖动功能
*/
@Deprecated("use {@link #setDraggable(boolean, OnPlayClickListener)} instead")
fun setOnPlayClickListener(onPlayClickListener: OnPlayClickListener?) {
mOnPlayClickListener = onPlayClickListener
}
/** 设置歌词为空时屏幕中央显示的文字,如“暂无歌词” */
fun setLabel(label: String?) {
runOnUi {
mDefaultLabel = label
invalidate()
}
}
/**
* 加载歌词文件
*
* @param lrcFile 歌词文件
*/
fun loadLrc(lrcFile: File) {
loadLrc(lrcFile, null)
}
/**
* 加载双语歌词文件两种语言的歌词时间戳需要一致
*
* @param mainLrcFile 第一种语言歌词文件
* @param secondLrcFile 第二种语言歌词文件
*/
fun loadLrc(mainLrcFile: File, secondLrcFile: File?) {
runOnUi {
reset()
val sb = StringBuilder("file://")
sb.append(mainLrcFile.path)
if (secondLrcFile != null) {
sb.append("#").append(secondLrcFile.path)
}
val flag = sb.toString()
this.flag = flag
object : AsyncTask<File?, Int?, List<LrcEntry>>() {
override fun doInBackground(vararg params: File?): List<LrcEntry>? {
return LrcUtils.parseLrc(params)
}
override fun onPostExecute(lrcEntries: List<LrcEntry>) {
if (flag === flag) {
onLrcLoaded(lrcEntries)
this@CoverLrcView.flag = null
}
}
}.execute(mainLrcFile, secondLrcFile)
}
}
/**
* 加载歌词文本
*
* @param lrcText 歌词文本
*/
fun loadLrc(lrcText: String?) {
loadLrc(lrcText, null)
}
/**
* 加载双语歌词文本两种语言的歌词时间戳需要一致
*
* @param mainLrcText 第一种语言歌词文本
* @param secondLrcText 第二种语言歌词文本
*/
fun loadLrc(mainLrcText: String?, secondLrcText: String?) {
runOnUi {
reset()
val sb = StringBuilder("file://")
sb.append(mainLrcText)
if (secondLrcText != null) {
sb.append("#").append(secondLrcText)
}
val flag = sb.toString()
this.flag = flag
object : AsyncTask<String?, Int?, List<LrcEntry>>() {
override fun doInBackground(vararg params: String?): List<LrcEntry>? {
return LrcUtils.parseLrc(params)
}
override fun onPostExecute(lrcEntries: List<LrcEntry>) {
if (flag === flag) {
onLrcLoaded(lrcEntries)
this@CoverLrcView.flag = null
}
}
}.execute(mainLrcText, secondLrcText)
}
}
/**
* 加载在线歌词
*
* @param lrcUrl 歌词文件的网络地址
* @param charset 编码格式
*/
/**
* 加载在线歌词默认使用 utf-8 编码
*
* @param lrcUrl 歌词文件的网络地址
*/
@JvmOverloads
fun loadLrcByUrl(lrcUrl: String, charset: String? = "utf-8") {
val flag = "url://$lrcUrl"
this.flag = flag
object : AsyncTask<String?, Int?, String>() {
override fun doInBackground(vararg params: String?): String? {
return LrcUtils.getContentFromNetwork(params[0], params[1])
}
override fun onPostExecute(lrcText: String) {
if (flag === flag) {
loadLrc(lrcText)
}
}
}.execute(lrcUrl, charset)
}
/**
* 歌词是否有效
*
* @return true如果歌词有效否则false
*/
fun hasLrc(): Boolean {
return mLrcEntryList.isNotEmpty()
}
/**
* 刷新歌词
*
* @param time 当前播放时间
*/
fun updateTime(time: Long) {
runOnUi {
if (!hasLrc()) {
return@runOnUi
}
val line = findShowLine(time)
if (line != mCurrentLine) {
mCurrentLine = line
if (!isShowTimeline) {
smoothScrollTo(line)
} else {
invalidate()
}
}
}
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
if (changed) {
initPlayDrawable()
initEntryList()
if (hasLrc()) {
smoothScrollTo(mCurrentLine, 0L)
}
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val centerY = height / 2
// 无歌词文件
if (!hasLrc()) {
mLrcPaint.color = mCurrentTextColor
@SuppressLint("DrawAllocation") val staticLayout = StaticLayout(
mDefaultLabel,
mLrcPaint,
lrcWidth.toInt(),
Layout.Alignment.ALIGN_CENTER,
1f,
0f,
false
)
drawText(canvas, staticLayout, centerY.toFloat())
return
}
val centerLine = centerLine
if (isShowTimeline) {
mPlayDrawable?.draw(canvas)
mTimePaint.color = mTimeTextColor
val timeText = LrcUtils.formatTime(mLrcEntryList[centerLine].time)
val timeX = (width - mTimeTextWidth / 2).toFloat()
val timeY = centerY - (mTimeFontMetrics!!.descent + mTimeFontMetrics!!.ascent) / 2
canvas.drawText(timeText, timeX, timeY, mTimePaint)
}
canvas.translate(0f, mOffset)
var y = 0f
for (i in mLrcEntryList.indices) {
if (i > 0) {
y += ((mLrcEntryList[i - 1].height + mLrcEntryList[i].height shr 1)
+ mDividerHeight)
}
if (BuildConfig.DEBUG) {
mLrcPaint.typeface = ResourcesCompat.getFont(context, R.font.sans)
}
if (i == mCurrentLine) {
mLrcPaint.textSize = mCurrentTextSize
mLrcPaint.color = mCurrentTextColor
} else if (isShowTimeline && i == centerLine) {
mLrcPaint.color = mTimelineTextColor
} else {
mLrcPaint.textSize = mNormalTextSize
mLrcPaint.color = mNormalTextColor
}
drawText(canvas, mLrcEntryList[i].staticLayout, y)
}
}
/**
* 画一行歌词
*
* @param y 歌词中心 Y 坐标
*/
private fun drawText(canvas: Canvas, staticLayout: StaticLayout, y: Float) {
canvas.save()
canvas.translate(mLrcPadding, y - (staticLayout.height shr 1))
staticLayout.draw(canvas)
canvas.restore()
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_UP
|| event.action == MotionEvent.ACTION_CANCEL
) {
isTouching = false
if (hasLrc() && !isFling) {
adjustCenter()
postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME)
}
}
return mGestureDetector!!.onTouchEvent(event)
}
override fun computeScroll() {
if (mScroller!!.computeScrollOffset()) {
mOffset = mScroller!!.currY.toFloat()
invalidate()
}
if (isFling && mScroller!!.isFinished) {
isFling = false
if (hasLrc() && !isTouching) {
adjustCenter()
postDelayed(hideTimelineRunnable, TIMELINE_KEEP_TIME)
}
}
}
override fun onDetachedFromWindow() {
removeCallbacks(hideTimelineRunnable)
super.onDetachedFromWindow()
}
private fun onLrcLoaded(entryList: List<LrcEntry>?) {
if (entryList != null && entryList.isNotEmpty()) {
mLrcEntryList.addAll(entryList)
}
mLrcEntryList.sort()
initEntryList()
invalidate()
}
private fun initPlayDrawable() {
val l = (mTimeTextWidth - mDrawableWidth) / 2
val t = height / 2 - mDrawableWidth / 2
val r = l + mDrawableWidth
val b = t + mDrawableWidth
mPlayDrawable!!.setBounds(l, t, r, b)
}
private fun initEntryList() {
if (!hasLrc() || width == 0) {
return
}
for (lrcEntry in mLrcEntryList) {
lrcEntry.init(mLrcPaint, lrcWidth.toInt(), mTextGravity)
}
mOffset = (height / 2).toFloat()
}
fun reset() {
endAnimation()
mScroller!!.forceFinished(true)
isShowTimeline = false
isTouching = false
isFling = false
removeCallbacks(hideTimelineRunnable)
mLrcEntryList.clear()
mOffset = 0f
mCurrentLine = 0
invalidate()
}
/** 将中心行微调至正中心 */
private fun adjustCenter() {
smoothScrollTo(centerLine, ADJUST_DURATION)
}
/** 滚动到某一行 */
/** 滚动到某一行 */
private fun smoothScrollTo(line: Int, duration: Long = mAnimationDuration) {
val offset = getOffset(line)
endAnimation()
mAnimator = ValueAnimator.ofFloat(mOffset, offset).apply {
this.duration = duration
interpolator = LinearInterpolator()
addUpdateListener { animation: ValueAnimator ->
mOffset = animation.animatedValue as Float
invalidate()
}
LrcUtils.resetDurationScale()
start()
}
}
/** 结束滚动动画 */
private fun endAnimation() {
if (mAnimator != null && mAnimator!!.isRunning) {
mAnimator!!.end()
}
}
/** 二分法查找当前时间应该显示的行数(最后一个 <= time 的行数) */
private fun findShowLine(time: Long): Int {
var left = 0
var right = mLrcEntryList.size
while (left <= right) {
val middle = (left + right) / 2
val middleTime = mLrcEntryList[middle].time
if (time < middleTime) {
right = middle - 1
} else {
if (middle + 1 >= mLrcEntryList.size || time < mLrcEntryList[middle + 1].time) {
return middle
}
left = middle + 1
}
}
return 0
}
/** 获取当前在视图中央的行数 */
private val centerLine: Int
get() {
var centerLine = 0
var minDistance = Float.MAX_VALUE
for (i in mLrcEntryList.indices) {
if (abs(mOffset - getOffset(i)) < minDistance) {
minDistance = abs(mOffset - getOffset(i))
centerLine = i
}
}
return centerLine
}
/** 获取歌词距离视图顶部的距离 采用懒加载方式 */
private fun getOffset(line: Int): Float {
if (mLrcEntryList[line].offset == Float.MIN_VALUE) {
var offset = (height / 2).toFloat()
for (i in 1..line) {
offset -= ((mLrcEntryList[i - 1].height + mLrcEntryList[i].height shr 1)
+ mDividerHeight)
}
mLrcEntryList[line].offset = offset
}
return mLrcEntryList[line].offset
}
/** 获取歌词宽度 */
private val lrcWidth: Float
get() = width - mLrcPadding * 2
/** 在主线程中运行 */
private fun runOnUi(r: Runnable) {
if (Looper.myLooper() == Looper.getMainLooper()) {
r.run()
} else {
post(r)
}
}
/** 播放按钮点击监听器,点击后应该跳转到指定播放位置 */
interface OnPlayClickListener {
/**
* 播放按钮被点击应该跳转到指定播放位置
*
* @return 是否成功消费该事件如果成功消费则会更新UI
*/
fun onPlayClick(time: Long): Boolean
}
companion object {
private const val ADJUST_DURATION: Long = 100
private const val TIMELINE_KEEP_TIME = 4 * DateUtils.SECOND_IN_MILLIS
}
init {
init(attrs)
}
}

View file

@ -153,7 +153,6 @@
android:layout_height="52dp" android:layout_height="52dp"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:background="?colorAccent" android:background="?colorAccent"
android:foreground="?attr/rectSelector"
android:padding="12dp" android:padding="12dp"
android:scaleType="fitCenter" android:scaleType="fitCenter"
tools:ignore="MissingPrefix" tools:ignore="MissingPrefix"

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout 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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -7,50 +8,24 @@
<androidx.viewpager.widget.ViewPager <androidx.viewpager.widget.ViewPager
android:id="@+id/viewPager" android:id="@+id/viewPager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:overScrollMode="@integer/overScrollMode" android:layout_height="match_parent"
android:layout_height="match_parent" /> android:overScrollMode="@integer/overScrollMode">
<FrameLayout </androidx.viewpager.widget.ViewPager>
android:id="@+id/playerLyrics"
<code.name.monkey.retromusic.lyrics.CoverLrcView
android:id="@+id/lyricsView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_gravity="center" android:layout_margin="16dp"
android:alpha="0"
android:clipToPadding="false"
android:elevation="20dp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
app:lrcLabel="@string/no_lyrics_found"
app:lrcNormalTextSize="28sp"
app:lrcPadding="24dp"
app:lrcTextGravity="center"
app:lrcTextSize="32sp"
app:lrcTimelineColor="@color/transparent"
tools:visibility="visible" />
<View
android:id="@+id/mask_lyrics"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/lyrics_mask" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/player_lyrics_line1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="16dp"
android:gravity="center"
android:shadowColor="@color/md_black_1000"
android:shadowRadius="4"
android:textAppearance="@style/TextViewHeadline5"
android:textColor="@color/md_white_1000"
android:visibility="gone" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/player_lyrics_line2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_margin="16dp"
android:gravity="center"
android:shadowColor="@color/md_black_1000"
android:shadowRadius="4"
android:textAppearance="@style/TextViewHeadline5"
android:textColor="@color/md_white_1000" />
</FrameLayout>
</FrameLayout> </FrameLayout>

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<integer name="lrc_animation_duration">1000</integer> <integer name="lrc_animation_duration">1000</integer>
<dimen name="lrc_text_size">16sp</dimen> <dimen name="lrc_text_size">20sp</dimen>
<dimen name="lrc_time_text_size">12sp</dimen> <dimen name="lrc_time_text_size">16sp</dimen>
<dimen name="lrc_divider_height">16dp</dimen> <dimen name="lrc_divider_height">16dp</dimen>
<dimen name="lrc_timeline_height">1dp</dimen> <dimen name="lrc_timeline_height">1dp</dimen>
<dimen name="lrc_drawable_width">30dp</dimen> <dimen name="lrc_drawable_width">30dp</dimen>