- 剪辑拖拽 View
- 主要支持功能:
1、左右滑动杆可支持左右滑动,确定剪辑的开始点和结束点
2、限定了可选剪辑时长(1s ~ 5s)
3、当选择剪辑时长为 5s 时,点击中间区域,可支持整个选中区域的左右滑动
4、支持音波展示,通过 putWaveValue()
方法传入数值
class AudioEditView : View {
companion object {
private const val TAG = "AudioEditView"
}
private var mPaint: Paint? = null
private var mDurationTextPaint: Paint? = null
private var mLinePaint: Paint? = null
private var mLineWidth = 4f
private var rectF1: RectF = RectF()
private var rectF2: RectF = RectF()
private var isInitRectF = false
private var mCenterRectF = RectF()
private var thumbWidth = 84F
private var thumbBitmap: Bitmap? = null
private var leftThumbBitmap: Bitmap? = null
private var rightThumbBitmap: Bitmap? = null
private var sideThumbBitmap: Bitmap? = null
private var maxMilliSecond = 5 * 60 * 1000
private var minMilliSecond = 1 * 1000
private var maxCutSecond = 5
private var maxPx = 0f
private var minPx = 0f
private val centerColor = context.getColor(R.color.audio_edit_center_color)
private val scrollBarColor = context.getColor(R.color.audio_edit_scrollbar_color)
private var onScrollListener: OnScrollListener? = null
private var downX = 0f
private var scrollCenter = false
private var scrollLeft = false
private var scrollRight = false
private var isScrollable = true
private var mWaveLinePaint: Paint? = null
private var mDefaultWaveBitmap: Bitmap? = null
private var mWaveLineColor = Color.TRANSPARENT
private var mWaveLineWidth = 2
private var mWaveLineSpace = 20
private var mHasOverWaveInput = false
private var maxLineCount = 0
private var maxValue = 1
private val fullValue = 100
private var mScale = 0f
private var isInitWaveData = true
private var mWaveValues: MutableList<Int>? = null
private var mWaveMaxHeight = 244
private var editEnable = true
private var waveEnable = true
private var mCurStartTime = 0
private var mCurEndTime = 0
private val mStartBoundaryValue by lazy {
paddingStart.toFloat() - thumbWidth / 2
}
private val mEndBoundaryValue by lazy {
width - paddingEnd.toFloat() + thumbWidth / 2
}
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
init(attrs)
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init(attrs)
}
@SuppressLint("Recycle")
private fun init(attrs: AttributeSet?) {
MLog.d(TAG, "init")
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.AudioEditView)
editEnable = typedArray.getBoolean(R.styleable.AudioEditView_editEnable, true)
waveEnable = typedArray.getBoolean(R.styleable.AudioEditView_waveEnable, true)
thumbBitmap = BitmapFactory.decodeResource(resources, R.drawable.usb_edit_scrollbar)
rightThumbBitmap = BitmapFactory.decodeResource(resources, R.drawable.usb_edit_scrollbar_right)
leftThumbBitmap = BitmapFactory.decodeResource(resources, R.drawable.usb_edit_scrollbar_left)
sideThumbBitmap = BitmapFactory.decodeResource(resources, R.drawable.usb_edit_scrollbar_side)
thumbWidth = typedArray.getDimensionPixelSize(R.styleable.AudioEditView_scrollThumbWidth, 84).toFloat()
mPaint = Paint().apply {
this.isAntiAlias = true
this.strokeWidth = typedArray.getDimensionPixelSize(R.styleable.AudioEditView_painStrokeWidth, 1).toFloat()
}
mDurationTextPaint = Paint().apply {
this.color = WTSkinManager.get().getColor(R.color.common_text_color_60)
this.strokeWidth = 3f
this.isAntiAlias = true
this.isFilterBitmap = true
this.style = Paint.Style.FILL
this.textSize = 28f
}
mLinePaint = Paint().apply {
this.color = WTSkinManager.get().getColor(R.color.audio_edit_line_color)
this.strokeWidth = mLineWidth
this.isAntiAlias = true
}
mWaveLineColor = typedArray.getColor(R.styleable.AudioEditView_waveLineColor, Color.parseColor("#8FBAE6"))
mWaveLineWidth = typedArray.getDimensionPixelSize(R.styleable.AudioEditView_waveLineWidth, 2)
mWaveLineSpace = typedArray.getDimensionPixelSize(R.styleable.AudioEditView_waveLineSpace, 20)
mWaveMaxHeight = typedArray.getDimensionPixelSize(R.styleable.AudioEditView_waveMaxHeight, 244)
mDefaultWaveBitmap = typedArray.getDrawable(R.styleable.AudioEditView_defaultWaveDrawable)?.toBitmap()
mWaveLinePaint = Paint().apply {
strokeWidth = mWaveLineWidth.toFloat()
isAntiAlias = true
color = mWaveLineColor
}
}
private fun initPx() {
MLog.d(TAG, "initPx: minMilliSecond = $minMilliSecond, maxMilliSecond = $maxMilliSecond, width = $width")
if (width > 0) {
maxPx = width - paddingStart - paddingEnd - thumbWidth
minPx = maxPx * minMilliSecond / maxMilliSecond
}
}
override fun onDraw(canvas: Canvas) {
MLog.d(TAG, "onDraw: waveEnable = $waveEnable, editEnable = $editEnable")
if (waveEnable) drawWave(canvas)
if (editEnable) {
drawLine(canvas)
drawEdit(canvas)
drawText(canvas)
}
}
private fun drawLine(canvas: Canvas) {
mLinePaint?.let {
canvas.drawLine(
paddingStart.toFloat(),
paddingBottom.toFloat(),
paddingStart + mLineWidth,
height - paddingBottom.toFloat(),
it
)
canvas.drawLine(
width - paddingEnd - mLineWidth,
paddingBottom.toFloat(),
width - paddingEnd.toFloat(),
height - paddingBottom.toFloat(),
it
)
}
}
private fun drawText(canvas: Canvas) {
mDurationTextPaint?.let {
val textMeasure = it.measureText("00:00")
val curStartTimePosition = rectF1.left + thumbWidth / 2 - textMeasure / 2
val curEndTimePosition = rectF2.left + thumbWidth / 2 - textMeasure / 2
val durationPosition = width - textMeasure - 20f
if (20f + textMeasure < curStartTimePosition) {
canvas.drawText("00:00", 20f, height - 20f, it)
}
if (curEndTimePosition + textMeasure < durationPosition) {
val duration = TimeUtil.secondsToTime(maxMilliSecond / 1000)
canvas.drawText(duration, durationPosition, height - 20f, it)
}
val curStartTime = TimeUtil.secondsToTime(mCurStartTime)
val curEndTime = TimeUtil.secondsToTime(mCurEndTime)
canvas.drawText(
curStartTime,
curStartTimePosition,
height - 20f,
it
)
canvas.drawText(
curEndTime,
curEndTimePosition,
height - 20f,
it
)
}
}
private fun drawEdit(canvas: Canvas) {
drawCenterRectF(canvas)
drawScrollBar(canvas)
}
private fun drawCenterRectF(canvas: Canvas) {
val mPaint = mPaint ?: return
mPaint.color = centerColor
mCenterRectF.left = rectF1.left + (rectF1.right - rectF1.left) / 2
mCenterRectF.right = rectF2.left + (rectF2.right - rectF2.left) / 2
canvas.drawRect(mCenterRectF, mPaint)
}
private fun drawScrollBar(canvas: Canvas) {
val mPaint = mPaint ?: return
mPaint.color = scrollBarColor
val leftScrollbarBitmap: Bitmap? = if (mCurStartTime == 0) {
sideThumbBitmap
} else if (mCurEndTime - mCurStartTime >= maxCutSecond) {
leftThumbBitmap
} else {
thumbBitmap
}
leftScrollbarBitmap?.let { canvas.drawBitmap(it, null, rectF1, mPaint) }
val rightScrollbarBitmap: Bitmap? = if (mCurEndTime == maxMilliSecond / 1_000) {
sideThumbBitmap
} else if (mCurEndTime - mCurStartTime >= maxCutSecond) {
rightThumbBitmap
} else {
thumbBitmap
}
rightScrollbarBitmap?.let { canvas.drawBitmap(it, null, rectF2, mPaint) }
}
private fun drawWave(canvas: Canvas) {
if (mWaveValues == null || (mWaveValues?.size ?: 0) <= 0) {
val rectF = RectF().apply {
left = paddingStart.toFloat()
right = width.toFloat() - paddingStart.toFloat()
top = ((height - mWaveMaxHeight) / 2).toFloat()
bottom = ((height - mWaveMaxHeight) / 2 + mWaveMaxHeight).toFloat()
}
mDefaultWaveBitmap?.let { canvas.drawBitmap(it, null, rectF, mPaint) }
MLog.d(TAG, "onDraw: mWaveValues is null!")
return
}
if (maxLineCount == 0) {
maxLineCount = (width - paddingBottom) / (mWaveLineSpace + mWaveLineWidth)
}
var startIndex = 0
if (mWaveValues!!.size > maxLineCount) {
startIndex = mWaveValues!!.size - maxLineCount
}
MLog.d(TAG, "onDraw: values = ${mWaveValues?.size}, maxLineCount = $maxLineCount, startIndex = $startIndex")
for (i in startIndex until mWaveValues!!.size) {
val lineHeight = (mWaveValues!![i].toFloat() * mScale / fullValue * mWaveMaxHeight).toInt()
val startX = (i - startIndex) * (mWaveLineSpace + mWaveLineWidth) + mWaveLineWidth / 2
canvas.drawLine(
paddingLeft + 4f + startX.toFloat(),
((height - lineHeight) / 2 ).toFloat(),
paddingLeft + 4f + startX.toFloat(),
((height - lineHeight) / 2 + lineHeight).toFloat(),
mWaveLinePaint!!
)
}
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
MLog.d(TAG, "onLayout: ")
if (!isInitRectF && editEnable) {
isInitRectF = true
initPx()
MLog.d(TAG, "onLayout: minPx = $minPx")
rectF1 = RectF().apply {
this.top = paddingTop.toFloat()
this.bottom = height.toFloat() - paddingBottom.toFloat()
this.left = paddingStart.toFloat() - thumbWidth / 2
this.right = this.left + thumbWidth
}
rectF2 = RectF().apply {
this.top = paddingTop.toFloat()
this.bottom = height.toFloat() - paddingBottom.toFloat()
this.left = rectF1.right + minPx * 5
this.right = this.left + thumbWidth
}
mCenterRectF = RectF().apply {
this.top = paddingTop.toFloat()
this.bottom = height.toFloat() - paddingBottom.toFloat()
this.left = rectF1.left + thumbWidth / 2
this.right = rectF2.right - thumbWidth / 2
}
updateListener()
}
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
move(event)
return isScrollable && (scrollLeft || scrollRight || scrollCenter)
}
fun setMinInterval(minMilliSec: Int) {
MLog.d(TAG, "setMinInterval: minMilliSec = $minMilliSec")
if (minMilliSec in minMilliSecond..maxMilliSecond) {
minMilliSecond = minMilliSec
}
initPx()
}
fun setDuration(duration: Int) {
MLog.d(TAG, "setDuration: duration = $duration")
maxMilliSecond = duration
initPx()
}
fun setOnScrollListener(listener: OnScrollListener?) {
onScrollListener = listener
}
fun setScrollable(isScrollable: Boolean) {
this.isScrollable = isScrollable
}
private fun move(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
downX = event.x
if (downX > rectF1.right - thumbWidth / 2 && downX < rectF2.left) {
scrollCenter = true
} else if (downX > rectF1.left - thumbWidth / 2 && downX < rectF1.right + thumbWidth / 2) {
scrollLeft = true
} else if (downX > rectF2.left - thumbWidth / 2 && downX < rectF2.right + thumbWidth / 2) {
scrollRight = true
}
}
MotionEvent.ACTION_MOVE -> {
val moveX = event.x
val scrollX = moveX - downX
MLog.d(TAG, "ACTION_MOVE: moveX = $moveX, scrollX = $scrollX")
if (scrollLeft) {
MLog.d(TAG, "ACTION_MOVE: scrollLeft")
moveLeftScrollBar(scrollX)
} else if (scrollRight) {
MLog.d(TAG, "ACTION_MOVE: scrollRight")
moveRightScrollBar(scrollX)
} else if (scrollCenter) {
MLog.d(TAG, "ACTION_MOVE: scrollCenter")
moveCenterArea(scrollX)
}
updateListener()
downX = moveX
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
downX = 0f
scrollLeft = false
scrollRight = false
}
}
return true
}
private fun moveLeftScrollBar(scrollX: Float) {
val isFit = rectF2.left - rectF1.right <= maxCutSecond * minPx + 0.01
MLog.d(TAG, "moveRightScrollBar: isFit = $isFit, scrollX = $scrollX")
if (isFit) {
rectF1.left = rectF1.left + scrollX
rectF1.right = rectF1.right + scrollX
}
if (rectF1.right > rectF2.left - minPx) {
rectF1.right = rectF2.left - minPx
rectF1.left = rectF1.right - thumbWidth
}
if (rectF2.left - rectF1.right > (maxCutSecond) * minPx) {
rectF1.right = rectF2.left - minPx * maxCutSecond
rectF1.left = rectF1.right - thumbWidth
}
if (rectF1.left < mStartBoundaryValue) {
rectF1.left = mStartBoundaryValue
rectF1.right = rectF1.left + thumbWidth
}
}
private fun moveRightScrollBar(scrollX: Float) {
val isFit = rectF2.left - rectF1.right <= maxCutSecond * minPx + 0.01
MLog.d(TAG, "moveRightScrollBar: isFit = $isFit, scrollX = $scrollX")
if (isFit) {
rectF2.left = rectF2.left + scrollX
rectF2.right = rectF2.right + scrollX
}
if (rectF2.left < rectF1.right + minPx) {
rectF2.left = rectF1.right + minPx
rectF2.right = rectF2.left + thumbWidth
}
if (rectF2.left - rectF1.right > (maxCutSecond ) * minPx) {
rectF2.left = rectF1.right + minPx * maxCutSecond
rectF2.right = rectF2.left + thumbWidth
}
if (rectF2.right > mEndBoundaryValue) {
rectF2.right = mEndBoundaryValue
rectF2.left = rectF2.right - thumbWidth
}
}
private fun moveCenterArea(scrollX: Float) {
val isFit = rectF2.left - rectF1.right >= maxCutSecond * minPx - 0.01
MLog.d(TAG, "moveCenterArea: isFit = $isFit, scrollX = $scrollX")
if (isFit) {
rectF1.left = rectF1.left + scrollX
rectF1.right = rectF1.right + scrollX
rectF2.left = rectF2.left + scrollX
rectF2.right = rectF2.right + scrollX
}
if (rectF1.left < mStartBoundaryValue) {
rectF1.left = mStartBoundaryValue
rectF1.right = rectF1.left + thumbWidth
rectF2.left = rectF1.right + maxCutSecond * minPx
rectF2.right = rectF2.left + thumbWidth
}
if (rectF2.right > mEndBoundaryValue) {
rectF2.right = mEndBoundaryValue
rectF2.left = rectF2.right - thumbWidth
rectF1.right = rectF2.left - maxCutSecond * minPx
rectF1.left = rectF1.right - thumbWidth
}
}
private fun updateListener() {
if (onScrollListener != null) {
MLog.d(TAG, "updateListener: maxMilliSecond = $maxMilliSecond, maxPx = $maxPx, minPx = $minPx")
MLog.d(TAG, "updateListener: rectF1 = $rectF1, rectF2 = $rectF2")
mCurStartTime = ((rectF1.left - paddingStart + thumbWidth / 2) / minPx).coerceAtLeast(0F).toInt()
mCurEndTime = (((rectF2.left - paddingStart - thumbWidth / 2) / minPx) + 0.01).toInt()
val info = ScrollInfo()
MLog.d(TAG, "updateListener: startTime = $mCurStartTime, endTime = $mCurEndTime")
info.startTime = mCurStartTime
info.endTime = mCurEndTime
info.startPx = rectF1.left
info.endPx = rectF2.right
MLog.d(TAG, "updateListener: info = $info")
onScrollListener?.onScrollThumb(info)
}
invalidate()
}
fun putWaveValue(value: Int) {
if (mHasOverWaveInput) return
if (value > maxValue) {
maxValue = value
mScale = fullValue.toFloat() / maxValue
}
if (mWaveValues == null || isInitWaveData) {
isInitWaveData = false
mWaveValues = ArrayList()
}
mWaveValues?.add(value)
invalidate()
}
fun setHasOverWaveInput(over: Boolean) {
isInitWaveData = !over
mHasOverWaveInput = over
}
interface OnScrollListener {
fun onScrollThumb(info: ScrollInfo?)
}
inner class ScrollInfo {
var startTime = 0
var endTime = 0
var startPx = 0F
var endPx = 0F
override fun toString(): String {
return "AudioEditView.ScrollInfo[startTime= $startTime, endTime = $endTime, startPx = $startPx, endPx = $endPx]"
}
}
}
![在这里插入图片描述](https://img-blog.csdnimg.cn/3c4ef5c416db476c988a5d07cbdd00a6.png)