第三周总结

时间:2019 年 01 月 14 日 ~ 2019 年 01 月 18 日

一、自定义水平进度条

效果截图:
LineProgress 运行截图

public class LineProgress extends View {

    private Paint mOriginalPaint; // 进度条中未拖动部分的画笔
    private Paint mProgressPaint; // 进度条中已拖动部分的画笔
    private Paint mCircleDragPaint; // 拖动圆画笔
    private Paint mProgressTipPaint; // 进度条进度提示文字画笔

    private int mMaxProgress = 100; // 进度条最大进度
    private int mCurProgress; // 进度条已拖动进度值

    private int mProgressDefaultColor = Color.parseColor("#F0F0F0"); // 进度条中未拖动部分的颜色
    private int mProgressDragColor = Color.parseColor("#0DE6C2"); // 进度条中已拖动部分的颜色

    private int mCircleRadius = dp2px(12); // 拖动圆半径
    private int mCircleDragColor = Color.RED; // 拖动圆颜色

    private int mWidth; // 进度条宽度
    private int mHeight; // 进度条高度
    private int mPaddingLeft; // 进度条左 Padding
    private int mPaddingRight; // 进度条右 Padding
    private float ratio; // 当前进度和最大进度的比值
    private int mTouchX; // 手指在进度条上点击的横坐标
    private int circle_x; // 拖动圆的圆心横坐标

    private boolean isTouch; // 当前是否在触摸进度条

    public LineProgress(Context context) {
        this(context, null);
    }

    public LineProgress(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LineProgress(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        // 获取自定义属性
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LineProgress);
        mProgressDefaultColor = ta.getColor(R.styleable.LineProgress_progress_default_color, mProgressDefaultColor);
        mProgressDragColor = ta.getColor(R.styleable.LineProgress_progress_drag_color, mProgressDragColor);
        mCircleDragColor = ta.getColor(R.styleable.LineProgress_progress_circle_drag_color, mCircleDragColor);
        mMaxProgress = ta.getInt(R.styleable.LineProgress_progress_max_value, mMaxProgress);
        ta.recycle();

        // 初始化画笔
        initPaint();
    }

