Android Canvas绘制自定义“线头”问题

原创
2021/02/26 17:22
阅读数 456

线头介绍:

Android 提供了线头设置的方法线头形状有三种:

BUTT 平头、ROUND 圆头、SQUARE 方头。

默认为 BUTT。

而当线条变粗的时候,它们就会表现出不同的样子:

 

问题:如何自定义文章开始那种线头呢?

 

解决方案:

线条描边,我们给线条添加border

boder宽度 * 2 + 中心线条宽度 = 总宽度

由于实现方案需要结合场景,因此不适合封装,但是具体用途我们按照本思路实现即可

下面我们给出一个案例

代码实现

public class SectionPointerMeterView extends View implements ValueAnimator.AnimatorUpdateListener {

    private final String TAG = "MeterView";
    private TextPaint mTextPaint;
    private DisplayMetrics mDisplayMetrics;
    private int mContentHeight = 0;
    private int mContentWidth = 0;

    private final float MIN_ARC_ANGLE = 120f;
    private final float MAX_ARC_ANGLE = 360 - MIN_ARC_ANGLE;
    private int progress = 0;
    private int maxProgress = 100;
    private String tagText = "";
    private final static String UNIT_PERCENT = "%";
    private ValueAnimator animatorProgress = null;
    private volatile boolean isRelease = false;
    private boolean disableComputeColorBlock = false;

    private float MIN_PADDING = 0.0f;
    private float lineRadius = 0;

    public static class ColorBlock {
        int color;
        float ratio;

        public ColorBlock(float ratio, int color) {
            this.color = color;
            this.ratio = ratio;
        }

        static ColorBlock build(float ratio, int color) {
            ColorBlock cb = new ColorBlock(ratio, color);
            return cb;
        }
    }

    public final ColorBlock[] colorBlocks = {
            ColorBlock.build(0f, 0xffFF1D1D),
            ColorBlock.build(0.1f, 0xffFF1D1D),
            ColorBlock.build(0.2f, 0xffF04D11),
            ColorBlock.build(0.3f, 0xffF04D11),
            ColorBlock.build(0.4f, 0xffFEA315),
            ColorBlock.build(0.5f, 0xffFEA315),
            ColorBlock.build(0.6f, 0xffFEA315),
            ColorBlock.build(0.7f, 0xffF8DF38),
            ColorBlock.build(0.8f, 0xffF8DF38),
            ColorBlock.build(0.9f, 0xff10D659),
            ColorBlock.build(1.0f, 0xff10D659),

    };

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

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

