修复BUG,更新UI

This commit is contained in:
muqing 2024-02-21 18:24:34 +08:00
parent 98a4bd87ad
commit 073fd37da7
10 changed files with 1824 additions and 1 deletions

View File

@ -19,7 +19,7 @@ android {
//noinspection ExpiredTargetSdkVersion,OldTargetApi
targetSdk 31
versionCode 1
versionName "2.0.0"
versionName "2.1.0"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8

View File

@ -0,0 +1,257 @@
package com.dirror.lyricviewx
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.text.Layout
import androidx.annotation.ColorInt
import androidx.annotation.FloatRange
import androidx.annotation.Px
import java.io.File
const val GRAVITY_CENTER = 0 // 居中
const val GRAVITY_LEFT = 1 // 左
const val GRAVITY_RIGHT = 2 // 右
fun Int.toLayoutAlign(): Layout.Alignment {
return when (this) {
GRAVITY_LEFT -> Layout.Alignment.ALIGN_NORMAL
GRAVITY_CENTER -> Layout.Alignment.ALIGN_CENTER
GRAVITY_RIGHT -> Layout.Alignment.ALIGN_OPPOSITE
else -> Layout.Alignment.ALIGN_CENTER
}
}
/**
* LyricViewX 接口
* LyricViewX 提取方便管理
*
* @author Moriafly
* @since 2021年1月28日16:29:16
*/
interface LyricViewXInterface {
/**
* 设置整句之间的间隔高度
* @param height px
*/
fun setSentenceDividerHeight(@Px height: Float)
/**
* 设置原句与翻译之间的间隔高度
* @param height px
*/
fun setTranslateDividerHeight(@Px height: Float)
/**
* 设置歌词整体的垂直偏移值配合[setHorizontalOffsetPercent]使用
* @param offset px
*
* @see [setHorizontalOffsetPercent]
*/
fun setHorizontalOffset(@Px offset: Float)
/**
* 设置歌词整体的垂直偏移相对于控件高度的百分比,0.5f即表示居中配合[setHorizontalOffset]使用
*
* @param percent 0.0f ~ 1.0f
*
* @see [setHorizontalOffset]
*/
fun setHorizontalOffsetPercent(@FloatRange(from = 0.0, to = 1.0) percent: Float)
/**
* 设置翻译相对与原词之间的缩放比例值
* @param scaleValue 一般来说 0.8f 是个不错的值
*/
fun setTranslateTextScaleValue(@FloatRange(from = 0.1, to = 2.0) scaleValue: Float)
/**
* 设置文字的对齐方向
*/
fun setTextGravity(gravity: Int)
/**
* 设置非当前行歌词字体颜色 [normalColor]
*/
fun setNormalColor(@ColorInt normalColor: Int)
/**
* 普通歌词文本字体大小 [size]单位 px
*/
fun setNormalTextSize(@Px size: Float)
/**
* 当前歌词文本字体大小
*/
fun setCurrentTextSize(size: Float)
/**
* 设置当前行歌词的字体颜色
*/
fun setCurrentColor(currentColor: Int)
/**
* 设置拖动歌词时选中歌词的字体颜色
*/
fun setTimelineTextColor(timelineTextColor: Int)
/**
* 设置拖动歌词时时间线的颜色
*/
fun setTimelineColor(timelineColor: Int)
/**
* 设置拖动歌词时右侧时间字体颜色
*/
fun setTimeTextColor(timeTextColor: Int)
/**
* 设置歌词为空时屏幕中央显示的文字 [label]暂无歌词
*/
fun setLabel(label: String)
/**
* 加载歌词文本
* 两种语言的歌词时间戳需要一致
*
* @param mainLyricText 第一种语言歌词文本
* @param secondLyricText 可选第二种语言歌词文本
*/
fun loadLyric(mainLyricText: String?, secondLyricText: String? = null)
/**
* 加载歌词 [LyricEntry] 集合
* 如果你在 Service 等地方自行解析歌词包装成 [LyricEntry] 集合那么可以使用此方法载入歌词
*
* @param lyricEntries 歌词集合
* @since 1.3.1
*/
fun loadLyric(lyricEntries: List<LyricEntry>)
/**
* 刷新歌词
*
* @param time 当前播放时间
*/
fun updateTime(time: Long, force: Boolean = false)
/**
* 设置歌词是否允许拖动
*
* @param draggable 是否允许拖动
* @param onPlayClickListener 设置歌词拖动后播放按钮点击监听器如果允许拖动则不能为 null
*/
fun setDraggable(draggable: Boolean, onPlayClickListener: OnPlayClickListener?)
/**
* 设置单击
*/
fun setOnSingerClickListener(onSingerClickListener: OnSingleClickListener?)
/**
* 获取当前歌词每句实体可用于歌词分享
*
* @return LyricEntry 集合
*/
fun getLyricEntryList(): List<LyricEntry>
/**
* 设置当前歌词每句实体
*/
fun setLyricEntryList(newList: List<LyricEntry>)
/**
* 获取当前行歌词
*/
fun getCurrentLineLyricEntry(): LyricEntry?
/**
* 为歌词设置自定义的字体
*
* @param file 字体文件
*/
fun setLyricTypeface(file: File)
/**
* 为歌词设置自定义的字体
*
* @param path 字体文件路径
*/
fun setLyricTypeface(path: String)
/**
* 为歌词设置自定义的字体可为空若为空则应清除字体
*
* @param typeface 字体对象
*/
fun setLyricTypeface(typeface: Typeface?)
/**
* 为歌词的过渡动画设置阻尼比数值越大回弹次数越多
*
* @param dampingRatio 阻尼比 详见[androidx.dynamicanimation.animation.SpringForce]
*/
fun setDampingRatioForLyric(dampingRatio: Float)
/**
* 为歌词视图的滚动动画设置阻尼比数值越大回弹次数越多
*
* @param dampingRatio 阻尼比 详见[androidx.dynamicanimation.animation.SpringForce]
*/
fun setDampingRatioForViewPort(dampingRatio: Float)
/**
* 为歌词的过渡动画设置刚度数值越大动画越短
*
* @param stiffness 刚度 详见[androidx.dynamicanimation.animation.SpringForce]
*/
fun setStiffnessForLyric(stiffness: Float)
/**
* 为歌词视图的滚动动画设置刚度数值越大动画越短
*
* @param stiffness 刚度 详见[androidx.dynamicanimation.animation.SpringForce]
*/
fun setStiffnessForViewPort(stiffness: Float)
/**
* 设置跳转播放按钮
*/
fun setPlayDrawable(drawable: Drawable)
/**
* 设置是否绘制歌词翻译
*/
fun setIsDrawTranslation(isDrawTranslation: Boolean)
/**
* 是否开启特定的模糊效果
*/
fun setIsEnableBlurEffect(isEnableBlurEffect: Boolean)
/**
* 设置元素的偏移百分比0.5f即表示居中
*
* @param itemOffsetPercent 0f ~ 1f 偏移百分比
*/
fun setItemOffsetPercent(@FloatRange(from = 0.0, to = 1.0) itemOffsetPercent: Float)
}
/**
* 播放按钮点击监听器点击后应该跳转到指定播放位置
*/
interface OnPlayClickListener {
/**
* 播放按钮被点击应该跳转到指定播放位置
*
* @return 是否成功消费该事件如果成功消费则会更新UI
*/
fun onPlayClick(time: Long): Boolean
}
/**
* 点击歌词布局
*/
interface OnSingleClickListener {
fun onClick()
}

View File

@ -0,0 +1,82 @@
package com.dirror.lyricviewx
import android.os.Build
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
/**
* 一行歌词实体
* @since 2021年1月19日09:51:40 Moriafly 基于 LrcEntry 改造转换为 kt 移除部分过时方法
* @param time 歌词时间
* @param text 歌词文本
*/
class LyricEntry(@JvmField val time: Long, @JvmField val text: String) : Comparable<LyricEntry> {
/**
* 第二文本
*/
@JvmField
var secondText: String? = null
/**
* staticLayout
*/
var staticLayout: StaticLayout? = null
private set
var secondStaticLayout: StaticLayout? = null
private set
@Deprecated("存在不显示翻译的情况会导致offset发生改变故不再固定存储offset")
/**
* 歌词距离视图顶部的距离
*/
var offset = Float.MIN_VALUE
/**
* 初始化
* @param textPaint 文本画笔
* @param width 宽度
* @param align 位置
*/
fun init(
textPaint: TextPaint,
secondTextPaint: TextPaint,
width: Int, align: Layout.Alignment
) {
staticLayout = createStaticLayout(text, textPaint, width, align)
secondStaticLayout = createStaticLayout(secondText, secondTextPaint, width, align)
offset = Float.MIN_VALUE
}
/**
* 继承 Comparable 比较
* @param other LyricEntry
* @return 时间差
*/
override fun compareTo(other: LyricEntry): Int {
return (time - other.time).toInt()
}
companion object {
fun createStaticLayout(
text: String?,
paint: TextPaint,
width: Number,
align: Layout.Alignment
): StaticLayout? {
if (text.isNullOrEmpty()) return null
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
StaticLayout.Builder
.obtain(text, 0, text.length, paint, width.toInt())
.setAlignment(align)
.setLineSpacing(0f, 1f)
.setIncludePad(false)
.build()
} else {
StaticLayout(text, paint, width.toInt(), align, 1f, 0f, false)
}
}
}
}

