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;
    }
}

image.png

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/83407.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Go语言基础之切片

切片 切片&#xff08;Slice&#xff09;是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活&#xff0c;支持自动扩容。 切片是一个引用类型&#xff0c;它的内部结构包含地址、长度和容量。切片一般用于快速地操作一块数据集合 切片的定义…

【简单认识Docker网络管理】

文章目录 一、Docker 网络实现原理二、Docker 的网络模式1.四种网络模式2.各网络模式详解&#xff08;1&#xff09;Host模式&#xff08;2&#xff09;Container模式&#xff08;3&#xff09;None模式&#xff08;4&#xff09;Bridge模式 3.指定容器网络模式4.自定义网络模式…

【力扣】77. 组合 <回溯、回溯剪枝>

目录 【力扣】77. 组合题解回溯回溯法三步剪枝优化 【力扣】77. 组合 给定两个整数 n 和 k&#xff0c;返回范围 [1, n] 中所有可能的 k 个数的组合。你可以按任何顺序返回答案。 示例 1&#xff1a; 输入&#xff1a;n 4, k 2 输出&#xff1a; [[2,4],[3,4],[2,3],[1,2]…

中睿天下受邀参加第六届电力信息通信新技术大会并发表主题演讲

2023年8月9-11日&#xff0c;中国电力企业联合会科技开发服务中心以“加快数字化转型助力新型电力系统建设”为主题&#xff0c;在杭州举办2023年&#xff08;第六届&#xff09;电力信息通信新技术大会暨数字化发展论坛。 大会旨在加快推进“双碳”目标下的新型能源体系和新型…

复合 类型

字符串和切片 切片 切片的作用是允许你引用集合中部分连续的元素序列&#xff0c;而不是引用整个集合。 例如&#xff1a; let s String::from("hello world");let hello &s[0..5]; // 切片 [0,5) 等效于&s[..5] let world &s[6..11]; // 切片…

Python学习 -- 类对象从创建到常用函数

在Python编程中&#xff0c;类是一种强大的工具&#xff0c;用于创建具有共同属性和行为的对象。本篇博客将详细介绍Python中类和对象的创建&#xff0c;类的属性和方法&#xff0c;以及一些常用的类函数&#xff0c;通过丰富的代码例子来帮助读者深入理解。 一、类和对象的创…

STM32设置为I2C从机模式(HAL库版本)

STM32设置为I2C从机模式&#xff08;HAL库版本&#xff09; 目录 STM32设置为I2C从机模式&#xff08;HAL库版本&#xff09;前言1 硬件连接2 软件编程2.1 步骤分解2.2 测试用例 3 运行测试3.1 I2C连续写入3.2 I2C连续读取3.3 I2C单次读写测试 4 总结 前言 我之前出过一篇关于…

AI 绘画Stable Diffusion 研究(十)sd图生图功能详解-精美二维码的制作

免责声明: 本案例所用安装包免费提供&#xff0c;无任何盈利目的。 大家好&#xff0c;我是风雨无阻。 为了让大家更直观的了解图生图功能&#xff0c;明白图生图功能到底是干嘛的&#xff0c;能做什么事情&#xff1f;今天我们继续介绍图生图的实用案例-精美二维码的制作。 对…

倒计时动效

