Android音视频剪辑器自定义View实战!

Android音视频剪辑器自定义View实战! - 掘金

/**
 * Created by zhouxuming on 2023/3/30
 *
 * @descr 音视频剪辑器
 */
public class AudioViewEditor extends View {
    //进度文本显示格式-数字格式
    public static final int HINT_FORMAT_NUMBER = 0;
    //进度文本显示格式-时间格式
    public static final int HINT_FORMAT_TIME = 1;
    private final Paint mPaint = new Paint();
    //空间最小宽度
    private final int MIN_WIDTH = 200;
    private final float playControlLeft = 10; //播控实际左边界
    private final float playControlRight = 80; //播控实际右边界
    //滑块bitmap
    private Bitmap mThumbImage;
    //progress bar 选中背景
//    private Bitmap mProgressBarSelBg;
    private Bitmap mMaxThumbImage;
    private Bitmap mMinThumbImage;
    //progress bar 背景
    private Bitmap mProgressBarBg;
    private float mThumbWidth;
    private float mThumbHalfWidth; //触摸响应宽度的一半
    private float mThumbHalfHeight;
    //seekbar 进度条高度
    private float mProgressBarHeight;
    //宽度左右padding
    private float mWidthPadding;
    //最小值(绝对)
    private float mAbsoluteMinValue;
    //最大值(绝对)
    private float mAbsoluteMaxValue;
    //已选标准(占滑动条百分比)最小值
    private double mPercentSelectedMinValue = 0d;
    //已选标准(占滑动条百分比)最大值
    private double mPercentSelectedMaxValue = 1d;
    //当前事件处理的thumb滑块
    private Thumb mPressedThumb = null;
    //滑块事件
    private ThumbListener mThumbListener;
    private RectF mProgressBarRect;
    private RectF mProgressBarSelRect;
    //是否可以滑动
    private boolean mIsEnable = true;
    //最大值和最小值之间要求的最小范围绝对值
    private float mBetweenAbsoluteValue;
    //文字格式
    private int mProgressTextFormat;
    //文本高度
    private int mWordHeight;
    //文本字体大小
    private float mWordSize;
    private float mStartMinPercent;
    private float mStartMaxPercent;
    private boolean fixedMode; //固定模式
    private Paint cursorPaint;
    private Paint borderPaint;
    //播控按钮部分逻辑
    private Paint playControlPaint;
    private boolean isPlay = true; //播控状态
    private Bitmap playResumeBitmap;
    private Bitmap playPauseBitmap;
    private PlayerControlListener playerControlListener;


    private float cur;// 光标坐标
    private float pre;// 100 份每一份的偏移量
    private float min;//起始坐标
    private float max;//最大坐标
    private boolean isFirst = true;


    public AudioViewEditor(Context context) {
        super(context);
    }

    public AudioViewEditor(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.AudioViewEditor, 0, 0);
        mAbsoluteMinValue = a.getFloat(R.styleable.AudioViewEditor_absoluteMin, (float) 0.0);
        mAbsoluteMaxValue = a.getFloat(R.styleable.AudioViewEditor_absolutemMax, (float) 100.0);
        mStartMinPercent = a.getFloat(R.styleable.AudioViewEditor_startMinPercent, 0);
        mStartMaxPercent = a.getFloat(R.styleable.AudioViewEditor_startMaxPercent, 1);
        mThumbImage = BitmapFactory.decodeResource(getResources(), a.getResourceId(R.styleable.AudioViewEditor_thumbImage, R.drawable.drag_left_bar));
        mMaxThumbImage = BitmapFactory.decodeResource(getResources(), R.drawable.drag_right_bar);
        mProgressBarBg = BitmapFactory.decodeResource(getResources(), a.getResourceId(R.styleable.AudioViewEditor_progressBarBg, R.drawable.seekbar_bg));

        playPauseBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.play_control_pause);
        playResumeBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.play_control_resume);

//        mProgressBarSelBg = BitmapFactory.decodeResource(getResources(), a.getResourceId(R.styleable.CustomRangeSeekBar_progressBarSelBg, R.mipmap.seekbar_sel_bg));

        mBetweenAbsoluteValue = a.getFloat(R.styleable.AudioViewEditor_betweenAbsoluteValue, 0);
        mProgressTextFormat = a.getInt(R.styleable.AudioViewEditor_progressTextFormat, HINT_FORMAT_NUMBER);
        mWordSize = a.getDimension(R.styleable.AudioViewEditor_progressTextSize, dp2px(context, 16));
        mPaint.setTextSize(mWordSize);
        mThumbWidth = mThumbImage.getWidth();
        mThumbHalfWidth = 0.5f * mThumbWidth;
        mThumbHalfHeight = 0.5f * mThumbImage.getHeight();
