It’s the old rule

Lu code

Use:

Val adapter = TestAdapter(this) main_cut.setFrameAdapter(adapter) main_cut.setVideoduration (20000) // Analog delay loading frame list main_cut.postDelayed({ main_cut.computeWithDataComplete(dp2px(this, 60f), Runnable {// The Runnable function is to add transparent item Adapter.addData ("") }) }, 3000) main_cut.setOnCutDurationListener { startMs, endMs, state, orientation -> /** * @param state * @see STATE_MOVE * @see STATE_IDLE * * * @param orientation * @see ORIENTATION_LEFT *  @see ORIENTATION_RIGHT * */ Log.d( TAG,"startMs = $startMs , endMs = $endMs ,state = $state , orientation = $orientation"
            )
            main_txt.text =
                "startMs = $startMs , endMs = $endMs ,state = $state , orientation = $orientation"} / / set the progress bar to monitor main_cut. SetOnProgressListener (object: VideoCutLayout.OnProgressChangedListener { override fun onDragDown(time: Override fun onDragMove(time: Long) {override fun onDragUp(time: Long) {// Release the progress bar}})Copy the code

Analyze:

  • The first layer is a list of frames

  • The second layer is a draggable control

First of all, there are two pictures on both sides of the drag bar, and there is a line up and down in the middle. You can change the left /right of the two pictures by sliding your finger to move them. Note that the slide coordinates are based on the highlighted part, so when returning to the outer layer, you need to subtract the width of the images on both sides.


Update: Added drag direction value (LEFT,RIGHT). And state value (MOVE,IDLE)

The key codes are as follows:

  #onDrawStyle = paint.style. FILL mPaint. Color = color.parsecolor ("# 80000000") canvas.drawRect(getLeftWidth().toFloat(), 0f, mLeftPadding, height.toFloat(), mPaint) canvas.drawRect(mRightPadding, 0f, width.toFloat(), height.toFloat(), Style = Paint.Style.STROKE mPaint. Color = ContextCompat. R.color.colorAccent) canvas.drawLine(mRectF.left, 0f, mRectF.right, 0f, mPaint) canvas.drawLine(mRectF.left, Height.tofloat (), mrectf.right, height.tofloat (), mPaint) drawBitmap(mLeftBitmap, mLeftPadding, 0f, drawBitmap(mLeftBitmap, mLeftPadding, 0f, mPaint) canvas.drawBitmap(mRightBitmap, mRightPadding - mRightBitmap.width, 0f, mPaint)Copy the code
# onTouchEvent


        var consumed = false
        if(! isEnabled)returnConsumed when (event.action) {motionEvent.action_down -> {// Determines whether the click point is on the boundary and processes the event if so. val downX = event.x mDownX = downX mLastX = downX consumed =if (downX > mLeftPadding && downX < mLeftPadding + mLeftBitmap.width) {
                    click = ORIENTATION_LEFT
                    mLastX = downX
                    true
                } else if (downX > mRightPadding - mRightBitmap.width && downX < mRightPadding) {
                    click = ORIENTATION_RIGHT
                    mLastX = downX
                    true
                } else {
                    click = -1
                    false}} // The move event determines whether the move exceeds the endpoint. ACTION_MOVE -> {val moveX = event.x val dx = movex-mlastx = when (click) { ORIENTATION_LEFT -> { val newPadding = max(mLeftPadding + dx, 0f)if (newPadding + mLeftBitmap.width < mRectF.right) {
                            val curDuration =
                                (mRectF.right - newPadding - mLeftBitmap.width) * durationPx
                            if(! (curDuration <= minDuration + 1 && dx > 0)) { mLeftPadding = newPadding } }true
                    }
                    ORIENTATION_RIGHT -> {
                        val newPadding = min(mRightPadding + dx, width.toFloat())
                        if (newPadding - mRightBitmap.width > mRectF.left) {
                            val curDuration =
                                (newPadding - mRightBitmap.width - mRectF.left) * durationPx
                            if(! (curDuration <= minDuration + 1 && dx < 0)) { mRightPadding = newPadding } }true
                    }
                    else- > {false}}if(consumed) { mLastX = moveX mRectF.left = mLeftPadding + mLeftBitmap.width mRectF.right = mRightPadding - mRightBitmap.width invalidate() mListener? .invoke(mRectF.left, mRectF.right, STATE_MOVE, click) } } MotionEvent.ACTION_UP, ACTION_CANCEL -> {// Long press events are not considered here, simple processing. You can reprocess it if you need to. consumed =true
//                val upX = event.x
//                if (abs(upX - mDownX) < 5) {
//                    performClick()
//                } else{ mListener? .invoke(mRectF.left, mRectF.right, STATE_IDLE, click) // } click = -1 } }return consumed
Copy the code

Then combine the frame list and drag bar. Here I use RecyclerView as the frame list control, and the adapter is passed in by the caller to meet different UI requirements. Create a CutLayout that inherits from FrameLayout. Here’s how to calculate the time: there are two cases where the video length is greater than the maximum and the video length is less than the maximum. When the video length is less than the maximum length, directly calculate how much time t represents for each pixel of the highlighted part of the drag bar, start time =start *t, end time =start + (end-start)*t. When the video length is greater than the maximum length, the frame list can be scrolled, so calculate the unit length of the whole scrollable length, so the start time = the scrollable length *s + start*t end time is the same. Also need to consider is due to the width of the frame list item is type int, and we calculated using float so sometimes when the video time is less than the maximum time frame list didn’t fill the highlighted portion, there will be a few pixels error, this time we need to change the drag bar width, and the calculation for the first time error will decrease. When the video length is larger than the maximum length, the tail needs to be exposed a little. What I use is to change the margin right of the drag bar and add a transparent item to the frame list.

The key calculation codes are as follows:

** @param func needs to add a transparent item */ Fun computeWithDataComplete(itemWidth: Int, func: Runnable) {mrecyclerView.post {var diff = 0f var offset = 0 val cutRange = McUtview.getend () -mcutView.getStart () var width = cutRange var width = cutRange mCutDuration =if (mVideoDuration <= mMaxDuration) {
                mVideoDuration
            } else{// If the video length is greater than the maximum length, drag the control to the right to expose some of the right. // Set recyclerView marginRight to 0, add a transparent item, Will cutView on the right side of the margin is set to the width of the item func. The run () val paramsList = mRecyclerView. LayoutParamsif (paramsList is MarginLayoutParams) {
                    paramsList.rightMargin = 0
                }
                val params = mCutView.layoutParams
                if(params is MarginLayoutParams) {// Change cutView margin, Offset = ItemWidth-mcutView.getrightwidth () params.rightMargin += offset} McUtview.layoutparams = params // If you want to expose the back, // Add a translucent view val layerView = View(context) layerView.setBackgroundColor(Color.parseColor("# 80000000")) val layerViewParams = LayoutParams(offset + mCutView.getRightWidth(), LayoutParams.MATCH_PARENT) val margin = dp2px(context, 3f) layerViewParams.topMargin = margin layerViewParams.bottomMargin = margin layerViewParams.gravity = Gravity.END addViewInLayout(layerView, 1, LayerViewParams) mMaxDuration} val range = mRecyclerView.com puteHorizontalScrollRange () / / because the item width of the frame for int, so will lose some precision, The two are off by a few pixels. So let's subtract it out when we calculate here.if (cutRange > range) {
                val params = mCutView.layoutParams
                if(params is MarginLayoutParams) {// After changing the padding of cutView, Diff = cutrange-range params.rightMargin = diff.toint () width -= diff} McUtview.layoutparams = params} // Calculate the duration of each pixel mPxDuration = mCutDuration/width McUtview.durationpx = mPxDuration MFramePxDuration = mVideoDuration/range.tofloat () Val params2 = LayoutParams(mLineWidth, LayoutParams.MATCH_PARENT) // params2.leftMargin = (mCutView.getStart() - mLineWidth / 2f).toInt() addViewInLayout(mLine, childCount, params2)if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                mLine.elevation = 30f
                mLine.outlineProvider = ViewOutlineProvider.BOUNDS
            }
            mLine.translationX = mCutView.getStart() - mLineWidth / 2f

            computeDuration(
                mCutView.getStart(),
                mCutView.getEnd() - diff - offset,
                STATE_IDLE,
                ORIENTATION_LEFT
            )
            isComplete = true
            mCutView.isEnabled = true
            mRecyclerView.isEnabled = trueRequestLayout ()} // Calculates the offset of the coordinates // Because CutView returns the coordinates of the highlighted part, but maxWidth is subtracted from the pointer width. CutView's width is the same as the parent's width, so this value is actually the same as CutView's leftPadding. But notice that just because the values are the same, the idea of computation is different. private fun computeDuration(left: Float, right: Float) { val startPx = left - mCutView.getLeftWidth() val offset = mRecyclerView.computeHorizontalScrollOffset() var StartMs = (startPx * mPxDuration + offset * mFramePxDuration).tolong () var endMs = (startMs + (right-left) * mPxDuration).toLong()if(endMs > mVideoDuration) { startMs -= (endMs - mVideoDuration) endMs = mVideoDuration } startMs = Math.max(0, startMs) mCutDuration = endMs - startMs mListener? .invoke(startMs, endMs) }Copy the code

Update:

  • Added a progress bar for video playback

Add a View to the original Layout and move it by changing translationX.

Distance moved = (playback time – start time)/unit pixel

/** * @param time updatePlayTime(time: Long) {//if (time < mStartTime)
//            return
        if (time < mStartTime)
            mStartTime = time

        if(! isComplete)return
        val duration = max(time - mStartTime, 0)
        val distance = duration / mPxDuration - mLineWidth / 2f
        setLineLeft(min(mCutView.getStart() + distance, mCutView.getEnd() - mLineWidth / 2f))
    }
Copy the code

In the code above, the distance is subtracted from mLineWidth / 2f, which means half the width of the little vertical line, and here because we need half of the little vertical line to cover the clipping boundary, where the little vertical line is used to indicate the time is in the middle of the little vertical line.

Rewrite Layout onInterceptTouchEvent and onTouchEvent to intercept the event if the pressed coordinate is in the vertical line and handle the vertical line movement yourself by changing translationX

Override fun onInterceptTouchEvent(EV: MotionEvent): Boolean {// If the pressed coordinate is on a vertical line, intercept the eventif (ev.action == MotionEvent.ACTION_DOWN) {
            return if (isContainsDown(ev.x, ev.y)) {
                true
            } else {
                super.onInterceptTouchEvent(ev)
            }
        }
        returnSuper. OnInterceptTouchEvent (ev)} / / drag the vertical bar @ SuppressLint ("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        var consume = false
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                mLastX = event.x
                consume = isContainsDown(event.x, event.y)
                if(consume) { mDragListener? .onDragDown(getProgressTime()) } } MotionEvent.ACTION_MOVE -> { val dx = event.x - mLastX val newMargin = mLine.translationX + dx consume =if(newMargin >= mCutView.getStart() && newMargin <= mCutView.getEnd()) { mLine.translationX = newMargin mLastX = event.x mDragListener? .onDragMove(getProgressTime())true
                } else {
                    false} } MotionEvent.ACTION_UP -> { mDragListener? .onDragUp(getProgressTime()) } }return consume || super.onTouchEvent(event)
    }
Copy the code

Another is to handle the drag cropping box, small vertical line to follow the move. The idea is the same.

All the code is on GitHub