    /**
     * 初始化画笔
     */
    private void initPaint() {
        // 初始化进度条背景画笔
        mOriginalPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mOriginalPaint.setColor(mProgressDefaultColor);
        mOriginalPaint.setStyle(Paint.Style.FILL_AND_STROKE); // 填充且描边
        mOriginalPaint.setStrokeCap(Paint.Cap.ROUND); // 圆头
        mOriginalPaint.setStrokeWidth(dp2px(3));

        // 初始化进度条画笔
        mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mProgressPaint.setColor(mProgressDragColor);
        mProgressPaint.setStyle(Paint.Style.FILL_AND_STROKE); // 填充且描边
        mProgressPaint.setStrokeCap(Paint.Cap.ROUND); // 圆头
        mProgressPaint.setStrokeWidth(dp2px(3));

        // 拖动圆点画笔
        mCircleDragPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCircleDragPaint.setColor(mCircleDragColor);
        mCircleDragPaint.setStyle(Paint.Style.FILL);

        // 进度条进度提示画笔
        mProgressTipPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mProgressTipPaint.setColor(mCircleDragColor);
        mProgressTipPaint.setTextSize(sp2px(12));
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int wm = MeasureSpec.getMode(widthMeasureSpec);
        int ws = MeasureSpec.getSize(widthMeasureSpec);
        // 高度模式
        int mode = MeasureSpec.getMode(heightMeasureSpec);
        // 高度大小
        int size = MeasureSpec.getSize(heightMeasureSpec);
        int hs;
        if (mode == MeasureSpec.EXACTLY) {
            hs = size;
        } else { // 没有指定具体值
            Rect bounds = new Rect();
            mProgressTipPaint.getTextBounds(mCurProgress + "", 0, (mCurProgress + "").length(), bounds);
            int dy = (bounds.bottom - bounds.top) / 2 - bounds.bottom;
            // 高度设置为能放下进度文字提示即可,根据文字大小去准确计算高度
            hs = mCircleRadius * 2 + bounds.height() * 2 + dy;
        }

        if (wm == MeasureSpec.AT_MOST) {
            ws = 200;
        }
        setMeasuredDimension(ws, hs);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        // 获取宽高
        mWidth = getWidth();
        mHeight = getHeight();
        // 获取左右 Padding 值
        mPaddingLeft = getPaddingLeft();
        mPaddingRight = getPaddingRight();
        // 修正拖动点圆心和左右 Padding 的大小关系
        if (mPaddingLeft != mCircleRadius) {
            mPaddingLeft = mCircleRadius;
        }
        if (mPaddingRight != mCircleRadius) {
            mPaddingRight = mCircleRadius;
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (null != mOnProgressListener) {
            mOnProgressListener.onDrag(mCurProgress);
        }
        // 画背景线
        canvas.drawLine(mPaddingLeft, mHeight - mCircleRadius, mWidth - mPaddingRight, mHeight - mCircleRadius, mOriginalPaint);
        // 画进度条(当 ratio 为 0 时,代表初始状态,即为拖动,此时拖动颜色结束点的 X 坐标应该向右移动 mPaddingLeft 的距离)
        int progress_x = (int) (mWidth * ratio);
        if (mWidth - progress_x < mCircleRadius) {
            progress_x = mWidth - mPaddingRight;
        }
        canvas.drawLine(mPaddingLeft, mHeight - mCircleRadius, progress_x, mHeight - mCircleRadius, mProgressPaint);
        if (isTouch) {
            if (mTouchX < mCircleRadius) {
                circle_x = mCircleRadius;
            } else if (mWidth - mTouchX < mCircleRadius) {
                circle_x = mWidth - mCircleRadius;
            } else {
                circle_x = (int) (mWidth * ratio);
            }
        } else {
            int circle_dx = 0;
            if (ratio == 0) {
                circle_dx = mPaddingLeft;
            }
            circle_x = (int) (mWidth * ratio) + circle_dx;
        }
        // 画拖动圆
        canvas.drawCircle(circle_x, mHeight - mCircleRadius, mCircleRadius, mCircleDragPaint);
        // 获取进度提示文字
        String progressStr = mCurProgress + "";
        Rect bounds = new Rect();
        mProgressTipPaint.getTextBounds(progressStr, 0, progressStr.length(), bounds);
        // 获取进度提示文字宽度
        float progressStrWidth = bounds.width();
        // 获取进度提示文字起始位置横坐标
        float strX = getProgressStrX(progressStrWidth);
        int dy = (bounds.bottom - bounds.top) / 2 - bounds.bottom;
        // 画进度提示文字
        canvas.drawText(progressStr, strX, mHeight - 2 * mCircleRadius - bounds.height() - dy, mProgressTipPaint);
    }

    private float getProgressStrX(float progressStrWidth) {
        // 获取进度提示文字起始位置(效果设置为提示文字关于拖动圆的圆心横坐标左右对称)
        float progressStrX = circle_x - progressStrWidth / 2;
        // 进度提示文字横坐标 < 进度提示文字宽度 / 2
        if (progressStrX < progressStrWidth / 2) {
            // 进度提示文字横坐标:拖动圆半径 /2
            progressStrX = mCircleRadius / 2;
        } else if (mWidth - circle_x < progressStrWidth) { // 进度条宽度 - 拖动圆的圆心横坐标 < 进度提示文字宽度
            // 进度提示文字横坐标:进度条宽度 -文字宽度 - 拖动圆半径 / 2
            progressStrX = mWidth - progressStrWidth - mCircleRadius / 2;
        }
        return progressStrX;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int touchX = (int) event.getX();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                // 设置正在触摸
                isTouch = true;
                // 避免触摸越界
                if (touchX < 0) {
                    touchX = 0;
                } else if (touchX > mWidth) {
                    touchX = mWidth;
                }
                // 上次的触摸点 mTouchX 和当前触摸点 touchX 不是同一点(避免触摸点重复,导致界面多次绘制)
                if (mTouchX != touchX) {
                    mTouchX = touchX;
                    // 根据手指触摸横坐标设置进度值
                    updateTouchProgress();
                    invalidate();
                }
                break;
            case MotionEvent.ACTION_UP:
                isTouch = false;
                break;
        }
        return true;
    }

    /**
     * 根据手指触摸横坐标设置进度值
     */
    private void updateTouchProgress() {
        mCurProgress = (int) (mTouchX * 1f / mWidth * mMaxProgress + 0.5f);
        ratio = mTouchX * 1f / mWidth;
        invalidate();
    }

    /**
     * 设置进度
     */
    public void setProgress(int progress) {
        if (progress < 0 || progress > mMaxProgress) {
            return;
        }
        mCurProgress = progress;
        // 这里必须使用 1f 转为小数,否则 progress < mMaxProgress 时,progress / mMaxProgress 始终为 0
        ratio = mCurProgress * 1f / mMaxProgress;
        postInvalidate();
    }

    /**
     * 获取最大进度
     */
    public int getMaxProgress() {
        return mMaxProgress;
    }

    private OnProgressListener mOnProgressListener;

    /**
     * 设置进度回调监听
     */
    public void setOnProgressListener(OnProgressListener onProgressListener) {
        this.mOnProgressListener = onProgressListener;
    }

    public interface OnProgressListener {
        // 进度回调
        void onDrag(int progress);
    }

    /**
     * dp 转 px
     */
    private int dp2px(int dp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }

    /**
     * sp 转 px
     */
    private float sp2px(int sp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
    }

}

attrs:

<declare-styleable name="LineProgress">
    <!--进度条中未拖动部分的颜色-->
    <attr name="progress_default_color" format="color" />
    <!--进度条中已拖动部分的颜色-->
    <attr name="progress_drag_color" format="color" />
    <!--拖动圆颜色-->
    <attr name="progress_circle_drag_color" format="color" />
    <!--最大进度-->
    <attr name="progress_max_value" format="integer" />
</declare-styleable>

二、自定义圆环拖动进度条

效果截图:
RingSeekBar 截图

发现的 BUG:在布局文件中设置宽或高为 match_parentwrap_content 时,拖动会有问题