//        mProgressBarHeight = 0.3f * mThumbHalfHeight;

        mProgressBarHeight = mThumbImage.getHeight();

        //TOOD 提供定义attr
        mWidthPadding = mThumbHalfHeight;
        mWidthPadding += playControlRight;//为了加左右侧播控按钮, 特地添加出来的空间
        Paint.FontMetrics metrics = mPaint.getFontMetrics();
        mWordHeight = (int) (metrics.descent - metrics.ascent);

        /*光标*/
        cursorPaint = new Paint();
        cursorPaint.setAntiAlias(true);
        cursorPaint.setColor(Color.WHITE);

        borderPaint = new Paint();
        borderPaint.setAntiAlias(true);
        borderPaint.setColor(Color.parseColor("#DBAE6A"));

        playControlPaint = new Paint();
        playControlPaint.setAntiAlias(true);
        playControlPaint.setColor(Color.parseColor("#1E1F21"));

        restorePercentSelectedMinValue();
        restorePercentSelectedMaxValue();

        a.recycle();
    }

    /**
     * 格式化毫秒->00:00
     */
    private static String formatSecondTime(int millisecond) {
        if (millisecond == 0) {
            return "00:00";
        }
        int second = millisecond / 1000;
        int m = second / 60;
        int s = second % 60;
        if (m >= 60) {
            int hour = m / 60;
            int minute = m % 60;
            return hour + ":" + (minute > 9 ? minute : "0" + minute) + ":" + (s > 9 ? s : "0" + s);
        } else {
            return (m > 9 ? m : "0" + m) + ":" + (s > 9 ? s : "0" + s);
        }
    }

    /**
     * 将dip或dp值转换为px值,保证尺寸大小不变
     *
     * @param dipValue (DisplayMetrics类中属性density)
     * @return
     */
    public static int dp2px(Context context, float dipValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }

    /**
     * 还原min滑块到初始值
     */
    public void restorePercentSelectedMinValue() {
        setPercentSelectedMinValue(mStartMinPercent);
    }

    /**
     * 还原max滑块到初始值
     */
    public void restorePercentSelectedMaxValue() {
        setPercentSelectedMaxValue(mStartMaxPercent);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mProgressBarRect = new RectF(mWidthPadding, mWordHeight + 0.5f * (h - mWordHeight - mProgressBarHeight),
                w - mWidthPadding, mWordHeight + 0.5f * (h - mWordHeight + mProgressBarHeight));
        mProgressBarSelRect = new RectF(mProgressBarRect);

    }

    /**
     * 设置seekbar 是否接收事件
     *
     * @param enabled
     */
    @Override
    public void setEnabled(boolean enabled) {
        super.setEnabled(enabled);
        this.mIsEnable = enabled;
    }

    /**
     * 返回被选择的最小值(绝对值)
     *
     * @return The currently selected min value.
     */
    public float getSelectedAbsoluteMinValue() {
        return percentToAbsoluteValue(mPercentSelectedMinValue);
    }

    /**
     * 设置被选择的最小值(绝对值)
     *
     * @param value 最小值的绝对值
     *              return 如果最小值与最大值的最小间距达到阈值返回false,正常返回true
     */
    public boolean setSelectedAbsoluteMinValue(float value) {
        boolean status = true;
        if (0 == (mAbsoluteMaxValue - mAbsoluteMinValue)) {
            setPercentSelectedMinValue(0d);
        } else {
            float maxValue = percentToAbsoluteValue(mPercentSelectedMaxValue);
            if (mBetweenAbsoluteValue > 0 && maxValue - value <= mBetweenAbsoluteValue) {
                value = new Float(maxValue - mBetweenAbsoluteValue);
                status = false;
            }
            if (maxValue - value <= 0) {
                status = false;
                value = maxValue;
            }
            setPercentSelectedMinValue(absoluteValueToPercent(value));
        }
        return status;
    }

    public float getAbsoluteMaxValue() {
        return mAbsoluteMaxValue;
    }

    public void setAbsoluteMaxValue(double maxvalue) {
        this.mAbsoluteMaxValue = new Float(maxvalue);
    }

    /**
     * 返回被选择的最大值(绝对值).
     */
    public float getSelectedAbsoluteMaxValue() {
        return percentToAbsoluteValue(mPercentSelectedMaxValue);
    }

    /**
     * 设置被选择的最大值(绝对值)
     *
     * @param value
     */
    public boolean setSelectedAbsoluteMaxValue(float value) {
        boolean status = true;
        if (0 == (mAbsoluteMaxValue - mAbsoluteMinValue)) {
            setPercentSelectedMaxValue(1d);
        } else {
            float minValue = percentToAbsoluteValue(mPercentSelectedMinValue);
            if (mBetweenAbsoluteValue > 0 && value - minValue <= mBetweenAbsoluteValue) {
                value = new Float(minValue + mBetweenAbsoluteValue);
                status = false;
            }
            if (value - minValue <= 0) {
                status = false;
                value = minValue;
            }
            setPercentSelectedMaxValue(absoluteValueToPercent(value));
        }
        return status;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!mIsEnable)
            return true;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (isTouchPlayControl(event.getX())) {
                    isPlay = !isPlay;
                    playerControlListener.onPlayerControl(isPlay);
                    invalidate();
                    return true;
                }
                if (mPressedThumb == null && isInCursorRange(event.getX(), cur)) {
//                    if (mThumbListener != null){
//                        mThumbListener.onCursor(cur);
//                    }
                } else {
                    mPressedThumb = evalPressedThumb(event.getX());
                    if (Thumb.MIN.equals(mPressedThumb)) {
                        if (mThumbListener != null)
                            mThumbListener.onClickMinThumb(getSelectedAbsoluteMaxValue(), getSelectedAbsoluteMinValue());
                    }
                    if (Thumb.MAX.equals(mPressedThumb)) {
                        if (mThumbListener != null)
                            mThumbListener.onClickMaxThumb();
                    }
                }
                invalidate();
                //Intercept parent TouchEvent
                if (getParent() != null) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (mPressedThumb == null && isInCursorRange(event.getX(), cur)) {
                    isMoving = true;

                    float eventX = event.getX();

                    if (eventX >= percentToScreen(mPercentSelectedMaxValue)) {
                        eventX = percentToScreen(mPercentSelectedMaxValue);
                    } else if (eventX <= percentToScreen(mPercentSelectedMinValue)) {
                        eventX = percentToScreen(mPercentSelectedMinValue);
                    }

                    cur = eventX;
                    if (mThumbListener != null) {
                        mThumbListener.onCursorMove(percentToAbsoluteValue(screenToPercent(cur)));
                    }
                    invalidate();
                } else if (mPressedThumb != null) {
                    float eventX = event.getX();
                    float maxValue = percentToAbsoluteValue(mPercentSelectedMaxValue);
                    float minValue = percentToAbsoluteValue(mPercentSelectedMinValue);
                    float eventValue = percentToAbsoluteValue(screenToPercent(eventX));

                    if (Thumb.MIN.equals(mPressedThumb)) {
                        minValue = eventValue;
                        if (mBetweenAbsoluteValue > 0 && maxValue - minValue <= mBetweenAbsoluteValue) {
                            minValue = new Float((maxValue - mBetweenAbsoluteValue));
                        }
//                        setPercentSelectedMinValue(screenToPercent(event.getX()));

                        if (isFixedMode()) {
                            mPercentSelectedMaxValue = Math.max(0d, Math.min(1d, Math.max(absoluteValueToPercent(eventValue + (maxValue - minValue)), mPercentSelectedMinValue)));
                        }

                        if (cur <= percentToScreen(mPercentSelectedMinValue)) {//防止光标静态越界
                            cur = percentToScreen(mPercentSelectedMinValue);
                        }

                        setPercentSelectedMinValue(absoluteValueToPercent(minValue));
                        if (mThumbListener != null)
                            mThumbListener.onMinMove(getSelectedAbsoluteMaxValue(), getSelectedAbsoluteMinValue());
                    } else if (Thumb.MAX.equals(mPressedThumb)) {
                        maxValue = eventValue;
                        if (mBetweenAbsoluteValue > 0 && maxValue - minValue <= mBetweenAbsoluteValue) {
                            maxValue = new Float(minValue + mBetweenAbsoluteValue);
                        }
//                        setPercentSelectedMaxValue(screenToPercent(event.getX()));

                        if (isFixedMode()) {
                            mPercentSelectedMinValue = Math.max(0d, Math.min(1d, Math.min(absoluteValueToPercent(eventValue - (maxValue - minValue)), mPercentSelectedMaxValue)));
                        }

                        if (cur >= percentToScreen(mPercentSelectedMaxValue)) {//防止光标静态越界
                            cur = percentToScreen(mPercentSelectedMaxValue);
                        }

                        setPercentSelectedMaxValue(absoluteValueToPercent(maxValue));
                        if (mThumbListener != null)
                            mThumbListener.onMaxMove(getSelectedAbsoluteMaxValue(), getSelectedAbsoluteMinValue());
                    }
                }
                //Intercept parent TouchEvent
                if (getParent() != null) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_UP:
                if (isMoving) {
                    if (mThumbListener != null) {
                        mThumbListener.onCursorUp(percentToAbsoluteValue(screenToPercent(cur)));
                    }
                    isMoving = false;
                }

                if (Thumb.MIN.equals(mPressedThumb)) {
                    if (mThumbListener != null)
                        mThumbListener.onUpMinThumb(getSelectedAbsoluteMaxValue(), getSelectedAbsoluteMinValue());
                }
                if (Thumb.MAX.equals(mPressedThumb)) {
                    if (mThumbListener != null)
                        mThumbListener.onUpMaxThumb(getSelectedAbsoluteMaxValue(), getSelectedAbsoluteMinValue());
                }
                //Intercept parent TouchEvent
                if (getParent() != null) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                if (Thumb.MIN.equals(mPressedThumb)) {
                    if (mThumbListener != null)
                        mThumbListener.onUpMinThumb(getSelectedAbsoluteMaxValue(), getSelectedAbsoluteMinValue());
                }
                if (Thumb.MAX.equals(mPressedThumb)) {
                    if (mThumbListener != null)
                        mThumbListener.onUpMaxThumb(getSelectedAbsoluteMaxValue(), getSelectedAbsoluteMinValue());
                }
                mPressedThumb = null;
                //Intercept parent TouchEvent
                if (getParent() != null) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
        }
        return true;
    }

    private boolean isTouchPlayControl(float eventX) {
        if (eventX > playControlLeft && eventX < playControlRight) {
            return true;
        }
        return false;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MIN_WIDTH;
        if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(widthMeasureSpec)) {
            width = MeasureSpec.getSize(widthMeasureSpec);
        }
        int height = mThumbImage.getHeight() + mWordHeight * 2;
        if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(heightMeasureSpec)) {
            height = Math.min(height, MeasureSpec.getSize(heightMeasureSpec));
        }
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // draw seek bar background line
        mPaint.setStyle(Paint.Style.FILL);
        drawPlayControl(canvas);
        canvas.drawBitmap(mProgressBarBg, null, mProgressBarRect, mPaint);
        // draw seek bar active range line
        mProgressBarSelRect.left = percentToScreen(mPercentSelectedMinValue);
        mProgressBarSelRect.right = percentToScreen(mPercentSelectedMaxValue);
        //canvas.drawBitmap(mProgressBarSelBg, mWidthPadding, 0.5f * (getHeight() - mProgressBarHeight), mPaint);

