说来惭愧,工作数年,连基本的自定义View都不会,而且,我并不是很low的程序员。当然,现在转了后端,自定义View会与不会已经很不重要了,但是今晚,我却发现它很简单,于是忍不住有了此文,没错,忍不住分享给你看呀。
先看一个视频吧:
这个自定义 View 非常简单,它类似于很多软件的开屏广告,随着倒计时,这个自定义的进度框也顺时针转了一圈。下面,我来说说怎么实现吧。
首先,对于自定义 View 切莫产生恐惧心理或者排斥心理,否则你做不下去。这里,我们需要做的是先画一个圆,然后再画一个圆,两个重叠,但是颜色不一样,另外,第二个圆不是完整的圆。在 Android 的 sdk 里,有这么个方法:
/** * <p> * Draw the specified arc, which will be scaled to fit inside the specified oval. * </p> * <p> * If the start angle is negative or >= 360, the start angle is treated as start angle modulo * 360. * </p> * <p> * If the sweep angle is >= 360, then the oval is drawn completely. Note that this differs * slightly from SkPath::arcTo, which treats the sweep angle modulo 360. If the sweep angle is * negative, the sweep angle is treated as sweep angle modulo 360 * </p> * <p> * The arc is drawn clockwise. An angle of 0 degrees correspond to the geometric angle of 0 * degrees (3 o'clock on a watch.) * </p> * * @param oval The bounds of oval used to define the shape and size of the arc * @param startAngle Starting angle (in degrees) where the arc begins * @param sweepAngle Sweep angle (in degrees) measured clockwise * @param useCenter If true, include the center of the oval in the arc, and close it if it is * being stroked. This will draw a wedge * @param paint The paint used to draw the arc */ public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint) { super.drawArc(oval, startAngle, sweepAngle, useCenter, paint); }
这个方法是在一个矩形里画一个圆弧,oval: 圆弧所在的矩形对象,即圆弧会限制在这个矩形对象内,startAngle: 起始角度,Android 的坐标系是 逆向的(和数学对比的话),sweepAngle 是 弧度的角度,比如 90°是四分之一个圆,180°是半圆,useCenter: 是否显示半径连线,true表示显示圆弧与圆心的半径连线,false表示不显示。paint 即画这个圆弧时使用的画笔。
我的核心代码如下:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); float fromAngle = progress * 3.6f; float startAngle = 270.0f; float sweepAngle; if (progress < 25) { startAngle += fromAngle; sweepAngle = 360 - fromAngle; } else { startAngle = fromAngle - 90; sweepAngle = 270 - startAngle; } canvas.drawArc(rectF, 0, 360, false, paint); canvas.drawArc(rectF, startAngle, sweepAngle, false, secondPaint); }
另外,我们还要计算自己的矩形的大小,这个可以在 onSizeChanged 里面计算。
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // realWidth 和 realHeight 是控件实际可显示的区域的宽和高 int realWidth = w - getPaddingLeft() - getPaddingRight(); int realHeight = h - getPaddingTop() - getPaddingBottom(); float rectFSize, startX, startY; // 我们在画圆,diffWidth 为 画笔宽度的一半 float diffWidth = secondWidth > width ? secondWidth / 2.0f : width / 2.0f; if (realWidth < realHeight) { // 宽度小于高度,以宽度为准 rectFSize = realWidth - diffWidth; startX = getPaddingLeft(); startY = (realHeight - realWidth) / 2.0f + getPaddingTop(); } else { // 宽度大于高度,以高度为准 rectFSize = realHeight - diffWidth; startX = (realWidth - realHeight) / 2.0f + getPaddingLeft(); startY = getPaddingTop(); } rectF = new RectF(startX + diffWidth, startY + diffWidth, startX + rectFSize, startY + rectFSize); }
至此,所有核心工作已经结束,你只需要初始化你的画笔即可。完整的代码如下:
TimeDownView.java
package top.kpromise.ui; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; import android.view.View; import androidx.annotation.Nullable; import top.kpromise.ibase.R; import top.kpromise.utils.CommonTools; public class TimeDownView extends View { private Paint paint; private Paint secondPaint; private RectF rectF; private int progress = 0; private int width; private int secondWidth; private int color; private int secondColor; private int paintStyle; public TimeDownView(Context context) { this(context, null); } public TimeDownView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public TimeDownView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initAttrs(context, attrs); initPaint(); } private void initAttrs(Context context, AttributeSet attrs) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TimeDownView); int defaultWidth = CommonTools.INSTANCE.dp2px(5); width = typedArray.getDimensionPixelSize(R.styleable.TimeDownView_width, defaultWidth); secondWidth = typedArray.getDimensionPixelSize(R.styleable.TimeDownView_secondWidth, defaultWidth); progress = typedArray.getInt(R.styleable.TimeDownView_progress, 0); paintStyle = typedArray.getInt(R.styleable.TimeDownView_paintStyle, 0); color = typedArray.getColor(R.styleable.TimeDownView_color, getResources().getColor(R.color.color_d8d8d8)); secondColor = typedArray.getColor(R.styleable.TimeDownView_secondColor, getResources().getColor(R.color.gray)); typedArray.recycle(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // realWidth 和 realHeight 是控件实际可显示的区域的宽和高 int realWidth = w - getPaddingLeft() - getPaddingRight(); int realHeight = h - getPaddingTop() - getPaddingBottom(); float rectFSize, startX, startY; // 我们在画圆,diffWidth 为 画笔宽度的一半 float diffWidth = secondWidth > width ? secondWidth / 2.0f : width / 2.0f; if (realWidth < realHeight) { // 宽度小于高度,以宽度为准 rectFSize = realWidth - diffWidth; startX = getPaddingLeft(); startY = (realHeight - realWidth) / 2.0f + getPaddingTop(); } else { // 宽度大于高度,以高度为准 rectFSize = realHeight - diffWidth; startX = (realWidth - realHeight) / 2.0f + getPaddingLeft(); startY = getPaddingTop(); } rectF = new RectF(startX + diffWidth, startY + diffWidth, startX + rectFSize, startY + rectFSize); } private Paint.Style getStyle() { if (1 == paintStyle) { return Paint.Style.FILL; } if (2 == paintStyle) { return Paint.Style.FILL_AND_STROKE; } return Paint.Style.STROKE; } private void initPaint() { paint = new Paint(); paint.setColor(color); paint.setStyle(getStyle()); paint.setStrokeWidth(width); secondPaint = new Paint(); secondPaint.setColor(secondColor); secondPaint.setStyle(paint.getStyle()); secondPaint.setStrokeWidth(secondWidth); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); float fromAngle = progress * 3.6f; float startAngle = 270.0f; float sweepAngle; if (progress < 25) { startAngle += fromAngle; sweepAngle = 360 - fromAngle; } else { startAngle = fromAngle - 90; sweepAngle = 270 - startAngle; } canvas.drawArc(rectF, 0, 360, false, paint); canvas.drawArc(rectF, startAngle, sweepAngle, false, secondPaint); } public void onProgress(int progress) { this.progress = progress; postInvalidate(); } }
attrs 里面的内容:
<declare-styleable name="TimeDownView"> <attr name="width" format="dimension|reference" /> <attr name="secondWidth" format="dimension|reference" /> <attr name="color" format="color|reference" /> <attr name="secondColor" format="color|reference" /> <attr name="progress" format="integer|dimension" /> <attr name="paintStyle" format="integer|dimension"> <enum name="STROKE" value="0" /> <enum name="FILL" value="1" /> <enum name="FILL_STROKE" value="2" /> </attr> </declare-styleable>