1. 效果 2. html <div class"count"><span>3</span><span>2</span><span>1</span> </div>3. css body {width: 100vw;height: 100vh;overflow: hidden;display: flex;justify-content: center;align-items: cente…

【Apollo】阿波罗自动驾驶技术:引领汽车行业革新

前言 Apollo (阿波罗)是一个开放的、完整的、安全的平台&#xff0c;将帮助汽车行业及自动驾驶领域的合作伙伴结合车辆和硬件系统&#xff0c;快速搭建一套属于自己的自动驾驶系统。 开放能力、共享资源、加速创新、持续共赢是 Apollo 开放平台的口号。百度把自己所拥有的强大、…

iTOP-RK3568开发板ubuntu环境下安装Eclipse

eclipse 是使用 Java 语言开发的&#xff0c;一个 Java 应用程序&#xff0c;这意味着 eclipse 只能运行在 Java虚拟机上。倘若没有安装 JDK&#xff08;Java Development Kit&#xff09;&#xff0c;即使在 ubuntu 上安装了 eclipse&#xff0c;也不能运行&#xff0c;所以要…

fastgpt构建镜像

1.把client目录复制到服务器 .next和node_modules文件夹不用上传到服务器 在服务器目录运行 docker build -t fastgpt:1.0.3 . 构建服务 再运行 docker ps 就可以看到容器了

easyexcel合并单元格底色

一、效果图 二、导出接口代码 PostMapping("selectAllMagicExport")public void selectAllMagicExport(HttpServletRequest request, HttpServletResponse response) throws IOException {ServiceResult<SearchResult<TestMetLineFe2o3Export>> result …

Java虚拟机(JVM):垃圾收集算法

目录 一、分代收集理论 二、标记-清除算法 三、标记-复制算法 四、标记-整理算法 一、分代收集理论 分代收集理论建立在两个分代假说之上&#xff1a; 1、弱分代假说&#xff1a;绝大多数对象都是朝生夕灭的。 2、强分代假说&#xff1a;熬过越多次垃圾收集过程的对象就…

mysql 、sql server trigger 触发器

sql server mySQL create trigger 触发器名称 { before | after } [ insert | update | delete ] on 表名 for each row 触发器执行的语句块## 表名&#xff1a; 表示触发器监控的对象 ## before | after : 表示触发的时间&#xff0c;before : 表示在事件之前触发&am…

JVM的元空间了解吗?

笔者近期在面试的时候被问到了这个问题&#xff0c;元空间也是Java8当时的一大重大革新&#xff0c;之前暑期实习求职的时候有专门看过&#xff0c;但是近期秋招的时候JVM相关的内容确实有点生疏了&#xff0c;故在此进行回顾。 结构 首先&#xff0c;我们应了解JVM的堆结构&a…

深入理解python虚拟机:程序执行的载体——栈帧

栈帧&#xff08;Stack Frame&#xff09;是 Python 虚拟机中程序执行的载体之一&#xff0c;也是 Python 中的一种执行上下文。每当 Python 执行一个函数或方法时&#xff0c;都会创建一个栈帧来表示当前的函数调用&#xff0c;并将其压入一个称为调用栈&#xff08;Call Stac…

docker学习(十五)docker安装MongoDB

什么是MongoDB? MongoDB 是一个开源的、面向文档的 NoSQL 数据库管理系统&#xff0c;它以高性能、灵活的数据存储方式而闻名。与传统的关系型数据库不同&#xff0c;MongoDB 采用了一种称为 BSON&#xff08;Binary JSON&#xff09;的二进制 JSON 格式来存储数据。它是一种非…

(详解踩坑)GIT版本回滚git stash、git reset、git reset --hard、git revert

目录 背景 一、&#xff08;git log、git reflog&#xff09;查看git提交日志及命令历史 1.1 git log&#xff08;提交日志&#xff09; 1.2 git reflog&#xff08;命令历史&#xff09; 二、git reset&#xff08;回退到指定的版本&#xff0c;并且保留更改&#xff09; …

IDEA启动报错【java.sql.SQLSyntaxErrorException: ORA-00904: “P“.“PRJ_NO“: 标识符无效】

IDEA报错如下&#xff1a; 2023-08-17 11:26:15.535 ERROR [egrant-biz,b48324d82fe23753,b48324d82fe23753,true] 24108 --- [ XNIO-1 task-1] c.i.c.l.c.RestExceptionController : 服务器异常org.springframework.jdbc.BadSqlGrammarException: ### Error queryin…