//        canvas.drawBitmap(mProgressBarSelBg, null, mProgressBarSelRect, mPaint); //原中部选中进度

        // draw minimum thumb
        drawThumb(percentToScreen(mPercentSelectedMinValue), Thumb.MIN.equals(mPressedThumb), canvas, false);
        // draw maximum thumb
        drawThumb(percentToScreen(mPercentSelectedMaxValue), Thumb.MAX.equals(mPressedThumb), canvas, true);
        mPaint.setColor(Color.rgb(255, 165, 0));
        mPaint.setAntiAlias(true);
//        mPaint.setTextSize(DensityUtils.dp2px(getContext(), 16));
        drawThumbMinText(percentToScreen(mPercentSelectedMinValue), getSelectedAbsoluteMinValue(), canvas);
        drawThumbMaxText(percentToScreen(mPercentSelectedMaxValue), getSelectedAbsoluteMaxValue(), canvas);
        drawBorder(canvas);
        drawCursor(canvas);
    }

    private void drawPlayControl(Canvas canvas) {
        canvas.drawRoundRect(playControlLeft, mProgressBarRect.top, playControlRight + mThumbWidth + mThumbHalfWidth, mProgressBarRect.bottom, 5, 5, playControlPaint);

        Bitmap targetBitmap = isPlay ? playPauseBitmap : playResumeBitmap;
        //x轴距离未计算准确 y轴正确
        canvas.drawBitmap(targetBitmap, (playControlLeft + (playControlRight - playControlLeft) / 2) - mThumbHalfWidth + (targetBitmap.getWidth() >> 1), mProgressBarRect.top + (mProgressBarRect.bottom - mProgressBarRect.top) / 2 - (targetBitmap.getHeight() >> 1), playControlPaint);
    }

    private void drawBorder(Canvas canvas) {
        //top
        float borderLeft = mProgressBarSelRect.left;
        float borderRight = mProgressBarSelRect.right;
        canvas.drawRect(borderLeft - 1, mProgressBarRect.top, borderRight + 1, mProgressBarRect.top + 10, borderPaint);
        //bottom
        canvas.drawRect(borderLeft - 1, mProgressBarRect.bottom, borderRight + 1, mProgressBarRect.bottom - 10, borderPaint);
    }

    private void drawCursor(Canvas canvas) {
        min = percentToScreen(mPercentSelectedMinValue);//开始坐标
        max = percentToScreen(mPercentSelectedMaxValue);//终点坐标
        pre = (getWidth() - 2 * mWidthPadding) / 1000; //每一份的坐标
        if (isFirst) {
            cur = min;
            isFirst = false;
        }
        canvas.drawRect(cur - 2, mProgressBarRect.top + 5, cur + 2, mProgressBarRect.bottom - 5, cursorPaint);
    }

    //启动播放线程检查 pts
    public void startMove() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (isPlay) {
                        long pts = playerCallback != null ? playerCallback.getCurrentPosition() : 0;
                        updatePTS(pts);
                    }
                    try {
                        Thread.sleep(30);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    /**
     * 根据播放器 pts 控制游标进度
     *
     * @param pts
     */
    public void updatePTS(float pts) {
        if (isMoving) {
            return;
        }
        if (pts > 0) {
            double v = absoluteValueToPercent(pts);
            cur = percentToScreen(v);
            if (cur >= max || cur < min) {
                cur = min;
            }
            invalidate();
        }
    }

    public boolean isPlay() {
        return isPlay;
    }

    public void setPlay(boolean play) {
        isPlay = play;
    }

    private boolean isMoving = false;

    @Override
    protected Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        bundle.putParcelable("SUPER", super.onSaveInstanceState());
        bundle.putDouble("MIN", mPercentSelectedMinValue);
        bundle.putDouble("MAX", mPercentSelectedMaxValue);
        return bundle;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable parcel) {
        Bundle bundle = (Bundle) parcel;
        super.onRestoreInstanceState(bundle.getParcelable("SUPER"));
        mPercentSelectedMinValue = bundle.getDouble("MIN");
        mPercentSelectedMaxValue = bundle.getDouble("MAX");
    }

    /**
     * Draws the "normal" resp. "pressed" thumb image on specified x-coordinate.
     *
     * @param screenCoord The x-coordinate in screen space where to draw the image.
     * @param pressed     Is the thumb currently in "pressed" state?
     * @param canvas      The canvas to draw upon.
     */
    private void drawThumb(float screenCoord, boolean pressed, Canvas canvas, boolean isMax) {
        //基准点 bar 居中位置
//        canvas.drawBitmap(isMax ? mMaxThumbImage : mThumbImage, screenCoord - mThumbHalfWidth, (mWordHeight + 0.5f * (getHeight() - mWordHeight) - mThumbHalfHeight), mPaint);
        //基准点顶两边位置
        canvas.drawBitmap(isMax ? mMaxThumbImage : mThumbImage, isMax ? screenCoord : screenCoord - mThumbHalfWidth * 2, (mWordHeight + 0.5f * (getHeight() - mWordHeight) - mThumbHalfHeight), mPaint);
    }

    /**
     * 画min滑块值text
     *
     * @param screenCoord
     * @param value
     * @param canvas
     */
    private void drawThumbMinText(float screenCoord, Number value, Canvas canvas) {
        String progress = getProgressStr(value.intValue());
        float progressWidth = mPaint.measureText(progress);
        canvas.drawText(progress, (screenCoord - progressWidth / 2) - mThumbHalfWidth, mWordSize, mPaint);
    }

    /**
     * 画max滑块值text
     *
     * @param screenCoord
     * @param value
     * @param canvas
     */
    private void drawThumbMaxText(float screenCoord, Number value, Canvas canvas) {
        String progress = getProgressStr(value.intValue());
        float progressWidth = mPaint.measureText(progress);
        canvas.drawText(progress, (screenCoord - progressWidth / 2) + mThumbHalfWidth, mWordSize
                , mPaint);
    }

    /**
     * 根据touchX, 判断是哪一个thumb(Min or Max)
     *
     * @param touchX 触摸的x在屏幕中坐标(相对于容器)
     */
    private Thumb evalPressedThumb(float touchX) {
        Thumb result = null;
        boolean minThumbPressed = isInThumbRange(touchX, mPercentSelectedMinValue, false);
        boolean maxThumbPressed = isInThumbRange(touchX, mPercentSelectedMaxValue, true);
        if (minThumbPressed && maxThumbPressed) {
            // if both thumbs are pressed (they lie on top of each other), choose the one with more room to drag. this avoids "stalling" the thumbs in a corner, not being able to drag them apart anymore.
            result = (touchX / getWidth() > 0.5f) ? Thumb.MIN : Thumb.MAX;
        } else if (minThumbPressed) {
            result = Thumb.MIN;
        } else if (maxThumbPressed) {
            result = Thumb.MAX;
        }
        return result;
    }

    /**
     * 判断touchX是否在滑块点击范围内
     *
     * @param touchX            需要被检测的 屏幕中的x坐标(相对于容器)
     * @param percentThumbValue 需要检测的滑块x坐标百分比值(滑块x坐标)
     */
    private boolean isInThumbRange(float touchX, double percentThumbValue, boolean isMax) {
        if (isMax) {
            return Math.abs(touchX - mThumbHalfWidth - percentToScreen(percentThumbValue)) <= mThumbHalfWidth;
        } else {
            return Math.abs(touchX + mThumbHalfWidth - percentToScreen(percentThumbValue)) <= mThumbHalfWidth;
        }
//        return Math.abs(touchX - percentToScreen(percentThumbValue)) <= mThumbHalfWidth; //居中基准时
    }

    /**
     * 判断用户是否触碰光标
     *
     * @param touchX  需要被检测的 屏幕中的x坐标(相对于容器)
     * @param cursorX 光标x坐标(滑块x坐标)
     * @return
     */
    private boolean isInCursorRange(float touchX, float cursorX) {
        return Math.abs(touchX - cursorX) <= mThumbHalfWidth;
    }

    /**
     * 设置已选择最小值的百分比值
     */
    public void setPercentSelectedMinValue(double value) {
        mPercentSelectedMinValue = Math.max(0d, Math.min(1d, Math.min(value, mPercentSelectedMaxValue)));
        invalidate();
    }

    /**
     * 设置已选择最大值的百分比值
     */
    public void setPercentSelectedMaxValue(double value) {
        mPercentSelectedMaxValue = Math.max(0d, Math.min(1d, Math.max(value, mPercentSelectedMinValue)));
        invalidate();
    }

    /**
     * 进度值,从百分比到绝对值
     *
     * @return
     */
    @SuppressWarnings("unchecked")
    private float percentToAbsoluteValue(double normalized) {
        return (float) (mAbsoluteMinValue + normalized * (mAbsoluteMaxValue - mAbsoluteMinValue));
    }

    /**
     * 进度值,从绝对值到百分比
     */
    private double absoluteValueToPercent(float value) {
        if (0 == mAbsoluteMaxValue - mAbsoluteMinValue) {
            // prevent division by zero, simply return 0.
            return 0d;
        }
        return (value - mAbsoluteMinValue) / (mAbsoluteMaxValue - mAbsoluteMinValue);
    }

    /**
     * 进度值,从百分比值转换到屏幕中坐标值
     */
    private float percentToScreen(double percentValue) {
        return (float) (mWidthPadding + percentValue * (getWidth() - 2 * mWidthPadding));
    }

    /**
     * 进度值,转换屏幕像素值到百分比值
     */
    private double screenToPercent(float screenCoord) {
        int width = getWidth();
        if (width <= 2 * mWidthPadding) {
            // prevent division by zero, simply return 0.
            return 0d;
        } else {
            double result = (screenCoord - mWidthPadding) / (width - 2 * mWidthPadding);
            return Math.min(1d, Math.max(0d, result));
        }
    }

    public void setThumbListener(ThumbListener mThumbListener) {
        this.mThumbListener = mThumbListener;
    }

    private String getProgressStr(int progress) {
        String progressStr;
        if (mProgressTextFormat == HINT_FORMAT_TIME) {
            progressStr = formatSecondTime(progress);
        } else {
            progressStr = String.valueOf(progress);
        }
        return progressStr;
    }

    public boolean isFixedMode() {
        return fixedMode;
    }

    public void setFixedMode(boolean fixedMode) {
        this.fixedMode = fixedMode;
    }

    /**
     * 重置总时长-单位秒
     *
     * @param totalSecond
     */
    public void resetTotalSecond(int totalSecond) {
        if (totalSecond > 0 && totalSecond < 12000) {
            mAbsoluteMaxValue = totalSecond * 1000;
            mAbsoluteMinValue = 0.0f;
            mProgressTextFormat = HINT_FORMAT_TIME;
            invalidate();
        }
    }

    /**
     * 重置总时长-单位毫秒
     *
     * @param totalMillisecond
     */
    public void resetTotalMillisecond(float totalMillisecond) {
        if (totalMillisecond > 0 && totalMillisecond < 1200000) {
            mAbsoluteMaxValue = totalMillisecond;
            mAbsoluteMinValue = 0.0f;
            mProgressTextFormat = HINT_FORMAT_TIME;
            invalidate();
        }
    }

    public void setPlayerControlListener(PlayerControlListener playerControlListener) {
        this.playerControlListener = playerControlListener;
    }

    /**
     * Thumb枚举, 最大或最小
     */
    private enum Thumb {
        MIN, MAX
    }

    public interface PlayerControlListener {
        void onPlayerControl(boolean isPlay);
    }


    /**
     * 游标以及滑块事件
     */
    public interface ThumbListener {
        void onClickMinThumb(Number max, Number min);

        void onClickMaxThumb();

        void onUpMinThumb(Number max, Number min);

        void onUpMaxThumb(Number max, Number min);

        void onMinMove(Number max, Number min);

        void onMaxMove(Number max, Number min);

        void onCursorMove(Number cur);

        void onCursorUp(Number cur);
    }

    public interface IPlayerCallback {
        long getCurrentPosition();
    }

    private IPlayerCallback playerCallback = null;

    public void setPlayerCallback(IPlayerCallback playerCallback) {
        this.playerCallback = playerCallback;
    }

    public void release() {
        isPlay = false;
    }
}