View File

@ -0,0 +1,289 @@
package com.dirror.lyricviewx
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.graphics.Rect
import android.text.TextUtils
import android.text.format.DateUtils
import android.view.MotionEvent
import java.io.*
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets
import java.util.*
import java.util.regex.Pattern
/**
* 工具类
* LrcUtils Kotlin
*/
object LyricUtil {
private val PATTERN_LINE = Pattern.compile("((\\[\\d\\d:\\d\\d\\.\\d{2,3}])+)(.+)")
private val PATTERN_TIME = Pattern.compile("\\[(\\d\\d):(\\d\\d)\\.(\\d{2,3})]")
private val argbEvaluator = ArgbEvaluator()
/**
* 从文件解析双语歌词
*/
fun parseLrc(lrcFiles: Array<out File?>?): List<LyricEntry>? {
if (lrcFiles == null || lrcFiles.size != 2 || lrcFiles[0] == null) {
return null
}
val mainLrcFile = lrcFiles[0]
val secondLrcFile = lrcFiles[1]
val mainEntryList = parseLrc(mainLrcFile)
val secondEntryList = parseLrc(secondLrcFile)
if (mainEntryList != null && secondEntryList != null) {
for (mainEntry in mainEntryList) {
for (secondEntry in secondEntryList) {
if (mainEntry.time == secondEntry.time) {
mainEntry.secondText = secondEntry.text
}
}
}
}
return mainEntryList
}
/**
* 从文件解析歌词
*/
private fun parseLrc(lrcFile: File?): List<LyricEntry>? {
if (lrcFile == null || !lrcFile.exists()) {
return null
}
val entryList: MutableList<LyricEntry> = ArrayList()
try {
val br =
BufferedReader(InputStreamReader(FileInputStream(lrcFile), StandardCharsets.UTF_8))
var line: String
while (br.readLine().also { line = it } != null) {
val list = parseLine(line)
if (list != null && list.isNotEmpty()) {
entryList.addAll(list)
}
}
br.close()
} catch (e: IOException) {
e.printStackTrace()
}
entryList.sort()
return entryList
}
/**
* 从文本解析双语歌词
*/
fun parseLrc(lrcTexts: Array<out String?>?): List<LyricEntry>? {
if (lrcTexts == null || lrcTexts.size != 2 || TextUtils.isEmpty(lrcTexts[0])) {
return null
}
val mainLrcText = lrcTexts[0]
val secondLrcText = lrcTexts[1]
val mainEntryList = mainLrcText?.let { parseLrc(it) }
/**
* 当输入的secondLrcText为空时,按如下格式解析歌词
* 音乐标签下载的第二种歌词格式
*
* [00:21.11]いつも待ち合わせより15分前集合
* [00:21.11]总会比相约时间早15分钟集合
* [00:28.32]駅の改札ぬける
* [00:28.32]穿过车站的检票口
* [00:31.39]ざわめきにわくわくだね
* [00:31.39]嘈杂声令内心兴奋不已
* [00:35.23]どこへ向かうかなんて
* [00:35.23]不在意接下来要去哪里
*/
if (TextUtils.isEmpty(secondLrcText)) {
var lastEntry: LyricEntry? = null
return mainEntryList?.filter { now ->
if (lastEntry == null) {
lastEntry = now
return@filter true
}
if (lastEntry!!.time == now.time) {
lastEntry!!.secondText = now.text
lastEntry = null
return@filter false
}
lastEntry = now
true
}
}
val secondEntryList = secondLrcText?.let { parseLrc(it) }
if (mainEntryList != null && secondEntryList != null) {
for (mainEntry in mainEntryList) {
for (secondEntry in secondEntryList) {
if (mainEntry.time == secondEntry.time) {
mainEntry.secondText = secondEntry.text
}
}
}
}
return mainEntryList
}
/**
* 从文本解析歌词
*/
private fun parseLrc(lrcText: String): List<LyricEntry>? {
var lyricText = lrcText.trim()
if (TextUtils.isEmpty(lyricText)) return null
if (lyricText.startsWith("\uFEFF")) {
lyricText = lyricText.replace("\uFEFF", "")
}
// 针对传入 Language="Media Monkey Format"; Lyrics="......"; 的情况
lyricText = lyricText.substringAfter("Lyrics=\"")
.substringBeforeLast("\";")
val entryList: MutableList<LyricEntry> = ArrayList()
val array = lyricText.split("\\n".toRegex()).toTypedArray()
for (line in array) {
val list = parseLine(line)
if (!list.isNullOrEmpty()) {
entryList.addAll(list)
}
}
entryList.sort()
return entryList
}
/**
* 获取网络文本需要在工作线程中执行
*/
fun getContentFromNetwork(url: String?, charset: String?): String? {
var lrcText: String? = null
try {
val url = URL(url)
val conn = url.openConnection() as HttpURLConnection
conn.requestMethod = "GET"
conn.connectTimeout = 10000
conn.readTimeout = 10000
if (conn.responseCode == 200) {
val `is` = conn.inputStream
val bos = ByteArrayOutputStream()
val buffer = ByteArray(1024)
var len: Int
while (`is`.read(buffer).also { len = it } != -1) {
bos.write(buffer, 0, len)
}
`is`.close()
bos.close()
lrcText = bos.toString(charset)
}
} catch (e: Exception) {
e.printStackTrace()
}
return lrcText
}
/**
* 解析一行歌词
*/
private fun parseLine(line: String): List<LyricEntry>? {
var lyricLine = line
if (TextUtils.isEmpty(lyricLine)) {
return null
}
lyricLine = lyricLine.trim { it <= ' ' }
// [00:17.65]让我掉下眼泪的
val lineMatcher = PATTERN_LINE.matcher(lyricLine)
if (!lineMatcher.matches()) {
return null
}
val times = lineMatcher.group(1)!!
val text = lineMatcher.group(3)!!
val entryList: MutableList<LyricEntry> = ArrayList()
// [00:17.65]
val timeMatcher = PATTERN_TIME.matcher(times)
while (timeMatcher.find()) {
val min = timeMatcher.group(1)!!.toLong()
val sec = timeMatcher.group(2)!!.toLong()
val milString = timeMatcher.group(3)!!
var mil = milString.toLong()
// 如果毫秒是两位数,需要乘以 10when 新增支持 1 - 6 位毫秒,很多获取的歌词存在不同的毫秒位数
when (milString.length) {
1 -> mil *= 100
2 -> mil *= 10
4 -> mil /= 10
5 -> mil /= 100
6 -> mil /= 1000
}
val time = min * DateUtils.MINUTE_IN_MILLIS + sec * DateUtils.SECOND_IN_MILLIS + mil
entryList.add(LyricEntry(time, text))
}
return entryList
}
/**
* 转为[:]
*/
fun formatTime(milli: Long): String {
val m = (milli / DateUtils.MINUTE_IN_MILLIS).toInt()
val s = (milli / DateUtils.SECOND_IN_MILLIS % 60).toInt()
val mm = String.format(Locale.getDefault(), "%02d", m)
val ss = String.format(Locale.getDefault(), "%02d", s)
return "$mm:$ss"
}
/**
* BUG java.lang.NoSuchFieldException: No field sDurationScale in class Landroid/animation/ValueAnimator; #3
*/
@SuppressLint("SoonBlockedPrivateApi")
@Deprecated("")
fun resetDurationScale() {
try {
val mField = ValueAnimator::class.java.getDeclaredField("sDurationScale")
mField.isAccessible = true
mField.setFloat(null, 1f)
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* 结合fraction计算两个值之间的比例
*/
fun calcScaleValue(a: Float, b: Float, f: Float, reverse: Boolean = false): Float {
if (b == 0f) return 1f
return 1f + ((a - b) / b) * (if (reverse) 1f - f else f)
}
/**
* 颜色值插值函数
*/
fun lerpColor(a: Int, b: Int, f: Float): Int {
return argbEvaluator.evaluate(f, a, b) as Int
}
/**
* 简单的插值函数
*/
fun lerp(from: Float, to: Float, fraction: Float): Float {
return from + (to - from) * fraction
}
/**
* 判断MotionEvent是否发生在Rect中
*/
fun MotionEvent.insideOf(rect: Rect?): Boolean {
rect ?: return false
return rect.contains(x.toInt(), y.toInt())
}
fun normalize(min: Float, max: Float, value: Float, limit: Boolean = false): Float {
if (min == max) return 1f
return ((value - min) / (max - min)).let {
if (limit) it.coerceIn(0f, 1f) else it
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,51 @@
package com.dirror.lyricviewx
import androidx.annotation.IntDef
const val STATE_CREATED = 1
const val STATE_INITIALIZING = 2
const val STATE_INITIALIZED = 3
const val STATE_ERROR = 4
@IntDef(
STATE_CREATED,
STATE_INITIALIZING,
STATE_INITIALIZED,
STATE_ERROR
)
@Retention(AnnotationRetention.SOURCE)
annotation class ReadyState
/**
* 简单的状态机根据 [readyState] 的状态决定当前任务的执行或延后与否
*/
open class ReadyHelper {
private var readyCallback: (Boolean) -> Unit = {}
@ReadyState
var readyState: Int = STATE_CREATED
set(value) {
if (field == value) return
when (value) {
STATE_INITIALIZED,
STATE_ERROR -> synchronized(readyCallback) {
field = value
readyCallback.invoke(value != STATE_ERROR)
}
else -> field = value
}
}
fun whenReady(performAction: (Boolean) -> Unit): Boolean {
return when (readyState) {
STATE_CREATED, STATE_INITIALIZING -> {
readyCallback = performAction
false
}
else -> {
performAction(readyState != STATE_ERROR)
true
}
}
}
}

View File

@ -0,0 +1,15 @@
package com.dirror.lyricviewx.extension
import android.graphics.BlurMaskFilter
import android.util.SparseArray
class BlurMaskFilterExt {
private val maskFilterCache = SparseArray<BlurMaskFilter>()
fun get(radius: Int): BlurMaskFilter? {
if (radius == 0 || radius > 25) return null
return maskFilterCache[radius] ?: BlurMaskFilter(radius.toFloat(), BlurMaskFilter.Blur.NORMAL)
.also { maskFilterCache.put(radius, it) }
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

1
lyricviewx/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

31
lyricviewx/build.gradle Normal file
View File

@ -0,0 +1,31 @@
plugins {
id 'com.android.library'
}
android {
compileSdkVersion 32
namespace 'com.jaeger.library'
defaultConfig {
minSdk 23
//noinspection ExpiredTargetSdkVersion,OldTargetApi
targetSdk 31
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
// implementation "org.jetbrains.kotlin:kotlin-stdlib:1.8.0"
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
}