Android

Android 自定义 View 入门

说来惭愧,工作数年,连基本的自定义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>

 

full-stack-trip

Share
Published by
full-stack-trip

Recent Posts

retrofit 同时支持 xml 和 json

retrofit 解析 jso…

4 年 ago

mysql - 存储过程 从入门到放弃

最近有个报表的需求,于是乎用了…

4 年 ago

奶嘴战略 - 你不得不知道的扎心真相(一)

一句:英雄枯骨无人问,戏子家事…

4 年 ago

acme.sh 的简单使用

acme.sh 是纯 shel…

4 年 ago

wrk -更现代化的http压测工具

wrk 是一款更现代化的 ht…

4 年 ago

java 枚举类实现的单例与 spring boot 枚举类依赖注入

我们知道,在单例的诸多实现里,…

4 年 ago