Android音视频剪辑器自定义View实战! - 掘金话不多说,先上一个代码完成效果。 动图好像录成横屏的了,也没找到调整反转 GIF 的位置,下面再补一张设计稿静态图吧 最近这几年音视频应用越来越广泛,随之而来的音视频相关的需求也越来越多,音视频的剪辑https://juejin.cn/post/7236635197071802424?utm_source=gold_browser_extension#heading-10

相关推荐
坚定信念,勇往无前6 分钟前
uni-app运行 安卓模拟器 MuMu模拟器
android·uni-app
ZZZCY200314 分钟前
路由策略与路由控制实验
前端·网络
shawya_void23 分钟前
javaweb-day01-html和css初识
前端·css·html
khatung23 分钟前
React——useReducer
前端·javascript·vscode·react.js·前端框架·1024程序员节
思考的橙子26 分钟前
CSS之3D转换
前端·css·3d
木子七1 小时前
vue3-setup中使用响应式
前端·vue
廖子默1 小时前
提供html2canvas+jsPDF将HTML页面以A4纸方式导出为PDF后,内容分页时存在截断的解决思路
前端·pdf·html
Moment1 小时前
毕业半年,终于拥有了两个近 500 star 的开源项目了 🤭🤭🤭
前端·后端·开源
光影少年2 小时前
react和vue图片懒加载及实现原理
前端·vue.js·react.js
AndyGoWei2 小时前
react react-router-dom history 实现原理,看这篇就够了
前端·javascript·react.js