音频-剪辑拖拽 View

5 篇文章 1 订阅
  • 剪辑拖拽 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()

    /*是否初始化 rectF1、rectF2*/
    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

    /*最大时长 5min,毫秒为单位*/
    private var maxMilliSecond = 5 * 60 * 1000

    /*最小时长 1s,毫秒为单位*/
    private var minMilliSecond = 1 * 1000

    /*最大裁剪时长 5s,秒为单位*/
    private var maxCutSecond = 5

    /*最大时长对应的 px*/
    private var maxPx = 0f

    /*最小间隔时长对应的 px: 1s*/
    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

    /*按下的点 X*/
    private var downX = 0f

    /*是否滑动整个选中区域*/
    private var scrollCenter = false

    /*是否滑动左滑动杆*/
    private var scrollLeft = false

    /*是否滑动右滑动杆*/
    private var scrollRight = false

    /*是否可滑动状态*/
    private var isScrollable = true

    /*音波 Paint*/
    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

    /*当前数组中的最大值,该值乘以 scale 应等于 fullValue */
    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")
            // 左边滑动杆起始位置 00:00
            rectF1 = RectF().apply {
                this.top = paddingTop.toFloat()
                this.bottom = height.toFloat() - paddingBottom.toFloat()
                this.left = paddingStart.toFloat() - thumbWidth / 2
                this.right = this.left + thumbWidth
            }
            // 右边滑动杆起始位置 00:05
            rectF2 = RectF().apply {
                this.top = paddingTop.toFloat()
                this.bottom = height.toFloat() - paddingBottom.toFloat()
                this.left = rectF1.right + minPx * 5
                this.right = this.left + thumbWidth
            }
            // centerRectF
            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)
    }

    /**
     * 设置最小时间间隔,单位毫秒
     *
     * @param minMilliSec 最小的时间间隔
     */
    fun setMinInterval(minMilliSec: Int) {
        MLog.d(TAG, "setMinInterval: minMilliSec = $minMilliSec")
        if (minMilliSec in minMilliSecond..maxMilliSecond) {
            minMilliSecond = minMilliSec
        }
        initPx()
    }

    /**
     * 设置音频时长,单位毫秒
     *
     * @param duration 音频时长
     */
    fun setDuration(duration: Int) {
        MLog.d(TAG, "setDuration: duration = $duration")
        maxMilliSecond = duration
        initPx()
    }

    /**
     * 添加滑动监听
     */
    fun setOnScrollListener(listener: OnScrollListener?) {
        onScrollListener = listener
    }

    /**
     * 设置可滑动状态
     * @param isScrollable 是否可以滑动
     */
    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
        }
        // 限制最小截取时长: 1s
        if (rectF1.right > rectF2.left - minPx) {
            rectF1.right = rectF2.left - minPx
            rectF1.left = rectF1.right - thumbWidth
        }
        // 限制最大截取时长: 5s
        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
        }
        // 限制最小截取时长: 1s
        if (rectF2.left < rectF1.right + minPx) {
            rectF2.left = rectF1.right + minPx
            rectF2.right = rectF2.left + thumbWidth
        }
        // 限制最大截取时长: 5s
        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) {
        // 避免如:4.99999 的特殊情况
        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
            // 避免 4.99999 的特殊情况
            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()
    }

    /**
     * 是否结束值输入
     * @param over 是否结束
     */
    fun setHasOverWaveInput(over: Boolean) {
        isInitWaveData = !over
        mHasOverWaveInput = over
    }

    interface OnScrollListener {
        /**
         * 滑动两边滑动杆监听
         *
         * @param info 滑动信息
         */
        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]"
        }
    }

}
  • 在这里插入图片描述
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值