线头介绍:
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();
}
}