public class RingSeekBar extends View {

    private static final int DEFAULT_RING_WIDTH = 30; // 默认宽度 dp
    private static final int DEFAULT_ROTATE_ANGLE = -90; // 默认旋转角度设置为 -90°(即拖动按钮初始显示在 CircleSeekBar 最顶部)
    private static final int DEFAULT_BORDER_WIDTH = 0; // 默认描边宽度
    private static final int DEFAULT_BORDER_COLOR = 0xffffffff; // 默认描边颜色
    private static final int DEFAULT_THUMB_RADIUS = 15; // 默认拖动按钮半径 dp
    private static final int DEFAULT_THUMB_WIDTH = 2; // 默认拖动按钮描边宽度 dp
    private static final int DEFAULT_THUMB_COLOR = 0xffffffff; // 默认拖动按钮颜色
    private static final int DEFAULT_EDGE_LENGTH = 260; // 默认宽高
    private static final int DEFAULT_PROGRESS_COLOR = 0xffD46526; // 默认进度条颜
    private static final int DEFAULT_MAX_VALUE = 100; // 默认最大进度值
    private static final int DEFAULT_MIN_VALUE = 0; // 默认最小进度值
    private static final int DEFAULT_TEXT_SIZE = 12; // 默认文字大小
    private static final int DEFAULT_TEXT_COLOR = Color.BLACK; // 默认文字颜色
    private static final int DEFAULT_THUMB_CIRCLE = 0; // 拖动按钮为实心圆
    private static final int DEFAULT_THUMB_RING = 1; // 拖动按钮为圆环

    private int[] mRingColors; // CircleSeekBar 颜色
    private float mRingWidth; // CircleSeekBar 宽度
    private float mRotateAngle; // 当前旋转的角度(有新的旋转角度后就把当前值赋给 mLastAngle,避免旋转角度连续不变,导致界面重复绘制,即多次调用)
    private float mLastAngle; // 最后一次保存的旋转角度
    private final int mBorderWidth; // 描边宽度
    private final int mBorderColor; // 描边颜色
    private final int mThumbRadius; // 拖动按钮半径
    private final int mThumbBorderWidth; // 拖动按钮描边宽度
    private final int mThumbStyle; // 拖动按钮样式
    private final int mThumbColor; // 拖动按钮颜色
    private final int mProgressColor; // 进度条颜色

    private Paint mRingPaint; // CircleSeekBar 画笔
    private Paint mProgressPaint;  // 进度画笔
    private Paint mThumbPaint; // 拖动按钮画笔
    private Paint mBorderPaint; // 描边画笔
    private Paint mTextPaint; // 中间文字画笔

    private int minValidateTouchRingRadius; // 最小有效点击半径
    private int maxValidateTouchRingRadius; // 最大有效点击半径

    private int mMaxValue;  // 最大值
    private int mMinValue;  // 最小值
    /**
     * @see OnProgressChangeListener#onProgressChanged(RingSeekBar seekBar, int progress, boolean isUser)
     */
    private int mCurProgress;  // 当前进度(有新的进度后就把当前值赋给 mLastProgress,避免经由旋转角度转换而来的进度值连续不变,导致重复回调进度监听)
    private int mLastProgress; // 最后一次保存的进度值
    private final int mTextSize;  // 中间大小
    private final int mTextColor;  // 中间文字颜色
    private boolean mTextBold = false; // 中间文字是否加粗,默认不加粗

    private float centerX; // 中心坐标 X
    private float centerY; // 中心坐标 Y
    private float radius; // 半径
    private boolean downOnArc; // 手指触摸点是否在圆弧上(大于 minValidateTouchArcRadius,小于 maxValidateTouchArcRadius 为 true,否则为 false)

    int dy; // 中间文字居中时,文字需要向下移动的偏移量

    /******************************三个构造函数*****************************/

    public RingSeekBar(Context context) {
        this(context, null);
    }

    public RingSeekBar(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RingSeekBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        // 获取自定义属性
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RingSeekBar);
        mRingColors = getArcColors(context, ta);
        mRingWidth = ta.getDimensionPixelSize(R.styleable.RingSeekBar_ring_width, dp2px(DEFAULT_RING_WIDTH));
        mRotateAngle = ta.getInt(R.styleable.RingSeekBar_ring_rotate_angle, 0);
        mBorderWidth = ta.getDimensionPixelSize(R.styleable.RingSeekBar_ring_border_width, dp2px(DEFAULT_BORDER_WIDTH));
        mBorderColor = ta.getColor(R.styleable.RingSeekBar_ring_border_color, DEFAULT_BORDER_COLOR);
        mProgressColor = ta.getColor(R.styleable.RingSeekBar_ring_progress_color, DEFAULT_PROGRESS_COLOR);
        mThumbStyle = ta.getInt(R.styleable.RingSeekBar_ring_thumb_style, DEFAULT_THUMB_CIRCLE);
        mThumbColor = ta.getColor(R.styleable.RingSeekBar_ring_thumb_color, DEFAULT_THUMB_COLOR);
        mThumbRadius = ta.getDimensionPixelSize(R.styleable.RingSeekBar_ring_thumb_radius, dp2px(DEFAULT_THUMB_RADIUS));
        mThumbBorderWidth = ta.getDimensionPixelSize(R.styleable.RingSeekBar_ring_thumb_border_width, dp2px(DEFAULT_THUMB_WIDTH));
        mMaxValue = ta.getInt(R.styleable.RingSeekBar_ring_max_value, DEFAULT_MAX_VALUE);
        mMinValue = ta.getInt(R.styleable.RingSeekBar_ring_min_value, DEFAULT_MIN_VALUE);
        updateMaxAndMinValue(); // 校正最大进度值、最小进度值
        mTextSize = ta.getDimensionPixelSize(R.styleable.RingSeekBar_ring_text_size, DEFAULT_TEXT_SIZE);
        mTextColor = ta.getColor(R.styleable.RingSeekBar_ring_text_color, DEFAULT_TEXT_COLOR);
        mTextBold = ta.getBoolean(R.styleable.RingSeekBar_ring_text_bold, mTextBold);
        ta.recycle();

