修复BUG,更新UI
This commit is contained in:
parent
98a4bd87ad
commit
073fd37da7
|
@ -19,7 +19,7 @@ android {
|
||||||
//noinspection ExpiredTargetSdkVersion,OldTargetApi
|
//noinspection ExpiredTargetSdkVersion,OldTargetApi
|
||||||
targetSdk 31
|
targetSdk 31
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "2.0.0"
|
versionName "2.1.0"
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
|
257
app/src/main/java/com/dirror/lyricviewx/ILyricViewX.kt
Normal file
257
app/src/main/java/com/dirror/lyricviewx/ILyricViewX.kt
Normal 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()
|
||||||
|
}
|
82
app/src/main/java/com/dirror/lyricviewx/LyricEntry.kt
Normal file
82
app/src/main/java/com/dirror/lyricviewx/LyricEntry.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
289
app/src/main/java/com/dirror/lyricviewx/LyricUtil.kt
Normal file
289
app/src/main/java/com/dirror/lyricviewx/LyricUtil.kt
Normal 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()
|
||||||
|
// 如果毫秒是两位数,需要乘以 10,when 新增支持 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1097
app/src/main/java/com/dirror/lyricviewx/LyricViewX.kt
Normal file
1097
app/src/main/java/com/dirror/lyricviewx/LyricViewX.kt
Normal file
File diff suppressed because it is too large
Load Diff
51
app/src/main/java/com/dirror/lyricviewx/ReadyHelper.kt
Normal file
51
app/src/main/java/com/dirror/lyricviewx/ReadyHelper.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
BIN
app/src/main/res/mipmap-hdpi/icon.png
Normal file
BIN
app/src/main/res/mipmap-hdpi/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 105 KiB |
1
lyricviewx/.gitignore
vendored
Normal file
1
lyricviewx/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
31
lyricviewx/build.gradle
Normal file
31
lyricviewx/build.gradle
Normal 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'
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user