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