parent
8006b12fe9
commit
05872ae26d
@ -0,0 +1,8 @@ |
|||||||
|
package com.frank.ffmpeg.listener |
||||||
|
|
||||||
|
import com.frank.ffmpeg.model.LrcLine |
||||||
|
|
||||||
|
interface OnLrcListener { |
||||||
|
|
||||||
|
fun onLrcSeek(position: Int, lrcLine: LrcLine) |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
package com.frank.ffmpeg.model |
||||||
|
|
||||||
|
|
||||||
|
class LrcLine : Comparable<LrcLine> { |
||||||
|
|
||||||
|
var timeString: String? = null |
||||||
|
|
||||||
|
var startTime: Long = 0 |
||||||
|
|
||||||
|
var endTime: Long = 0 |
||||||
|
|
||||||
|
var content: String? = null |
||||||
|
|
||||||
|
override fun compareTo(another: LrcLine): Int { |
||||||
|
return (this.startTime - another.startTime).toInt() |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,88 @@ |
|||||||
|
package com.frank.ffmpeg.tool |
||||||
|
|
||||||
|
import com.frank.ffmpeg.model.LrcLine |
||||||
|
|
||||||
|
import java.util.ArrayList |
||||||
|
import java.util.Collections |
||||||
|
|
||||||
|
object LrcLineTool { |
||||||
|
|
||||||
|
private fun createLine(lrcLine: String?): List<LrcLine>? { |
||||||
|
try { |
||||||
|
if (lrcLine == null || lrcLine.isEmpty() || |
||||||
|
lrcLine.indexOf("[") != 0 || lrcLine.indexOf("]") != 9) { |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
val lastIndexOfRightBracket = lrcLine.lastIndexOf("]") |
||||||
|
val content = lrcLine.substring(lastIndexOfRightBracket + 1) |
||||||
|
val times = lrcLine.substring(0, lastIndexOfRightBracket + 1).replace("[", "-") |
||||||
|
.replace("]", "-") |
||||||
|
if (times.isEmpty() || !Character.isDigit(times[1])) return null |
||||||
|
val arrTimes = times.split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() |
||||||
|
val listTimes = ArrayList<LrcLine>() |
||||||
|
for (temp in arrTimes) { |
||||||
|
if (temp.trim { it <= ' ' }.isEmpty()) { |
||||||
|
continue |
||||||
|
} |
||||||
|
val mLrcLine = LrcLine() |
||||||
|
mLrcLine.content = content |
||||||
|
mLrcLine.timeString = temp |
||||||
|
val startTime = timeConvert(temp) |
||||||
|
mLrcLine.startTime = startTime |
||||||
|
listTimes.add(mLrcLine) |
||||||
|
} |
||||||
|
return listTimes |
||||||
|
} catch (e: Exception) { |
||||||
|
e.printStackTrace() |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* string time to milliseconds |
||||||
|
*/ |
||||||
|
private fun timeConvert(timeStr: String): Long { |
||||||
|
var timeString = timeStr |
||||||
|
timeString = timeString.replace('.', ':') |
||||||
|
val times = timeString.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() |
||||||
|
return (Integer.valueOf(times[0]) * 60 * 1000 + |
||||||
|
Integer.valueOf(times[1]) * 1000 + |
||||||
|
Integer.valueOf(times[2])).toLong() |
||||||
|
} |
||||||
|
|
||||||
|
fun getLrcLine(line: String?): List<LrcLine>? { |
||||||
|
if (line == null || line.isEmpty()) { |
||||||
|
return null |
||||||
|
} |
||||||
|
val rows = ArrayList<LrcLine>() |
||||||
|
try { |
||||||
|
val lrcLines = createLine(line) |
||||||
|
if (lrcLines != null && lrcLines.isNotEmpty()) { |
||||||
|
rows.addAll(lrcLines) |
||||||
|
} |
||||||
|
} catch (e: Exception) { |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
return rows |
||||||
|
} |
||||||
|
|
||||||
|
fun sortLyrics(lrcList: List<LrcLine>): List<LrcLine> { |
||||||
|
Collections.sort(lrcList) |
||||||
|
if (lrcList.isNotEmpty()) { |
||||||
|
val size = lrcList.size |
||||||
|
for (i in 0 until size) { |
||||||
|
val lrcLine = lrcList[i] |
||||||
|
if (i < size - 1) { |
||||||
|
lrcLine.endTime = lrcList[i + 1].startTime |
||||||
|
} else { |
||||||
|
lrcLine.endTime = lrcLine.startTime + 10 * 1000 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return lrcList |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,258 @@ |
|||||||
|
package com.frank.ffmpeg.view |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import android.graphics.Bitmap |
||||||
|
import android.graphics.Canvas |
||||||
|
import android.graphics.Color |
||||||
|
import android.graphics.Paint |
||||||
|
import android.graphics.Paint.Align |
||||||
|
import android.util.AttributeSet |
||||||
|
import android.view.MotionEvent |
||||||
|
import android.view.View |
||||||
|
|
||||||
|
import com.frank.ffmpeg.listener.OnLrcListener |
||||||
|
import com.frank.ffmpeg.model.LrcLine |
||||||
|
import kotlin.math.abs |
||||||
|
|
||||||
|
/** |
||||||
|
* LrcView: display lyrics with playing time, seek and sync |
||||||
|
*/ |
||||||
|
|
||||||
|
class LrcView(context: Context, attr: AttributeSet) : View(context, attr) { |
||||||
|
|
||||||
|
private val mPadding = 10 |
||||||
|
|
||||||
|
private val mLrcFontSize = 35 |
||||||
|
|
||||||
|
private var mHighLightRow = 0 |
||||||
|
|
||||||
|
private var mLastMotionY = 0f |
||||||
|
|
||||||
|
private var currentMillis: Long = 0 |
||||||
|
|
||||||
|
private var mLrcLines: List<LrcLine>? = null |
||||||
|
|
||||||
|
private var mLrcViewListener: OnLrcListener? = null |
||||||
|
|
||||||
|
private val mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) |
||||||
|
|
||||||
|
private val mNormalRowColor = Color.BLACK |
||||||
|
|
||||||
|
private val mHighLightRowColor = Color.BLUE |
||||||
|
|
||||||
|
private var mDisplayMode = DISPLAY_MODE_NORMAL |
||||||
|
|
||||||
|
private val mode = MODE_HIGH_LIGHT_KARAOKE |
||||||
|
|
||||||
|
|
||||||
|
init { |
||||||
|
mPaint.textSize = mLrcFontSize.toFloat() |
||||||
|
} |
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) { |
||||||
|
val height = height |
||||||
|
val width = width |
||||||
|
if (mLrcLines == null || mLrcLines!!.isEmpty()) { |
||||||
|
mPaint.color = mHighLightRowColor |
||||||
|
mPaint.textSize = mLrcFontSize.toFloat() |
||||||
|
mPaint.textAlign = Align.CENTER |
||||||
|
canvas.drawText(noLrcTip, (width / 2).toFloat(), (height / 2 - mLrcFontSize).toFloat(), mPaint) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
var rowY: Int |
||||||
|
val rowX = width / 2 |
||||||
|
var rowNum: Int = mHighLightRow - 1 |
||||||
|
|
||||||
|
// 1: current highlight lyrics |
||||||
|
val highlightRowY = height / 2 - mLrcFontSize |
||||||
|
|
||||||
|
if (mode == MODE_HIGH_LIGHT_KARAOKE) { |
||||||
|
// highlight one by one |
||||||
|
drawKaraokeHighLightLrcRow(canvas, width, rowX, highlightRowY) |
||||||
|
} else { |
||||||
|
drawHighLrcRow(canvas, height, rowX, highlightRowY) |
||||||
|
} |
||||||
|
|
||||||
|
if (mDisplayMode == DISPLAY_MODE_SEEK) { |
||||||
|
mPaint.color = mSeekLineColor |
||||||
|
val mSeekLinePaddingX = 0 |
||||||
|
canvas.drawLine(mSeekLinePaddingX.toFloat(), (highlightRowY + mPadding).toFloat(), |
||||||
|
(width - mSeekLinePaddingX).toFloat(), (highlightRowY + mPadding).toFloat(), mPaint) |
||||||
|
mPaint.color = mSeekLineTextColor |
||||||
|
mPaint.textSize = mSeekLineTextSize.toFloat() |
||||||
|
mPaint.textAlign = Align.LEFT |
||||||
|
canvas.drawText(mLrcLines!![mHighLightRow].timeString!!, 0f, highlightRowY.toFloat(), mPaint) |
||||||
|
} |
||||||
|
|
||||||
|
// lyrics above the highlight one |
||||||
|
mPaint.color = mNormalRowColor |
||||||
|
mPaint.textSize = mLrcFontSize.toFloat() |
||||||
|
mPaint.textAlign = Align.CENTER |
||||||
|
rowY = highlightRowY - mPadding - mLrcFontSize |
||||||
|
while (rowY > -mLrcFontSize && rowNum >= 0) { |
||||||
|
val text = mLrcLines!![rowNum].content |
||||||
|
canvas.drawText(text!!, rowX.toFloat(), rowY.toFloat(), mPaint) |
||||||
|
rowY -= mPadding + mLrcFontSize |
||||||
|
rowNum-- |
||||||
|
} |
||||||
|
|
||||||
|
// lyrics below the highlight one |
||||||
|
rowNum = mHighLightRow + 1 |
||||||
|
rowY = highlightRowY + mPadding + mLrcFontSize |
||||||
|
while (rowY < height && rowNum < mLrcLines!!.size) { |
||||||
|
val text = mLrcLines!![rowNum].content |
||||||
|
canvas.drawText(text!!, rowX.toFloat(), rowY.toFloat(), mPaint) |
||||||
|
rowY += mPadding + mLrcFontSize |
||||||
|
rowNum++ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun drawKaraokeHighLightLrcRow(canvas: Canvas, width: Int, rowX: Int, highlightRowY: Int) { |
||||||
|
val highLrcLine = mLrcLines!![mHighLightRow] |
||||||
|
val highlightText = highLrcLine.content |
||||||
|
|
||||||
|
mPaint.color = mNormalRowColor |
||||||
|
mPaint.textSize = mLrcFontSize.toFloat() |
||||||
|
mPaint.textAlign = Align.CENTER |
||||||
|
canvas.drawText(highlightText!!, rowX.toFloat(), highlightRowY.toFloat(), mPaint) |
||||||
|
|
||||||
|
val highLineWidth = mPaint.measureText(highlightText).toInt() |
||||||
|
val leftOffset = (width - highLineWidth) / 2 |
||||||
|
val start = highLrcLine.startTime |
||||||
|
val end = highLrcLine.endTime |
||||||
|
val highWidth = ((currentMillis - start) * 1.0f / (end - start) * highLineWidth).toInt() |
||||||
|
if (highWidth > 0) { |
||||||
|
mPaint.color = mHighLightRowColor |
||||||
|
val textBitmap = Bitmap.createBitmap(highWidth, highlightRowY + mPadding, Bitmap.Config.ARGB_8888) |
||||||
|
val textCanvas = Canvas(textBitmap) |
||||||
|
textCanvas.drawText(highlightText, (highLineWidth / 2).toFloat(), highlightRowY.toFloat(), mPaint) |
||||||
|
canvas.drawBitmap(textBitmap, leftOffset.toFloat(), 0f, mPaint) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun drawHighLrcRow(canvas: Canvas, height: Int, rowX: Int, highlightRowY: Int) { |
||||||
|
val highlightText = mLrcLines!![mHighLightRow].content |
||||||
|
mPaint.color = mHighLightRowColor |
||||||
|
mPaint.textSize = mLrcFontSize.toFloat() |
||||||
|
mPaint.textAlign = Align.CENTER |
||||||
|
canvas.drawText(highlightText!!, rowX.toFloat(), highlightRowY.toFloat(), mPaint) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onTouchEvent(event: MotionEvent): Boolean { |
||||||
|
if (mLrcLines == null || mLrcLines!!.isEmpty()) { |
||||||
|
return super.onTouchEvent(event) |
||||||
|
} |
||||||
|
when (event.action) { |
||||||
|
MotionEvent.ACTION_DOWN -> { |
||||||
|
mLastMotionY = event.y |
||||||
|
invalidate() |
||||||
|
} |
||||||
|
MotionEvent.ACTION_MOVE -> doSeek(event) |
||||||
|
MotionEvent.ACTION_CANCEL, |
||||||
|
|
||||||
|
MotionEvent.ACTION_UP -> { |
||||||
|
if (mDisplayMode == DISPLAY_MODE_SEEK) { |
||||||
|
seekLrc(mHighLightRow, true) |
||||||
|
} |
||||||
|
mDisplayMode = DISPLAY_MODE_NORMAL |
||||||
|
invalidate() |
||||||
|
} |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Moving lyrics, when touch and move the screen |
||||||
|
*/ |
||||||
|
private fun doSeek(event: MotionEvent) { |
||||||
|
val y = event.y |
||||||
|
val offsetY = y - mLastMotionY |
||||||
|
if (abs(offsetY) < mMinSeekFiredOffset) { |
||||||
|
return |
||||||
|
} |
||||||
|
mDisplayMode = DISPLAY_MODE_SEEK |
||||||
|
val rowOffset = abs(offsetY.toInt() / mLrcFontSize) |
||||||
|
|
||||||
|
if (offsetY < 0) { |
||||||
|
mHighLightRow += rowOffset |
||||||
|
} else if (offsetY > 0) { |
||||||
|
mHighLightRow -= rowOffset |
||||||
|
} |
||||||
|
mHighLightRow = 0.coerceAtLeast(mHighLightRow) |
||||||
|
mHighLightRow = mHighLightRow.coerceAtMost(mLrcLines!!.size - 1) |
||||||
|
if (rowOffset > 0) { |
||||||
|
mLastMotionY = y |
||||||
|
invalidate() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun setListener(listener: OnLrcListener) { |
||||||
|
mLrcViewListener = listener |
||||||
|
} |
||||||
|
|
||||||
|
fun setLrc(lrcLines: List<LrcLine>) { |
||||||
|
mLrcLines = lrcLines |
||||||
|
invalidate() |
||||||
|
} |
||||||
|
|
||||||
|
private fun seekLrc(position: Int, cb: Boolean) { |
||||||
|
if (mLrcLines == null || position < 0 || position > mLrcLines!!.size) { |
||||||
|
return |
||||||
|
} |
||||||
|
val lrcLine = mLrcLines!![position] |
||||||
|
mHighLightRow = position |
||||||
|
invalidate() |
||||||
|
if (mLrcViewListener != null && cb) { |
||||||
|
mLrcViewListener!!.onLrcSeek(position, lrcLine) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun seekToTime(time: Long) { |
||||||
|
if (mLrcLines == null || mLrcLines!!.isEmpty()) { |
||||||
|
return |
||||||
|
} |
||||||
|
if (mDisplayMode != DISPLAY_MODE_NORMAL) { |
||||||
|
return |
||||||
|
} |
||||||
|
currentMillis = time |
||||||
|
|
||||||
|
for (i in mLrcLines!!.indices) { |
||||||
|
val current = mLrcLines!![i] |
||||||
|
val next = if (i + 1 == mLrcLines!!.size) null else mLrcLines!![i + 1] |
||||||
|
if (time >= current.startTime && next != null && time < next.startTime || time > current.startTime && next == null) { |
||||||
|
seekLrc(i, false) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
companion object { |
||||||
|
|
||||||
|
private const val mSeekLineTextSize = 15 |
||||||
|
|
||||||
|
private const val mSeekLineColor = Color.RED |
||||||
|
|
||||||
|
private const val mSeekLineTextColor = Color.BLUE |
||||||
|
|
||||||
|
/** |
||||||
|
* Mode normal |
||||||
|
*/ |
||||||
|
const val DISPLAY_MODE_NORMAL = 0 |
||||||
|
/** |
||||||
|
* Mode seek |
||||||
|
*/ |
||||||
|
const val DISPLAY_MODE_SEEK = 1 |
||||||
|
|
||||||
|
/** |
||||||
|
* Minimum seeking distance |
||||||
|
*/ |
||||||
|
private const val mMinSeekFiredOffset = 10 |
||||||
|
|
||||||
|
private const val noLrcTip = "No lyrics..." |
||||||
|
|
||||||
|
private const val MODE_HIGH_LIGHT_NORMAL = 0 |
||||||
|
|
||||||
|
private const val MODE_HIGH_LIGHT_KARAOKE = 1 |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue