[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:
parent
9806e2119a
commit
e4a309af66
7 changed files with 818 additions and 172 deletions
|
@ -134,12 +134,11 @@ class UserInfoFragment : Fragment() {
|
|||
private fun loadProfile() {
|
||||
binding.bannerImage.let {
|
||||
GlideApp.with(this)
|
||||
.asBitmap()
|
||||
.load(RetroGlideExtension.getBannerModel())
|
||||
.profileBannerOptions(RetroGlideExtension.getBannerModel())
|
||||
.into(it)
|
||||
}
|
||||
GlideApp.with(this).asBitmap()
|
||||
GlideApp.with(this)
|
||||
.load(RetroGlideExtension.getUserModel())
|
||||
.userProfileOptions(RetroGlideExtension.getUserModel())
|
||||
.into(binding.userImage)
|
||||
|
|
|
@ -14,42 +14,36 @@
|
|||
*/
|
||||
package code.name.monkey.retromusic.fragments.player
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
import code.name.monkey.appthemehelper.util.MaterialValueHelper
|
||||
import code.name.monkey.retromusic.R
|
||||
import code.name.monkey.retromusic.SHOW_LYRICS
|
||||
import code.name.monkey.retromusic.adapter.album.AlbumCoverPagerAdapter
|
||||
import code.name.monkey.retromusic.adapter.album.AlbumCoverPagerAdapter.AlbumCoverFragment
|
||||
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.base.AbsMusicServiceFragment
|
||||
import code.name.monkey.retromusic.fragments.base.AbsPlayerFragment
|
||||
import code.name.monkey.retromusic.fragments.base.goToLyrics
|
||||
import code.name.monkey.retromusic.helper.MusicPlayerRemote
|
||||
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.transform.CarousalPagerTransformer
|
||||
import code.name.monkey.retromusic.transform.ParallaxPagerTransformer
|
||||
import code.name.monkey.retromusic.util.LyricUtil
|
||||
import code.name.monkey.retromusic.util.PreferenceUtil
|
||||
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),
|
||||
ViewPager.OnPageChangeListener, MusicProgressViewUpdateHelper.Callback,
|
||||
|
@ -70,9 +64,7 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe
|
|||
}
|
||||
private var progressViewUpdateHelper: MusicProgressViewUpdateHelper? = null
|
||||
|
||||
private val lyricsLayout: FrameLayout get() = binding.playerLyrics
|
||||
private val lyricsLine1: TextView get() = binding.playerLyricsLine1
|
||||
private val lyricsLine2: TextView get() = binding.playerLyricsLine2
|
||||
private val lrcView: CoverLrcView get() = binding.lyricsView
|
||||
|
||||
var lyrics: Lyrics? = null
|
||||
|
||||
|
@ -82,102 +74,28 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe
|
|||
}
|
||||
|
||||
private fun updateLyrics() {
|
||||
lyrics = null
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
binding.lyricsView.setLabel("Empty")
|
||||
val song = MusicPlayerRemote.currentSong
|
||||
val lyrics = try {
|
||||
var lrcFile: File? = null
|
||||
if (LyricUtil.isLrcOriginalFileExist(song.data)) {
|
||||
lrcFile = LyricUtil.getLocalLyricOriginalFile(song.data)
|
||||
} else if (LyricUtil.isLrcFileExist(song.title, song.artistName)) {
|
||||
lrcFile = LyricUtil.getLocalLyricFile(song.title, song.artistName)
|
||||
when {
|
||||
LyricUtil.isLrcOriginalFileExist(song.data) -> {
|
||||
LyricUtil.getLocalLyricOriginalFile(song.data)
|
||||
?.let { binding.lyricsView.loadLrc(it) }
|
||||
}
|
||||
val data: String = LyricUtil.getStringFromLrc(lrcFile)
|
||||
if (!TextUtils.isEmpty(data)) {
|
||||
Lyrics.parse(song, data)
|
||||
} 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
|
||||
LyricUtil.isLrcFileExist(song.title, song.artistName) -> {
|
||||
LyricUtil.getLocalLyricFile(song.title, song.artistName)
|
||||
?.let { binding.lyricsView.loadLrc(it) }
|
||||
}
|
||||
if (AbsSynchronizedLyrics.isSynchronized(embeddedLyrics)) {
|
||||
Lyrics.parse(song, embeddedLyrics)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (err: FileNotFoundException) {
|
||||
null
|
||||
} catch (e: CannotReadException){
|
||||
null
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
this@PlayerAlbumCoverFragment.lyrics = lyrics
|
||||
else -> {
|
||||
binding.lyricsView.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUpdateProgressViews(progress: Int, total: Int) {
|
||||
if (_binding == null) return
|
||||
|
||||
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
|
||||
}
|
||||
binding.lyricsView.updateTime(progress.toLong())
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
_binding = FragmentPlayerAlbumCoverBinding.bind(view)
|
||||
|
@ -210,14 +128,25 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe
|
|||
progressViewUpdateHelper = MusicProgressViewUpdateHelper(this, 500, 1000)
|
||||
// Don't show lyrics container for below conditions
|
||||
if (!(nps == Circle || nps == Peak || nps == Tiny || !PreferenceUtil.showLyrics)) {
|
||||
lyricsLayout.isVisible = false
|
||||
lrcView.isVisible = false
|
||||
viewPager.isInvisible = false
|
||||
progressViewUpdateHelper?.stop()
|
||||
} else {
|
||||
lyricsLayout.isVisible = true
|
||||
lrcView.isVisible = true
|
||||
viewPager.isInvisible = true
|
||||
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
|
||||
binding.playerLyricsLine2.setOnClickListener {
|
||||
lrcView.setOnClickListener {
|
||||
goToLyrics(requireActivity())
|
||||
}
|
||||
}
|
||||
|
@ -227,10 +156,12 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe
|
|||
val nps = PreferenceUtil.nowPlayingScreen
|
||||
// Don't show lyrics container for below conditions
|
||||
if (nps == Circle || nps == Peak || nps == Tiny || !PreferenceUtil.showLyrics) {
|
||||
lyricsLayout.isVisible = false
|
||||
lrcView.isVisible = false
|
||||
viewPager.isInvisible = false
|
||||
progressViewUpdateHelper?.stop()
|
||||
} else {
|
||||
lyricsLayout.isVisible = true
|
||||
lrcView.isVisible = true
|
||||
viewPager.isInvisible = true
|
||||
progressViewUpdateHelper?.start()
|
||||
}
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
|
@ -266,27 +197,39 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe
|
|||
val nps = PreferenceUtil.nowPlayingScreen
|
||||
// Don't show lyrics container for below conditions
|
||||
if (!(nps == Circle || nps == Peak || nps == Tiny || !PreferenceUtil.showLyrics)) {
|
||||
lyricsLayout.isVisible = false
|
||||
progressViewUpdateHelper?.stop()
|
||||
} else {
|
||||
lyricsLayout.isVisible = true
|
||||
lrcView.isVisible = true
|
||||
viewPager.isInvisible = true
|
||||
progressViewUpdateHelper?.start()
|
||||
lyricsLayout.animate().alpha(1f).duration =
|
||||
lrcView.animate().alpha(1f).duration =
|
||||
AbsPlayerFragment.VISIBILITY_ANIM_DURATION
|
||||
binding.playerLyrics.isVisible = true
|
||||
} else {
|
||||
lrcView.isVisible = false
|
||||
viewPager.isInvisible = false
|
||||
progressViewUpdateHelper?.stop()
|
||||
}
|
||||
} else {
|
||||
lrcView.isVisible = false
|
||||
viewPager.isInvisible = false
|
||||
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) {
|
||||
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) {
|
||||
|
|
|
@ -47,7 +47,7 @@ class AdaptiveFragment : AbsPlayerFragment(R.layout.fragment_adaptive_player) {
|
|||
_binding = FragmentAdaptivePlayerBinding.bind(view)
|
||||
setUpSubFragments()
|
||||
setUpPlayerToolbar()
|
||||
binding.root.drawAboveSystemBars()
|
||||
binding.playbackControlsFragment.drawAboveSystemBars()
|
||||
}
|
||||
|
||||
private fun setUpSubFragments() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -153,7 +153,6 @@
|
|||
android:layout_height="52dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="?colorAccent"
|
||||
android:foreground="?attr/rectSelector"
|
||||
android:padding="12dp"
|
||||
android:scaleType="fitCenter"
|
||||
tools:ignore="MissingPrefix"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
@ -7,50 +8,24 @@
|
|||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/viewPager"
|
||||
android:layout_width="match_parent"
|
||||
android:overScrollMode="@integer/overScrollMode"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="match_parent"
|
||||
android:overScrollMode="@integer/overScrollMode">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/playerLyrics"
|
||||
</androidx.viewpager.widget.ViewPager>
|
||||
|
||||
<code.name.monkey.retromusic.lyrics.CoverLrcView
|
||||
android:id="@+id/lyricsView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:alpha="0"
|
||||
android:clipToPadding="false"
|
||||
android:elevation="20dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="16dp"
|
||||
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>
|
|
@ -1,8 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="lrc_animation_duration">1000</integer>
|
||||
<dimen name="lrc_text_size">16sp</dimen>
|
||||
<dimen name="lrc_time_text_size">12sp</dimen>
|
||||
<dimen name="lrc_text_size">20sp</dimen>
|
||||
<dimen name="lrc_time_text_size">16sp</dimen>
|
||||
<dimen name="lrc_divider_height">16dp</dimen>
|
||||
<dimen name="lrc_timeline_height">1dp</dimen>
|
||||
<dimen name="lrc_drawable_width">30dp</dimen>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue