美文网首页Android自定义View
Android 仿抖音视频裁剪范围选择控件,支持本地视频和网络视

Android 仿抖音视频裁剪范围选择控件,支持本地视频和网络视

作者: lucasDev | 来源:发表于2020-11-03 00:54 被阅读0次

    实现后效果:由于是在模拟器上跑的背面的封面列表加载不出来,实际效果请真机运行


    image.png

    具体代码如下:

    绘制上层滑动控件部分

    package com.cj.customwidget.widget
    
    import android.content.Context
    import android.graphics.*
    import android.util.AttributeSet
    import android.view.MotionEvent
    import android.view.View
    import com.cj.customwidget.R
    import com.cj.customwidget.p
    
    /**
     * @package    com.cj.customwidget.widget
     * @author     luan
     * @date       2020/10/16
     * @des        视频裁剪区域选择
     */
    class CropSeekBar : View {
        constructor(context: Context) : super(context) {
            initView(context, null)
        }
    
        constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
            initView(context, attrs)
        }
    
        constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
            initView(context, attrs)
        }
    
        private val color = Color.WHITE//边框颜色
        var slideW = 20f//两侧滑块宽度
        var strokeW = 4f//上下边框宽度
        var slideOutH = 10f//进度滑块越界高度
        var midSlideW = 8f//中间滑块宽度
        private var radio = 16f//圆角角度
        val slidePadding = 100//两侧滑块外边距
    
        var midProgress = 0f//中间滑块的x坐标
        var seekLeft = 0f//左测滑块的x坐标
        var seekRight = 0f//右测滑块的x坐标
        var maxInterval = 60L * 1000//最大区间-时长ms
        var minInterval = 10L * 1000//最小区间-时长ms
    
        private val strokeLinePaint = Paint()
        private val slidePaint = Paint()
        private val path = Path()
    
        private val progressRectF = RectF()//中间滑块有效触摸范围
        private val leftSlideTouchRectF = RectF()//左滑块有效触摸范围
        private val rightSlideTouchRectF = RectF()//右滑块有效触摸范围
        private var isMoveSlide: Boolean = false
        var onChangeProgress: (progress: Float) -> Unit = { progress -> }
        var onSectionChange: (left: Float, right: Float) -> Unit = { left, right -> }
    //    var onTouchChange: (isTouch: Boolean) -> Unit = {}
    
        private fun initView(context: Context, attrs: AttributeSet?) {
            setWillNotDraw(false)
            attrs?.apply {
                val obtain = context.obtainStyledAttributes(attrs, R.styleable.CropSeekBar)
                slideOutH = obtain.getDimension(R.styleable.CropSeekBar_vc_slide_out_h, slideOutH)
                radio = obtain.getDimension(R.styleable.CropSeekBar_vc_radio, radio)
                obtain.recycle()
            }
            strokeLinePaint.isAntiAlias = true
            strokeLinePaint.strokeWidth = strokeW
            strokeLinePaint.color = color
            strokeLinePaint.strokeWidth = strokeW
    
            slidePaint.isAntiAlias = true
            slidePaint.color = color
            slidePaint.style = Paint.Style.FILL
    
        }
    
        override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
            seekLeft = slidePadding.toFloat() + slideW / 2
            seekRight = width - slidePadding.toFloat() - slideW / 2
            midProgress = slidePadding.toFloat() + slideW + midSlideW / 2
            super.onLayout(changed, left, top, right, bottom)
        }
    
    
        override fun onDraw(canvas: Canvas) {
            leftSlideTouchRectF.left = seekLeft - slideW / 2
            leftSlideTouchRectF.top = slideOutH
            leftSlideTouchRectF.right = seekLeft + slideW / 2
            leftSlideTouchRectF.bottom = height.toFloat() - slideOutH
    
            rightSlideTouchRectF.left = seekRight - slideW / 2
            rightSlideTouchRectF.top = slideOutH
            rightSlideTouchRectF.right = seekRight + slideW / 2
            rightSlideTouchRectF.bottom = height.toFloat() - slideOutH
    
            //绘制播放进度滑块
            progressRectF.left = midProgress - midSlideW / 2
            progressRectF.top = 0f
            progressRectF.right = midProgress + midSlideW / 2
            progressRectF.bottom = height.toFloat()
    
            //绘制上下边框
            canvas.drawLine(
                leftSlideTouchRectF.right,
                slideOutH + strokeW / 2,
                rightSlideTouchRectF.left,
                slideOutH + strokeW / 2,
                strokeLinePaint
            )
            canvas.drawLine(
                leftSlideTouchRectF.right,
                height.toFloat() - slideOutH - strokeW / 2,
                rightSlideTouchRectF.left,
                height.toFloat() - slideOutH - strokeW / 2,
                strokeLinePaint
            )
            //绘制两边滑块
            path.reset()
            path.moveTo(leftSlideTouchRectF.left, radio + leftSlideTouchRectF.top)
            path.quadTo(
                leftSlideTouchRectF.left,
                leftSlideTouchRectF.top,
                radio + leftSlideTouchRectF.left,
                leftSlideTouchRectF.top
            )
            path.lineTo(leftSlideTouchRectF.right, leftSlideTouchRectF.top)
            path.lineTo(leftSlideTouchRectF.right, leftSlideTouchRectF.bottom)
            path.lineTo(radio + leftSlideTouchRectF.left, leftSlideTouchRectF.bottom)
            path.quadTo(
                leftSlideTouchRectF.left,
                leftSlideTouchRectF.bottom,
                leftSlideTouchRectF.left,
                leftSlideTouchRectF.bottom - radio
            )
            path.lineTo(leftSlideTouchRectF.left, radio + leftSlideTouchRectF.top)
            canvas.drawPath(path, slidePaint)
    
            path.reset()
            path.moveTo(rightSlideTouchRectF.left, rightSlideTouchRectF.top)
            path.lineTo(rightSlideTouchRectF.right - radio, rightSlideTouchRectF.top)
            path.quadTo(
                rightSlideTouchRectF.right,
                rightSlideTouchRectF.top,
                rightSlideTouchRectF.right,
                radio + rightSlideTouchRectF.top
            )
            path.lineTo(rightSlideTouchRectF.right, rightSlideTouchRectF.bottom - radio)
            path.quadTo(
                rightSlideTouchRectF.right,
                rightSlideTouchRectF.bottom,
                rightSlideTouchRectF.right - radio,
                rightSlideTouchRectF.bottom
            )
            path.lineTo(rightSlideTouchRectF.left, rightSlideTouchRectF.bottom)
            path.lineTo(rightSlideTouchRectF.left, rightSlideTouchRectF.top)
            canvas.drawPath(path, slidePaint)
    
            canvas.drawRoundRect(progressRectF, midSlideW, midSlideW, slidePaint)
            super.onDraw(canvas)
        }
    
        private val SCROLL_MODE_NONE = 0
        private val SCROLL_MODE_LEFT = 1//左滑块
        private val SCROLL_MODE_RIGHT = 2//右滑块
        private val SCROLL_MODE_PROGRESS = 3//播放进度滑块
        private var scrollMode = SCROLL_MODE_NONE
        override fun onTouchEvent(event: MotionEvent): Boolean {
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    if (leftSlideTouchRectF.contains(event.x, event.y)) {
                        //移动左滑块
                        scrollMode = SCROLL_MODE_LEFT
                        return true
                    } else if (rightSlideTouchRectF.contains(event.x, event.y)) {
                        //移动右滑块
                        scrollMode = SCROLL_MODE_RIGHT
                        return true
                    } else if (event.x in progressRectF.left - 10..progressRectF.right + 10) {
                        //移动中间滑块
                        scrollMode = SCROLL_MODE_PROGRESS
                        return true
                    }
    
                }
                MotionEvent.ACTION_MOVE -> {
                    val minW = (width - slidePadding * 2 - slideW * 2) * (minInterval.toFloat() / maxInterval)
                    if (scrollMode == SCROLL_MODE_LEFT) {
                        if (event.x > slidePadding) {
                            if (seekRight - event.x - slideW > minW) { //判断最小区间
                                seekLeft = event.x
                            } else {
                                seekLeft = seekRight - minW - slideW
                            }
                        } else {//回到默认位置
                            seekLeft = slidePadding.toFloat() + slideW / 2
                        }
                        midProgress = seekLeft + slideW / 2 + midSlideW / 2
                        isMoveSlide = true
                        onSectionChange(seekLeft, seekRight)
                        onChangeProgress(midProgress)
                        invalidate()
                        return true
                    } else if (scrollMode == SCROLL_MODE_RIGHT) {
                        if (event.x < width - slidePadding) {
                            if (event.x - seekLeft - slideW > minW) { //判断最小区间
                                seekRight = event.x
                            } else {
                                seekRight = seekLeft + minW + slideW
                            }
                        } else {
                            seekRight = width - slidePadding.toFloat() - slideW / 2
                        }
                        midProgress = seekRight - slideW / 2 - midSlideW / 2
                        isMoveSlide = true
                        onSectionChange(seekLeft, seekRight)
                        onChangeProgress(midProgress)
                        invalidate()
                        return true
                    } else if (scrollMode == SCROLL_MODE_PROGRESS) {
                        if (event.x in seekLeft + slideW / 2..seekRight - slideW / 2) {//只允许在区间内滑动
                            midProgress = event.x
                        }
                        isMoveSlide = false
                        onChangeProgress(midProgress)
                        invalidate()
                        return true
                    }
                }
                MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                    isMoveSlide = false
                    if (scrollMode == SCROLL_MODE_RIGHT || scrollMode == SCROLL_MODE_LEFT) {
                        onChangeProgress(midProgress)
                    }
                    scrollMode = SCROLL_MODE_NONE
                }
            }
            return super.onTouchEvent(event)
        }
    
    }
    

    绘制下层封面列表,以及与上层空间联动

    package com.cj.customwidget.widget
    
    import android.content.Context
    import android.graphics.Bitmap
    import android.graphics.Color
    import android.graphics.RectF
    import android.media.MediaMetadataRetriever
    import android.util.AttributeSet
    import android.view.MotionEvent
    import android.widget.FrameLayout
    import android.widget.ImageView
    import android.widget.LinearLayout
    import com.cj.customwidget.p
    import java.lang.Exception
    import kotlin.concurrent.thread
    
    /**
     * @package    com.cj.customwidget.widget
     * @author     luan
     * @date       2020/10/28
     * @des
     */
    class VideoCropSeekBar : FrameLayout {
        constructor(context: Context) : super(context) {
            initView(context, null)
        }
    
        constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
            initView(context, attrs)
        }
    
        constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
            initView(context, attrs)
        }
    
        lateinit var coverView: LinearLayout
        lateinit var seekBar: CropSeekBar
        private var coverRectF = RectF()
        private var picW = 80f//每张封面宽度--这并不是最终值,会根据控件长度调整
        var videoDuration = 0L//视频时长
        var onSeekChange: (progress: Long) -> Unit = { }//当进度发生变化
        var onSectionChange: (left: Float, right: Float) -> Unit = { left, right -> }
        var onTouchChange: (isTouch: Boolean) -> Unit = {}
    
        private fun initView(context: Context, attrs: AttributeSet?) {
            coverView = LinearLayout(context)
            addView(coverView)
            seekBar = CropSeekBar(context).apply {
                layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
            }
            addView(seekBar)
            seekBar.onChangeProgress = {
                seekChange()
            }
            seekBar.onSectionChange = { left, right ->
                seekChange()
                onSectionChange(left, right)
            }
        }
    
        //获取左侧滑块时间轴
        fun getLeftSlideSecond(): Long {
            return (((seekBar.seekLeft - coverRectF.left + seekBar.slideW / 2) / coverView.width) * videoDuration).toLong()
        }
    
        //获取右侧滑块时间轴
        fun getRightSlideSecond(): Long {
            return (((seekBar.seekRight - coverRectF.left - seekBar.slideW / 2) / coverView.width) * videoDuration).toLong()
        }
    
        //设置视频资源
        fun setVideoUri(videoPath: String) {
            getVideoInfo(videoPath) { retriever ->
                //计算封面列表矩形大小和位置
                var coverW: Float
                if (videoDuration < seekBar.maxInterval) {//如果视频长度小于最大区间,则封面列表宽度等于最大区间,这个时候时间轴会被拉伸
                    coverW = seekBar.seekRight - seekBar.seekLeft - seekBar.slideW
                    seekBar.maxInterval = videoDuration
                } else {
                    coverW =
                        (videoDuration.toFloat() / seekBar.maxInterval) * (width - seekBar.slidePadding * 2 - seekBar.slideW * 2)
                }
    
                val coverMargin = seekBar.slidePadding + seekBar.slideW
                coverRectF.set(
                    coverMargin,
                    seekBar.slideOutH + seekBar.strokeW,
                    coverW + coverMargin,
                    height - seekBar.slideOutH - seekBar.strokeW
                )
                invalidate()
                onSectionChange(seekBar.seekLeft, seekBar.seekRight)
                thread {
                    try {
                        //计算需要获取多少张封面
                        val picNum = (coverW / picW).toInt()
                        //根据图片数量再次计算封面宽度,使图片可以填满整个列表
                        picW = coverW / picNum
                        //获取第一帧
                        var firstFrame = retriever.getFrameAtTime(1, MediaMetadataRetriever.OPTION_PREVIOUS_SYNC)
                        addCover(firstFrame)
                        //计算获取每张封面的时间间隔
                        val videoDuration1 = videoDuration
                        val diffTime = videoDuration1 / picNum
                        var index = 1
                        while (index * diffTime < videoDuration) {
                            firstFrame =
                                retriever.getFrameAtTime(
                                    index++ * diffTime * 1000,
                                    MediaMetadataRetriever.OPTION_CLOSEST_SYNC
                                )
                            addCover(firstFrame)
                        }
                        retriever.release()
                    } catch (e: Exception) {
                        e.printStackTrace()
                    }
                }
            }
        }
    
        private fun addCover(firstFrame: Bitmap?) {
            post {
                try {
                    coverView.addView(ImageView(context).apply {
                        layoutParams = LinearLayout.LayoutParams(picW.toInt(), LinearLayout.LayoutParams.MATCH_PARENT)
                        scaleType = ImageView.ScaleType.CENTER_CROP
                        setImageBitmap(firstFrame)
                    })
                } catch (e: Exception) {
                    e.printStackTrace()
                }
            }
        }
    
        private fun getVideoInfo(videoPath: String, block: (MediaMetadataRetriever) -> Unit) {
            thread {
                //解析视频参数
                val retriever = MediaMetadataRetriever()
                if (videoPath.startsWith("http:") || videoPath.startsWith("https:"))
                    retriever.setDataSource(videoPath, HashMap<String, String>())//网络视频
                else
                    retriever.setDataSource(videoPath)
                //获取视频长度
                videoDuration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong()
                "videoDuration:$videoDuration".p()
                seekBar.invalidate()
                post { block.invoke(retriever) }
            }
        }
    
        override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
            super.onLayout(changed, left, top, right, bottom)
            seekBar.layout(0, 0, width, height)
            layoutCover()
        }
    
        private var lastX = 0f
        private var lastY = 0f
    
        override fun onTouchEvent(event: MotionEvent): Boolean {
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    lastX = event.x
                    lastY = event.y
                    onTouchChange(true)
                    return true
                }
                MotionEvent.ACTION_MOVE -> {
                    //限制边界
                    val diffX = event.x - lastX
                    if (coverRectF.left + diffX > seekBar.slidePadding + seekBar.slideW) {
                        coverRectF.left = seekBar.slidePadding + seekBar.slideW
                        coverRectF.right = coverView.width + coverRectF.left
                    } else if (coverRectF.right + diffX <= seekBar.width - seekBar.slidePadding - seekBar.slideW) {
                        coverRectF.right = seekBar.width - seekBar.slidePadding - seekBar.slideW
                        coverRectF.left = coverRectF.right - coverView.width
                    } else {
                        coverRectF.left += diffX
                        coverRectF.right += diffX
                    }
                    layoutCover()
                    seekChange()
                    lastX = event.x
                    lastY = event.y
                    return true
                }
                MotionEvent.ACTION_UP -> {
                    onTouchChange(false)
                }
            }
            return super.onTouchEvent(event)
        }
    
        private fun seekChange() {
            val progress = (seekBar.midProgress - coverRectF.left) / coverView.width//进度百分比
            onSeekChange((progress * videoDuration).toLong())
        }
    
        private fun layoutCover() {
            coverView.layout(
                coverRectF.left.toInt(),
                coverRectF.top.toInt(),
                coverRectF.right.toInt(),
                coverRectF.bottom.toInt()
            )
        }
    
    }
    

    项目地址:https://github.com/LucasDevelop/CustomView. 视频裁剪Seek Bar界面

    相关文章

      网友评论

        本文标题:Android 仿抖音视频裁剪范围选择控件,支持本地视频和网络视

        本文链接:https://www.haomeiwen.com/subject/vugvvktx.html