话不多说,先上一个代码完结效果。

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

动图如同录成横屏的了,也没找到调整回转 GIF 的方位,下面再补一张规划稿静态图吧

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

最近这几年音视频使用越来越广泛,随之而来的音视频相关的需求也越来越多,音视频的剪辑也是一些音频软件、视频软件必不可少的功能,假定你的公司需求你做一个音视频修改功能。抛开下层的音视频录制、编解码等作业,上面的音视频修改器你有思路吗?今日咱们带着这个假定出题来一起夯实自界说控件相关的技术。

拆解与剖析

依据规划稿咱们将音视频修改器拆解为:

  • 左侧可移动拖拽的 Min Bar(左滑块)
  • 右侧可移动拖拽的 Max Bar(右滑块)
  • Min Bar 和 Max Bar 中心范围内可移动拖拽的游标
  • 修改器左部的播控按钮(支持 pause、resume)
  • 布景与主体框
  • Min Bar 和 Max Bar 顶部时刻展现
  • 供给给外部调用和播映器绑定的相关 API 与接口

大约便是拆解成这六个块,接下来咱们进行难点剖析

难点剖析

  • Min Bar 和 Max Bar以及中心的游标都是可以拖拽的,所以手势处理是其中的一个点
  • Min Bar 和 Max Bar以及中心的游标存在很近的间隔,这个时分如何判别 touch 目标的优先级
  • 中心的游标单步移动的间隔以及和播映器的 pts 如何绑定,游标在 Min Max 左右防过界处理
  • 物理像素尺度间隔与总进展时刻换算与逆换算
  • 与左右滑块全体的可缩放上下黄色边框

手势事情与 touch 处理

物理像素居理与时刻或百分比换算

  • 从播映器播映时游标移动的单步间隔需求把pts换算成物理像素 X 轴的移动间隔
  • Min Bar 和 Max Bar以及中心游标移动后需求把 物理像素 X 轴的移动间隔换成时刻

所以咱们需求来界说正换算和逆换算的办法如下:

/**
 * 进展值,从百分比到肯定值
 *
 * @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));
    }
}

判别滑块与游标是否在选中范围

这儿需求留意的点是,光标是近乎一根线但是滑块的宽度是较宽的,所以左右滑块是否选中咱们需求取滑块 x 居中的点。

  /**
     * 依据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;
    }

onTouchEvent 事情处理

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

onTouchEvent 的代码量比较大,咱们来别离做下解析解读。首要经过 DOWN 事情咱们确认了各个可呼应事情的优先级,他们优先级如下:

事情呼应优先级

播控按钮 > 游标 > 左右滑块

咱们剖析一下播控按钮(pause 和 resume) 在控件的最左侧,是不会和游标和左右滑块发生 touch 抵触的,所以这个判别顺序放在最前或者最结尾其实是没有影响的、或许发生事情抵触的只有左右滑块和游标,由于游标的开端方位挨着左侧滑块,游标的完毕方位挨着右侧滑块。所以 DOWN 事情中咱们清晰了假如 X 轴方向 touch 把游标优先级给到前置。关于左右滑块的有上文 evalPressedThumb 办法进行判别,实践产品形态的逻辑中,也会限制最短修改时长来限制左右滑块在物理像素中不会贴合到一起。

Move 剖析

在 move 中首要完成的是左右滑块和光标的跟随手势移动,以及外部时刻回传。举个栗子,咱们 touch 游标移动的时分假如需求完成外部播映器的画面帧和音频和我的拖动方位同步,就需求拖动过程中实时把物理像素值换算成时刻(pts)回传给播映器

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

游标越界处理

1 假如咱们手指一直 touch 住游标想把游标带出左右滑块的鸿沟这个如何处理

2 别的一种状况是光标不动,咱们去移动左右滑块,假如不处理这种状况光标会停留在原地跑出到 Min 和 Max 之外

想清楚思路处理起来也比较简略,计划是在 Min 和 Max 方向参加如下逻辑

if (cur <= percentToScreen(mPercentSelectedMinValue)) {//避免光标静态越界
    cur = percentToScreen(mPercentSelectedMinValue);
}
if (cur >= percentToScreen(mPercentSelectedMaxValue)) {//避免光标静态越界
    cur = percentToScreen(mPercentSelectedMaxValue);
}

其他

滑块全体的完成方法

这儿有个细节,咱们看到的左右滑块和顶部底部是一个完好的框,这个怎么完成比较简略? 悉数自己 draw ? 最简略的方法是左右滑块使用规划供给的 icon、上下两个闭合的黄色横梁则自己画。交代部位给个负差就行,这样看上去便是一整个天衣无缝了,完成还简略。上下只需求 drawRect 就行

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

完好源码

好了经过上文,信任没有其他难点可以堵塞到你,这边给出完好的源码,希望对你可以有一点点协助,也欢迎给完成方法供给更优解和纠错。欢迎在下方进行谈论和指点!

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