        // 初始化画笔
        initPaint();
    }

    /**
     * 初始化画笔
     */
    private void initPaint() {
        // ArcSeekBar 画笔
        mRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mRingPaint.setStrokeWidth(mRingWidth);
        mRingPaint.setStyle(Paint.Style.STROKE);
        mRingPaint.setStrokeCap(Paint.Cap.ROUND);

        // 拖动按钮画笔
        mThumbPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mThumbPaint.setStrokeWidth(mThumbBorderWidth);
        mThumbPaint.setColor(mThumbColor);
        if (mThumbStyle == DEFAULT_THUMB_CIRCLE) {
            mThumbPaint.setStyle(Paint.Style.FILL);
        } else if (mThumbStyle == DEFAULT_THUMB_RING) {
            mThumbPaint.setStyle(Paint.Style.STROKE);
        }

        // 进度画笔
        mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mProgressPaint.setStrokeWidth(mRingWidth);
        mProgressPaint.setColor(mProgressColor);
        mProgressPaint.setStyle(Paint.Style.STROKE);
        mProgressPaint.setStrokeCap(Paint.Cap.ROUND);

        // 描边画笔
        mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBorderPaint.setStrokeWidth(mBorderWidth);
        mBorderPaint.setColor(mBorderColor);
        mBorderPaint.setStyle(Paint.Style.STROKE);

        // 中间文字画笔
        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setTextSize(sp2px(mTextSize));
        mTextPaint.setColor(mTextColor);
        mTextPaint.setFakeBoldText(mTextBold);
    }

    /**
     * 测量
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int ws = MeasureSpec.getSize(widthMeasureSpec); //取出宽度的确切数值
        int wm = MeasureSpec.getMode(widthMeasureSpec); //取出宽度的测量模式
        int hs = MeasureSpec.getSize(heightMeasureSpec); //取出高度的确切数值
        int hm = MeasureSpec.getMode(heightMeasureSpec); //取出高度的测量模式

        if (wm == MeasureSpec.UNSPECIFIED) {
            ws = dp2px(DEFAULT_EDGE_LENGTH);
        } else if (wm == MeasureSpec.AT_MOST) {
            ws = Math.min(dp2px(DEFAULT_EDGE_LENGTH), ws);
        }
        if (hm == MeasureSpec.UNSPECIFIED) {
            hs = dp2px(DEFAULT_EDGE_LENGTH);
        } else if (hm == MeasureSpec.AT_MOST) {
            hs = Math.min(dp2px(DEFAULT_EDGE_LENGTH), hs);
        }

        setMeasuredDimension(ws, hs);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        centerX = w / 2;
        centerY = h / 2;
        radius = Math.min(centerX, centerY);

        minValidateTouchRingRadius = (int) (radius * 0.7f);
        maxValidateTouchRingRadius = (int) radius;

        resetShaderColor();
    }

    /**
     * 绘制
     */
    @Override
    protected void onDraw(Canvas canvas) {
        // 画外圆
        canvas.drawCircle(centerX, centerY, radius - mRingWidth / 2 - mBorderWidth - mThumbBorderWidth / 2, mRingPaint);
        canvas.drawCircle(centerX, centerY, radius - mBorderWidth / 2 - mThumbBorderWidth / 2, mBorderPaint);
        if (mRotateAngle >= 0) {
            // 画进度
            RectF oval = new RectF(centerX - (radius - mRingWidth / 2) + mBorderWidth + mThumbBorderWidth / 2, centerY - (radius - mRingWidth / 2) + mBorderWidth + mThumbBorderWidth / 2, centerX + (radius - mRingWidth / 2) - mBorderWidth - mThumbBorderWidth / 2, centerY + (radius - mRingWidth / 2) - mBorderWidth - mThumbBorderWidth / 2);
            canvas.drawArc(oval, -90, mRotateAngle, false, mProgressPaint);
            // 画点
            PointF startPoint = calcArcEndPointXY(centerX, centerY, radius - mRingWidth / 2 - mBorderWidth - mThumbBorderWidth / 2, mRotateAngle, DEFAULT_ROTATE_ANGLE);
            canvas.drawCircle(startPoint.x, startPoint.y, mRingWidth / 2, mThumbPaint);
        }
        String curProgressStr = mCurProgress + mMinValue + "";
        float curProgressStrWidth = mTextPaint.measureText(curProgressStr);
        if (dy == 0) {
            Rect bounds = new Rect();
            mTextPaint.getTextBounds(curProgressStr, 0, curProgressStr.length(), bounds);
            dy = (bounds.bottom - bounds.top) / 2 - bounds.bottom;
        }
        canvas.drawText(curProgressStr, centerX - curProgressStrWidth / 2, centerY + dy, mTextPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (isTouchArc(x, y)) {
                    downOnArc = true;
                    updateArc(x, y);
                    if (null != mOnProgressChangeListener) {
                        mOnProgressChangeListener.onStartTrackingTouch(this);
                        mOnProgressChangeListener.onProgressChanged(this, mCurProgress, true);
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (downOnArc) {
                    updateArc(x, y);
                    if (mLastAngle != mRotateAngle) {
                        if (null != mOnProgressChangeListener) {
                            if (mLastProgress != mCurProgress) {
                                mOnProgressChangeListener.onProgressChanged(this, mCurProgress, true);
                                mLastProgress = mCurProgress;
                            }
                        }
                        mLastAngle = mRotateAngle;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                downOnArc = false;
                if (null != mOnProgressChangeListener) {
                    mOnProgressChangeListener.onStopTrackingTouch(this);
                }
                invalidate();
                break;
        }
        return true;
    }

    /**
     * 校正最大进度值、最小进度值
     */
    private void updateMaxAndMinValue() {
        // 最大进度值小于等于最小进度值
        if (mMaxValue <= mMinValue) {
            // 最大进度值赋值为默认最大进度值 100
            mMaxValue = DEFAULT_MAX_VALUE;
            // 最小进度值赋值为默认最小进度值 0
            mMinValue = DEFAULT_MIN_VALUE;
        }
    }

    /**
     * 根据点 (x, y) 的位置,更新中间文字进度
     */
    private void updateArc(int x, int y) {
        if (Math.abs(mRotateAngle - getAngle(x, y)) < 180) {
            mRotateAngle = (int) getAngle(x, y);
        }
        if (mRotateAngle != mLastAngle) {
            updateText(mRotateAngle);
            invalidate();
        }
    }

    /**
     * 计算指定位置 (px, py) 与内容区域中心点(圆点)的夹角
     */
    private float getAngle(float px, float py) {
        float angle = (float) ((Math.atan2(py - centerY, px - centerY)) * 180 / 3.14f);
        if (angle < DEFAULT_ROTATE_ANGLE) {
            angle += 360;
        }
        return angle - DEFAULT_ROTATE_ANGLE + 0.5f;
    }

    /**
     * 判断手指触摸点 (x, y) 是否按在圆环上(大于 minValidateTouchArcRadius,小于 maxValidateTouchArcRadius 为 true,否则为 false)
     */
    private boolean isTouchArc(int x, int y) {
        double d = getTouchRadius(x, y);
        boolean flag = false;
        if (d >= minValidateTouchRingRadius && d <= maxValidateTouchRingRadius) {
            flag = true;
        }
        return flag;
    }

    /**
     * 计算手指触摸点 (x, y) 到内容区域中心点(圆点)的距离
     */
    private double getTouchRadius(int x, int y) {
        int cx = (int) (x - centerX);
        int cy = (int) (y - centerY);
        double v = Math.hypot(cx, cy);
        return v;
    }

    /**
     * 依圆心坐标、半径、扇形角度,计算出扇形终射线与圆弧交叉点的 XY 坐标
     *
     * @param cirX          圆 centerX
     * @param cirY          圆 centerY
     * @param radius        圆半径
     * @param cirAngle      当前滑过的弧对应的角度
     * @param originalAngle 起点弧角度
     * @return 扇形终射线与圆弧交叉点的 XY 坐标
     */
    public static PointF calcArcEndPointXY(float cirX, float cirY, float radius, float cirAngle, float originalAngle) {
        float posX;
        float posY;
        cirAngle = (originalAngle + cirAngle) % 360;
        //将角度转换为弧度
        float arcAngle = (float) (Math.PI * cirAngle / 180.0);
        if (cirAngle < 90) {
            posX = cirX + (float) (Math.cos(arcAngle)) * radius;
            posY = cirY + (float) (Math.sin(arcAngle)) * radius;
        } else if (cirAngle == 90) {
            posX = cirX;
            posY = cirY + radius;
        } else if (cirAngle > 90 && cirAngle < 180) {
            arcAngle = (float) (Math.PI * (180 - cirAngle) / 180.0);
            posX = cirX - (float) (Math.cos(arcAngle)) * radius;
            posY = cirY + (float) (Math.sin(arcAngle)) * radius;
        } else if (cirAngle == 180) {
            posX = cirX - radius;
            posY = cirY;
        } else if (cirAngle > 180 && cirAngle < 270) {
            arcAngle = (float) (Math.PI * (cirAngle - 180) / 180.0);
            posX = cirX - (float) (Math.cos(arcAngle)) * radius;
            posY = cirY - (float) (Math.sin(arcAngle)) * radius;
        } else if (cirAngle == 270) {
            posX = cirX;
            posY = cirY - radius;
        } else {
            arcAngle = (float) (Math.PI * (360 - cirAngle) / 180.0);
            posX = cirX + (float) (Math.cos(arcAngle)) * radius;
            posY = cirY - (float) (Math.sin(arcAngle)) * radius;
        }
        return new PointF(posX, posY);
    }

    /*****************************设置圆环背景渐变*****************************/

    /**
     * 获取 ArcSeekBar 颜色数组
     */
    private int[] getArcColors(Context context, TypedArray ta) {
        int[] ret;
        int resId = ta.getResourceId(R.styleable.RingSeekBar_ring_colors, 0);
        if (0 == resId) {
            resId = R.array.arc_colors_default;
        }
        ret = getColorsByArrayResId(context, resId);
        return ret;
    }

    /**
     * 根据 resId 获取颜色数组
     */
    private int[] getColorsByArrayResId(Context context, int resId) {
        int[] ret;
        TypedArray colorArray = context.getResources().obtainTypedArray(resId);
        ret = new int[colorArray.length()];
        for (int i = 0; i < colorArray.length(); i++) {
            ret[i] = colorArray.getColor(i, 0);
        }
        colorArray.recycle();
        return ret;
    }

    /**
     * 重置 Shader 颜色
     */
    private void resetShaderColor() {
        // 计算渐变数组
        float startPos = (mRotateAngle / 2) / 360;
        float stopPos = (360 - (mRotateAngle / 2)) / 360;
        int len = mRingColors.length - 1;
        float distance = (stopPos - startPos) / len;
        float pos[] = new float[mRingColors.length];
        for (int i = 0; i < mRingColors.length; i++) {
            pos[i] = startPos + (distance * i);
        }
        SweepGradient gradient = new SweepGradient(centerX, centerY, mRingColors, pos);
        mRingPaint.setShader(gradient);
    }

    /******************************外部修改*****************************/

    private void updateText(float curAngle) {
        mCurProgress = (int) ((curAngle * (mMaxValue - mMinValue)) / 360);
    }

    public void setProgress(int progress) {
        this.mCurProgress = progress;
        this.mRotateAngle = mCurProgress * 360 / (mMaxValue - mMinValue);
        if (null != mOnProgressChangeListener && mLastProgress != mCurProgress) {
            mOnProgressChangeListener.onProgressChanged(this, mCurProgress, false);
            mLastProgress = mCurProgress;
        }
        postInvalidate();
    }

    public RingSeekBar setMaxValue(int maxValue) {
        this.mMaxValue = maxValue;
        updateMaxAndMinValue();
        return this;
    }

    public RingSeekBar setMinValue(int minValue) {
        this.mMinValue = minValue;
        updateMaxAndMinValue();
        return this;
    }

    /******************************设置进度回调*****************************/

    private OnProgressChangeListener mOnProgressChangeListener;

    public void setOnProgressChangeListener(OnProgressChangeListener onProgressChangeListener) {
        this.mOnProgressChangeListener = onProgressChangeListener;
    }

    public interface OnProgressChangeListener {
        /**
         * 进度发生变化
         *
         * @param seekBar  圆环拖动进度条
         * @param progress 当前进度值
         * @param isUser   是否由用户操作
         */
        void onProgressChanged(RingSeekBar seekBar, int progress, boolean isUser);

        /**
         * 用户开始拖动
         */
        void onStartTrackingTouch(RingSeekBar seekBar);

        /**
         * 用户结束拖动
         */
        void onStopTrackingTouch(RingSeekBar seekBar);
    }

    /*****************************单位转换*****************************/

    /**
     * sp 转 px
     */
    private float sp2px(int sp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
    }

    /**
     * dp 转 px
     */
    private int dp2px(int dp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }
}

attrs:

<declare-styleable name="RingSeekBar">
    <!--圆弧宽度-->
    <attr name="ring_width" format="dimension" />
    <!--圆弧旋转角度-->
    <attr name="ring_rotate_angle" format="integer" />
    <!--描边宽度-->
    <attr name="ring_border_width" format="dimension" />
    <!--圆弧渐变色-->
    <attr name="ring_colors" format="reference" />
    <!--描边颜色-->
    <attr name="ring_border_color" format="color" />
    <!--拖动进度颜色-->
    <attr name="ring_progress_color" format="color" />
    <!--拖动按钮样式-->
    <attr name="ring_thumb_style" format="enum">
        <enum name="CIRCLE" value="0" />
        <enum name="RING" value="1" />
    </attr>
    <!--拖动按钮描边颜色-->
    <attr name="ring_thumb_color" format="color" />
    <!--拖动按钮半径-->
    <attr name="ring_thumb_radius" format="dimension" />
    <!--拖动按钮描边宽度-->
    <attr name="ring_thumb_border_width" format="dimension" />
    <!--进度最大值-->
    <attr name="ring_max_value" format="integer" />
    <!--进度最小值-->
    <attr name="ring_min_value" format="integer" />
    <!--进度文字大小-->
    <attr name="ring_text_size" format="dimension" />
    <!--进度文字颜色-->
    <attr name="ring_text_color" format="color" />
    <!--进度文字是否加粗-->
    <attr name="ring_text_bold" format="boolean" />
</declare-styleable>

colors:

<array name="arc_colors_default">
    <item>#1a2a6c</item>
    <item>#b21f1f</item>
    <item>#fdbb2d</item>
</array>

三、时分秒时间格式显示控件

效果截图:
TimeSurfaceView 截图

发现的 BUG:在电视上显示时,会导致应用不响应遥控器的方向键

public class TimeSurfaceView extends SurfaceView implements SurfaceHolder.Callback {

    private static final long DRAW_INTERVAL = 30; // 最大帧数 (1000 / 30)
    private SurfaceHolder mHolder; // 里面保存了一个对 Surface 对象的引用
    private Canvas mCanvas; // 绘图的画布
    private boolean mIsCanDrawing; // 控制绘画线程的标识
    private Paint mPaint; // 画笔
    private int mTextSize = 12; // 文字大小

    private int w; // 宽
    private int h; // 高
    private int dy; // 文字在高度上居中显示时,需要向下偏移的距离

    private final int SHOW_FULL_TIME = 0;
    private final int SHOW_NO_WEEK = 1;
    private final int SHOW_ONE_DAY = 2;
    private int mTimeStyle = SHOW_FULL_TIME;

    private final int GRAVITY_LEFT = 100;
    private final int GRAVITY_RIGHT = 101;
    private int mGravity = GRAVITY_LEFT;
    private int mTextColor = Color.BLACK;

    public TimeSurfaceView(Context context) {
        this(context, null);
    }

    public TimeSurfaceView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TimeSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TimeSurfaceView);
        mTextSize = ta.getDimensionPixelSize(R.styleable.TimeSurfaceView_time_text_size, sp2px(mTextSize));
        mTextColor = ta.getColor(R.styleable.TimeSurfaceView_time_text_color, mTextColor);
        mTimeStyle = ta.getInt(R.styleable.TimeSurfaceView_time_style, mTimeStyle);
        mGravity = ta.getInt(R.styleable.TimeSurfaceView_time_gravity, mGravity);
        ta.recycle();

        init();
    }

    public void setTextSize(int textSize) {
        this.mTextSize = sp2px(textSize);
    }

    /**
     * sp 转 px
     */
    private int sp2px(int sp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
    }

    /**
     * 初始化
     */
    private void init() {
        // 获取 SurfaceHolder 对象
        mHolder = getHolder();
        // 注册 SurfaceHolder 的回调方法
        mHolder.addCallback(this);

        // 设置画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(mTextColor);
        mPaint.setFakeBoldText(true);
        mPaint.setTextSize(mTextSize);
        
        // 数字 0~9 的宽度不一致
        // 如果显示的初始时间含有比较窄的数字(如:1),当显示比较宽的数字(如:4)时就会导致,如果时间居右显示时,会显示不全
        // 当控件位于 LinearLayout 中,当控件宽度大于实际内容宽度时,且假设实际内容居右显示,则需要时间文本中各数字取最大宽度,左侧宽度为控件宽度减去时间文本最大宽度(各数字取最大宽度)
        String maxWidthTime = "4444年02月24日 星期五 24:44:44";
        Rect bounds = new Rect();
        mPaint.getTextBounds(maxWidthTime, 0, maxWidthTime.length(), bounds);
        w = bounds.width();
        h = bounds.height();
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        dy = (int) ((fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom);

        setZOrderOnTop(true);
        mHolder.setFormat(PixelFormat.TRANSLUCENT);
    }

    /**
     * 格式化系统时间
     */
    private String getTime() {
        // 创建 Calendar 对象,用于获取系统时间
        Calendar calendar = Calendar.getInstance();
        String ymd;
        String week;
        String sfm;
        switch (mTimeStyle) {
            case SHOW_FULL_TIME: // 格式如:2019年01月18日 星期五 11:48:23
                ymd = getYMD(calendar); // 获取年月日
                week = getWeek(calendar); // 获取星期
                sfm = getSFM(calendar); // 获取时分秒
                return ymd + " " + week + " " + sfm;
            case SHOW_NO_WEEK: // 格式如:2019年01月18日 11:48:23
                ymd = getYMD(calendar); // 获取年月日
                sfm = getSFM(calendar); // 获取时分秒
                return ymd + " " + sfm;
            case SHOW_ONE_DAY: // 格式如:11:48:23
                sfm = getSFM(calendar); // 获取时分秒
                return sfm;
        }
        return "";
    }

    /*****************************获取系统时间*****************************/

    /**
     * 获取时分秒
     */
    private String getSFM(Calendar calendar) {
        int hour_num = calendar.get(Calendar.HOUR_OF_DAY);
        String hour = String.valueOf(hour_num < 10 ? "0" + hour_num : hour_num);

        int minute_num = calendar.get(Calendar.MINUTE);
        String minute = String.valueOf(minute_num < 10 ? "0" + minute_num : minute_num);

        int second_num = calendar.get(Calendar.SECOND);
        String second = String.valueOf(second_num < 10 ? "0" + second_num : second_num);
        return hour + ":" + minute + ":" + second;
    }

    /**
     * 获取星期
     */
    private String getWeek(Calendar calendar) {
        String week = String.valueOf(calendar.get(Calendar.DAY_OF_WEEK));
        switch (week) {
            case "1":
                week = "日";
                break;
            case "2":
                week = "一";
                break;
            case "3":
                week = "二";
                break;
            case "4":
                week = "三";
                break;
            case "5":
                week = "四";
                break;
            case "6":
                week = "五";
                break;
            case "7":
                week = "六";
                break;
        }
        return "星期" + week;
    }

    /**
     * 获取年月日
     */
    private String getYMD(Calendar calendar) {
        String year = String.valueOf(calendar.get(Calendar.YEAR));

        int month_num = calendar.get(Calendar.MONTH) + 1;
        String month = String.valueOf(month_num < 10 ? "0" + month_num : month_num);

        int day_num = calendar.get(Calendar.DAY_OF_MONTH);
        String day = String.valueOf(day_num < 10 ? "0" + day_num : day_num);
        return year + "年" + month + "月" + day + "日";
    }

    /*****************************子线程中绘制时间*****************************/

    /**
     * 绘制时间的线程
     */
    private class TimeThread extends Thread {
        @Override
        public void run() {
            do {
                long tickTime = System.currentTimeMillis();
                // 绘制时间
                drawTime(getTime());
                long deltaTime = System.currentTimeMillis() - tickTime;
                // 绘制消耗的时间小于系统单个绘制帧的绘制时间
                if (deltaTime < DRAW_INTERVAL) {

                    SystemClock.sleep(DRAW_INTERVAL - deltaTime);
                }
            } while (mIsCanDrawing);
        }
    }

    /**
     * 绘制时间
     */
    private void drawTime(String time) {
        // 绘制标识为 true,可以绘制,如果为 false,则线程停止
        if (mIsCanDrawing) {
            try {
                synchronized (mHolder) {
                    // 通过 SurfaceHolder#lockCanvans() 方法获取 Canvas 绘图对象
                    mCanvas = mHolder.lockCanvas();
                    if (null != mCanvas) {
                        /**
                         * 设置背景透明步骤:
                         *      1. 在构造方法中调用
                         *          setZOrderOnTop(true);
                         *          mHolder.setFormat(PixelFormat.TRANSLUCENT);
                         *      2. 绘制前,通过 drawColor() 方法进行清屏操作
                         *          mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
                         */
                        mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
                        // 绘制文字
                        if (wm == MeasureSpec.AT_MOST) {
                            mCanvas.drawText(time, 0, h / 2 + dy, mPaint);
                        } else {
                        	// 居右显示时,当前绘制的 x 起始坐标为 getWidth() - w
                        	// 因为 0~9 间各数字宽度不一致,所以为了保证不出现显示不全的情况,时间文本中各数字应取最宽的数字,x 才能最小,最宽的时间文本才能显示完全
                            if (mGravity == GRAVITY_RIGHT) {
                                mCanvas.drawText(time, getWidth() - w, h / 2 + dy, mPaint);
                            } else {
                                mCanvas.drawText(time, 0, h / 2 + dy, mPaint);
                            }
                        }
                    }
                }
            } finally {
                if (null != mCanvas) {
                    // 通过 SurfaceHolder#unlockCanvasAndPost() 方法对画布内容进行提交
                    mHolder.unlockCanvasAndPost(mCanvas);
                }
            }
        }
    }

    int wm;

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int ws = MeasureSpec.getSize(widthMeasureSpec);
        wm = MeasureSpec.getMode(widthMeasureSpec);
        if (wm == MeasureSpec.AT_MOST) {
            ws = w;
        }
        int hs = MeasureSpec.getSize(heightMeasureSpec);
        int hm = MeasureSpec.getMode(heightMeasureSpec);
        if (hm == MeasureSpec.AT_MOST) {
            hs = h;
        }
        setMeasuredDimension(ws, hs);
    }

    /******************************SurfaceView 回调*****************************/

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        // 控制绘画线程的标识置为 true,可以开始绘制
        mIsCanDrawing = true;
        // 开启子线程,进行绘制
        new TimeThread().start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        // 控制绘画线程的标识置为 false,停止绘制
        mIsCanDrawing = false;
    }

}

attrs:

<declare-styleable name="TimeSurfaceView">
    <!--文字大小-->
    <attr name="time_text_size" format="dimension" />
    <!--文字颜色-->
    <attr name="time_text_color" format="color" />
    <!--时间显示格式-->
    <attr name="time_style" format="enum">
        <enum name="SHOW_FULL_TIME" value="0" />
        <enum name="SHOW_NO_WEEK" value="1" />
        <enum name="SHOW_ONE_DAY" value="2" />
    </attr>
    <!--实际显示内容对应的 GRAVITY-->
    <attr name="time_gravity" format="enum">
        <enum name="left" value="100" />
        <enum name="right" value="101" />
    </attr>
</declare-styleable>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值