    public SectionPointerMeterView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
        if (isInEditMode() || BuildConfig.DEBUG) {
            setMaxProgress(100);
            setProgress(25);
            setTagText("功效");
        }
    }

    public void setMaxProgress(int maxProgress) {
        if (maxProgress <= 0) {
            throw new IllegalArgumentException(" max progress must be postive num");
        }
        this.maxProgress = maxProgress;
        invalidate();
    }

    public void setProgress(int progress) {

        if (progress < 0) {
            throw new IllegalArgumentException("  progress must be no-nagtive num");
        }
        this.progress = progress;
        invalidate();
    }

    public void setProgress(int progress, boolean isAnimate) {
        if (progress < 0) {
            throw new IllegalArgumentException("  progress must be no-nagtive num");
        }
        if (!isAnimate) {
            setProgress(progress);
            return;
        }

        int current = this.progress;
        int target = progress;

        if (animatorProgress != null) {
            animatorProgress.cancel();
            animatorProgress = null;
        }
        if (target == current) {
            return;
        }
        startProgressAnimation(current, target);

    }

    private void startProgressAnimation(int current, int target) {
        ValueAnimator animator = ValueAnimator.ofInt(current, target);
        animator.setDuration(1100);
        animator.setInterpolator(new BounceInterpolator());
        animatorProgress = animator;
        animatorProgress.addUpdateListener(this);
        animator.start();
    }

    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        this.progress = (int) animation.getAnimatedValue();
        invalidate();
    }

    public void setTagText(String tagText) {
        this.tagText = tagText;
    }

    private void initPaint() {
        mDisplayMetrics = getResources().getDisplayMetrics();
        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setAntiAlias(true);
        MIN_PADDING = dp2px(1);
        lineRadius = dp2px(5);

    }


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

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        if (widthMode != MeasureSpec.EXACTLY) {
            widthSize = mDisplayMetrics.widthPixels / 2;
        }

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (heightMode != MeasureSpec.EXACTLY) {
            heightSize = widthSize / 2;
        }
        setMeasuredDimension(widthSize, heightSize);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {

        super.onSizeChanged(w, h, oldw, oldh);

        mContentHeight = (int) (h - MIN_PADDING*2);
        mContentWidth = (int) (w - MIN_PADDING*2);

    }


    private float minLength = 0f;
    private float outArcR = 0f;
    private float  centerX = 0f;
    private float  centerY = 0f;
    private float  startAngle = 0f;
    private float offsetDegree = 5f;

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (isRelease) {
            return;
        }

        mTextPaint.setStyle(Paint.Style.STROKE);

        mTextPaint.setStrokeWidth(dp2px(20));
        float arcStrokeWidth = mTextPaint.getStrokeWidth();
        if (mContentWidth <= arcStrokeWidth || mContentHeight <= arcStrokeWidth) {
            return;
        }

        minLength = Math.min(mContentWidth, mContentHeight) - arcStrokeWidth;
        outArcR  = (float) (minLength/(1f+Math.cos(Math.toRadians(MIN_ARC_ANGLE/2))));
        centerX = getWidth()/2f;
        centerY = outArcR + MIN_PADDING + arcStrokeWidth/2;
        startAngle = (180f - MIN_ARC_ANGLE)/2f +  MIN_ARC_ANGLE;

        Bitmap targetBitmap =  Bitmap.createBitmap(getWidth(),getHeight(),Bitmap.Config.ARGB_8888);
        drawArcSection(new Canvas(targetBitmap), arcStrokeWidth);
        RectF rectF = new RectF();
        rectF.left = 0;
        rectF.right = getWidth();
        rectF.top = 0;
        rectF.bottom = getHeight();
        canvas.drawBitmap(targetBitmap,null,rectF,null);
        targetBitmap.recycle();

        int saveCount = canvas.save();
        canvas.translate(centerX,centerY);

        float totalDegree = 360f - (MIN_ARC_ANGLE+ offsetDegree *2);
        float perDegree = totalDegree/10f;
        mTextPaint.setStrokeWidth(dp2px(1));
        mTextPaint.setColor(Color.WHITE);
        mTextPaint.setStrokeCap(Paint.Cap.BUTT);
        for (int i=0;i<11;i++){
            float angle = (float) Math.toRadians(startAngle+ offsetDegree + i* perDegree);
            float sx = (float) (Math.cos(angle)*(outArcR - arcStrokeWidth/2));
            float sy = (float) (Math.sin(angle)*(outArcR - arcStrokeWidth/2));

            float ex = (float) (Math.cos(angle)*(outArcR - arcStrokeWidth/2-dp2px(5)));
            float ey = (float) (Math.sin(angle)*(outArcR - arcStrokeWidth/2-dp2px(5)));

            canvas.drawLine(sx,sy,ex,ey,mTextPaint);
        }


        drawPointer(canvas,outArcR);

        float  innerArcR = outArcR/2f;
        SweepGradient colorShader = new SweepGradient(0, 0, new int[]{
                0x33ffffff,
                Color.TRANSPARENT,
                0x33ffffff,
                Color.WHITE,
                0x33ffffff
        },new float[]{
                0,
                (90)/360f,
                180f/360f,
                270/360f,
                1.0f
        });
        mTextPaint.setStyle(Paint.Style.STROKE);
        mTextPaint.setShader(colorShader);


        RectF innerArcRect = new RectF();
        innerArcRect.left   =  -innerArcR;
        innerArcRect.right  =   innerArcR;
        innerArcRect.top    =  -innerArcR;
        innerArcRect.bottom  =   innerArcR;
        canvas.drawArc(innerArcRect,startAngle+ offsetDegree,MAX_ARC_ANGLE-2* offsetDegree,false,mTextPaint);

        mTextPaint.setShader( null);

        canvas.restoreToCount(saveCount);

        mTextPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        drawTextBlock(canvas,centerX,centerY);
        drawTextScale(canvas,centerX,centerY,outArcR-arcStrokeWidth-dp2px(8));

    }

    private void drawPointer(Canvas canvas, float compassRadius) {
        int count = canvas.save();
        float ratio = (progress * 1f / maxProgress * 1f);
        float CONTENT_MAX_ARC_ANGEL = (MAX_ARC_ANGLE - offsetDegree*2);
        float progressAngle = ratio * (MAX_ARC_ANGLE - offsetDegree*2);

        canvas.rotate(-(CONTENT_MAX_ARC_ANGEL) / 2f + progressAngle);
        RectF pointerRectF  = new RectF();
        pointerRectF.left   = - dp2px(1);
        pointerRectF.right  =  dp2px(1);
        pointerRectF.top    = - compassRadius * 2 / 3f;
        pointerRectF.bottom = 0;

        mTextPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mTextPaint.setColor(0xee5191FF);

        canvas.drawRoundRect(pointerRectF, 0,0, mTextPaint);

        canvas.restoreToCount(count);
    }


    private void drawArcSection(Canvas canvas, float strokeWidth) {


        float  centerLineWidth = strokeWidth - lineRadius * 2;
        float  degree = MAX_ARC_ANGLE* 2f/(11f);

        int saveCount = canvas.save();
        canvas.translate(centerX,centerY);

        mTextPaint.setColor(0xffFF1D1D);  //最左侧边缘
        drawArcRoundLine(canvas, outArcR, startAngle,degree, centerLineWidth);

        mTextPaint.setColor(0xff10D659);//最右侧边缘
        drawArcRoundLine(canvas, outArcR, startAngle+MAX_ARC_ANGLE - degree,degree, centerLineWidth);

        mTextPaint.setStrokeWidth(strokeWidth);
        mTextPaint.setColor(0xffF04D11);
        drawArcLine(canvas,outArcR,startAngle+degree,degree);


        mTextPaint.setColor(0xffF8DF38);  //最右侧第二半圆
        drawArcLine(canvas,outArcR,startAngle+MAX_ARC_ANGLE - degree*2,degree);

        //顶部半圆
        mTextPaint.setColor(0xffFEA315);
        drawArcLine(canvas,outArcR,startAngle+degree*2,degree+degree/2);

        mTextPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        drawSplitLines(canvas, outArcR, startAngle, degree,strokeWidth);
        mTextPaint.setXfermode(null);

        canvas.restoreToCount(saveCount);
    }

    private void drawSplitLines(Canvas canvas, float outArcR, float startAngle, float degree,float strokeWith) {
        float degreePadding = 5f;

        int color = mTextPaint.getColor();
        mTextPaint.setColor(Color.MAGENTA);
        mTextPaint.setStrokeWidth(strokeWith+dp2px(1));

        drawSplitLine(canvas,outArcR,startAngle+degree-degreePadding/2,degreePadding/2);

        drawSplitLine(canvas,outArcR,startAngle+degree*2-degreePadding/2,degreePadding/2);

        drawSplitLine(canvas,outArcR,(startAngle+degree*2 + degree+degree/2),degreePadding/2);

        drawSplitLine(canvas,outArcR,startAngle+MAX_ARC_ANGLE-degree,degreePadding/2);

        mTextPaint.setColor(color);
    }


    private void drawArcLine(Canvas canvas, float outArcR, float startAngle,float swipeAngle) {
        RectF arcRect = new RectF();

        arcRect.left = - outArcR;
        arcRect.right = outArcR;
        arcRect.top = - outArcR;
        arcRect.bottom = outArcR;

        mTextPaint.setStrokeCap(Paint.Cap.BUTT);
        canvas.drawArc(arcRect, startAngle, swipeAngle, false, mTextPaint);

    }
    private void drawSplitLine(Canvas canvas, float outArcR, float startAngle,float swipeAngle) {
        RectF arcRect = new RectF();
        arcRect.left = - outArcR;
        arcRect.right = outArcR;
        arcRect.top = - outArcR;
        arcRect.bottom = outArcR;

        mTextPaint.setStrokeCap(Paint.Cap.BUTT);
        canvas.drawArc(arcRect, startAngle, swipeAngle, false, mTextPaint);

    }


    private void drawArcRoundLine(Canvas canvas, float outArcR, float startAngle,float swipeAngle, float centerLineWidth) {
        RectF arcRect = new RectF();
        arcRect.left = - outArcR;
        arcRect.right = outArcR;
        arcRect.top = - outArcR;
        arcRect.bottom = outArcR;
        if(centerLineWidth<=0) {
            mTextPaint.setStrokeCap(Paint.Cap.ROUND);
            canvas.drawArc(arcRect, startAngle, swipeAngle, false, mTextPaint);
        }else{
            mTextPaint.setStrokeCap(Paint.Cap.SQUARE);

            mTextPaint.setStrokeWidth(centerLineWidth);
            canvas.drawArc(arcRect, startAngle, swipeAngle, false, mTextPaint);

            mTextPaint.setStrokeCap(Paint.Cap.ROUND);
            mTextPaint.setStrokeWidth(lineRadius*2);

            RectF oArcRect = new RectF();
            oArcRect.left = - outArcR - centerLineWidth/2;
            oArcRect.right = outArcR + centerLineWidth/2;
            oArcRect.top = - outArcR - centerLineWidth /2;
            oArcRect.bottom = outArcR + centerLineWidth/2;

            canvas.drawArc(oArcRect, startAngle, swipeAngle, false, mTextPaint);

            RectF iArcRect = new RectF();
            iArcRect.left = - outArcR + centerLineWidth/2;
            iArcRect.right = outArcR - centerLineWidth/2;
            iArcRect.top = - outArcR + centerLineWidth /2;
            iArcRect.bottom = outArcR - centerLineWidth/2;
            canvas.drawArc(iArcRect, startAngle, swipeAngle, false, mTextPaint);
        }
    }

    private void drawTextScale(Canvas canvas, float centerX, float centerY, float pointerLength) {
        int id = canvas.save();
        canvas.translate(centerX, centerY);
        final String startText = "0%";
        final String endText = maxProgress + "%";

        mTextPaint.setTextSize(dp2px(12));

        float offsetAngle = (180 - MIN_ARC_ANGLE  ) / 2f; //计算从X轴方向逆时针的角度
        float sx = (float) (Math.cos(Math.toRadians((MIN_ARC_ANGLE + offsetAngle + offsetDegree))) * pointerLength);
        float sy = (float) (Math.sin(Math.toRadians((MIN_ARC_ANGLE + offsetAngle + offsetDegree))) * pointerLength);

        canvas.drawText(startText, sx - mTextPaint.measureText(startText) / 2, sy + getTextPaintBaseline(mTextPaint), mTextPaint);

        float x = (float) (Math.cos(Math.toRadians((offsetAngle - offsetDegree))) * pointerLength);
        float y = (float) (Math.sin(Math.toRadians((offsetAngle - offsetDegree))) * pointerLength);
        canvas.drawText(endText, x - mTextPaint.measureText(endText) / 2, y + getTextPaintBaseline(mTextPaint), mTextPaint);


        canvas.restoreToCount(id);
    }

    private void drawTextBlock(Canvas canvas, float centerX, float centerY) {

        float ratio = (progress * 1f / maxProgress * 1f);


        int id = canvas.save();
        canvas.translate(centerX, centerY);

        final String text = "+" + progress;
        ColorBlock colorBlock = computeColorBlock(ratio);

        if (colorBlock != null) {
            mTextPaint.setColor(colorBlock.color);
        }
        final float textSize = dp2px(40);
        final float textUnitSize = dp2px(20);
        final float textPadding = dp2px(2);
        final float topOffset = dp2px(5) * -1f;

        mTextPaint.setTextSize(textSize);
        mTextPaint.setFakeBoldText(true);
        final float textBaseline = getTextPaintBaseline(mTextPaint);
        float textWidth = mTextPaint.measureText(text);

        mTextPaint.setTextSize(textUnitSize);
        mTextPaint.setFakeBoldText(false);

        float textUnitWidth = mTextPaint.measureText(UNIT_PERCENT);
        float topTextWidth = textWidth + textUnitWidth + textPadding;


        mTextPaint.setTextSize(textSize);
        mTextPaint.setFakeBoldText(true);

        canvas.drawText(text, -topTextWidth / 2, textBaseline + topOffset, mTextPaint);

        mTextPaint.setTextSize(textUnitSize);
        mTextPaint.setFakeBoldText(false);
        canvas.drawText(UNIT_PERCENT, -topTextWidth / 2 + textWidth + textPadding, textBaseline + topOffset, mTextPaint);


        mTextPaint.setTextSize(dp2px(15));
        mTextPaint.setFakeBoldText(false);
        mTextPaint.setColor(0xddffffff);
        float bottomTextWidth = mTextPaint.measureText(tagText);

        canvas.drawText(tagText, -bottomTextWidth / 2, topOffset + textBaseline + getTextHeight(mTextPaint) + getTextPaintBaseline(mTextPaint), mTextPaint);
        canvas.restoreToCount(id);
    }

    private ColorBlock computeColorBlock(float ratio) {
        if (disableComputeColorBlock) {
            return ColorBlock.build(ratio, Color.WHITE);
        }
        if (ratio <= 0) {
            return colorBlocks[0];
        }
        if (ratio >= 1f) {
            return colorBlocks[colorBlocks.length - 1];
        }
        for (int i = 0; i < colorBlocks.length; i++) {
            if (ratio > colorBlocks[i].ratio) {
                continue;
            }
            if (colorBlocks[i].ratio == ratio) {
                return colorBlocks[i];
            }
            int preIndex = i - 1;
            float dx = Math.abs(colorBlocks[i].ratio - ratio) - Math.abs(colorBlocks[preIndex].ratio - ratio);
            if (dx > 0) {
                return colorBlocks[preIndex];
            }
            return colorBlocks[i];
        }

        return ColorBlock.build(ratio, Color.WHITE);
    }

    public float dp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDisplayMetrics);
    }

    public float sp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDisplayMetrics);
    }

    public static int argb(
            int alpha,
            int red,
            int green,
            int blue) {
        return (alpha << 24) | (red << 16) | (green << 8) | blue;
    }

    //真实宽度 + 笔画上下两侧间隙(符合文本绘制基线)
    private static int getTextHeight(Paint paint) {
        Paint.FontMetricsInt fm = paint.getFontMetricsInt();
        int textHeight = ~fm.top - (~fm.top - ~fm.ascent) - (fm.bottom - fm.descent);
        return textHeight;
    }

    /**
     * 基线到中线的距离=(Descent+Ascent)/2-Descent
     * 注意,实际获取到的Ascent是负数。公式推导过程如下:
     * 中线到BOTTOM的距离是(Descent+Ascent)/2,这个距离又等于Descent+中线到基线的距离,即(Descent+Ascent)/2=基线到中线的距离+Descent。
     */
    public static float getTextPaintBaseline(Paint p) {
        Paint.FontMetrics fontMetrics = p.getFontMetrics();
        return (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent;
    }



    public void setDisableComputeColorBlock(boolean disableComputeColorBlock) {
        this.disableComputeColorBlock = disableComputeColorBlock;
        invalidate();
    }
}

 

 

 

 

展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部