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

相关推荐
HerayChen21 分钟前
HbuildderX运行到手机或模拟器的Android App基座识别不到设备 mac
android·macos·智能手机
顾北川_野23 分钟前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
hairenjing112325 分钟前
在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序
android·人工智能·windows·macos·智能手机
GIS程序媛—椰子37 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_00143 分钟前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端1 小时前
Content Security Policy (CSP)
前端·javascript·面试
小黄人软件1 小时前
android浏览器源码 可输入地址或关键词搜索 android studio 2024 可开发可改地址
android·ide·android studio
木舟10091 小时前
ffmpeg重复回听音频流,时长叠加问题
前端
王大锤43911 小时前
golang通用后台管理系统07(后台与若依前端对接)
开发语言·前端·golang
dj15402252031 小时前
group_concat配置影响程序出bug
android·bug