Android自定义节点进度条NodeProgressBar

2023-05-16

NodeProgressBar

一.简介

Android日常开发中我们可能会遇到开发一个带节点的进度条的需求,这个需求看似简单,实际上可以挖掘出不少东西。做的好的话也可以做成相对通用的自定义组件。

二.自定义属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="NodeProgressBar">
        <attr name="nodeCount" format="integer" />
        <attr name="nodeWidth" format="dimension" />
        <attr name="nodeHeight" format="dimension" />
        <attr name="nodeUnreached" format="reference" />
        <attr name="nodeReached" format="reference" />
        <attr name="nodeFinished" format="reference" />
        <attr name="nodeFailed" format="reference" />
        <attr name="nodeRatio" format="float" />

        <attr name="topTxtEnable" format="boolean" />
        <attr name="topTxtSize" format="dimension" />
        <attr name="topTxtColor" format="color" />
        <attr name="topTxtGap" format="dimension" />
        <attr name="topTxtStyle" format="enum">
            <enum name="common" value="0" />
            <enum name="bold" value="1" />
            <enum name="italic" value="2" />
        </attr>

        <attr name="bottomTxtEnable" format="boolean" />
        <attr name="bottomTxtSize" format="dimension" />
        <attr name="bottomTxtColor" format="color" />
        <attr name="bottomTxtGap" format="dimension" />
        <attr name="bottomTxtStyle" format="enum">
            <enum name="common" value="0" />
            <enum name="bold" value="1" />
            <enum name="italic" value="2" />
        </attr>
        <attr name="bottomWarnTxtColor" format="color" />
        <attr name="bottomWarnTxtStyle" format="enum">
            <enum name="common" value="0" />
            <enum name="bold" value="1" />
            <enum name="italic" value="2" />
        </attr>

        <attr name="lineWidth" format="dimension" />
        <attr name="reachedLineColor" format="color" />
        <attr name="unreachedLineColor" format="color" />

        <attr name="regionWidth" format="dimension" />
    </declare-styleable>
</resources>
  • nodeCount——节点个数

  • nodeWidth——节点宽度

  • nodeHeight——节点高度

  • nodeUnreached——未到达节点的图片

  • nodeReached——已到达节点的图片

  • nodeFailed——失败节点的图片

  • nodeFinished——完成节点的图片

  • nodeRatio——节点缩放比,到达或未到达节点比失败或完成节点

  • topTxtEnable——上方文字是否可见

  • topTxtSize——上方文字尺寸

  • topTxtColor——上方文字颜色

  • topTxtGap——上方文字与节点的间距

  • topTxtStyle——上方文字的样式(常规或加粗或斜体)

  • bottomTxtEnable——下方文字是否可见

  • bottomTxtSize——下方文字尺寸

  • bottomTxtColor——下方文字颜色

  • bottomTxtGap——下方文字与节点的间距

  • bottomTxtStyle——下方文字的样式(常规或加粗或斜体)

  • bottomWarnTxtColor——下方提醒文字的颜色,如果节点是失败类型,可能会用到

  • bottomWarnTxtStyle——下方提醒文字的样式

  • lineWidth——连线宽度

  • unreachedLineColor——未到达连线的颜色

  • reachedLineColor——已到达连线的颜色

  • regionWidth——节点区域宽度,该宽度用来计算重要元素的坐标

三.数据定义

/**
 * 节点对象
 */
public static class Node {
    public interface NodeState {
        int UNREACHED = 1;
        int REACHED = 2;
        int FINISHED = 3;
        int FAILED = 4;
    }

    public interface LineState {
        int REACHED = 0;
        int UNREACHED = 1;
    }

    // 节点上方文字
    public String topTxt;
    // 节点下方文字
    public String bottomTxt;
    // 节点状态
    public int nodeState;
    // 节点后连线状态
    public int nodeAfterLineState;
}
  • 一个几点只有topTxt + bottomTxt + nodeState + nodeAfterLineState四个属性
  • NodeState定义了节点的四种类型
  • LineState定义了连接线的两种类型

除了面的各种自定义属性,还支持动态设置以下属性。其中setNodeList()是必须的,将节点列表塞进来才能进行相应的绘制。

/**
 * 上方文字是否生效
 *
 * @param mTopTxtEnable
 */
public void setTopTxtEnable(boolean mTopTxtEnable) {
    this.mTopTxtEnable = mTopTxtEnable;
    invalidate();
}

/**
 * 下方文字是否生效
 *
 * @param mBottomTxtEnable
 */
public void setBottomTxtEnable(boolean mBottomTxtEnable) {
    this.mBottomTxtEnable = mBottomTxtEnable;
    invalidate();
}

/**
 * 设置节点信息
 *
 * @param mNodeList
 */
public void setNodeList(@NonNull List<Node> mNodeList) {
    this.mNodeList = mNodeList;
    this.mNodeCount = mNodeList.size();
    invalidate();
}

四.核心代码

package com.openld.nodeprogressbar.view;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.openld.nodeprogressbar.R;

import java.util.ArrayList;
import java.util.List;

/**
 * author: lllddd
 * created on: 2020/7/1 21:10
 * description:
 */
public class NodeProgressBar extends View {
    // 自定义View宽度
    private int mWidth;
    // 自定义View高度
    private int mHeight;

    // 节点个数
    private int mNodeCount;
    // 节点宽度
    private int mNodeWidth;
    // 节点高度
    private int mNodeHeight;
    // 未到达节点的资源id
    private int mNodeUnreached;
    // 已经到达节点的资源id
    private int mNodeReached;
    // 已完成节点的资源id
    private int mNodeFinished;
    // 失败节点的资源id
    private int mNodeFailed;
    // 节点大小比例,用于处理成功/失败节点比到达/未到达节点大的情况
    private float mNodeRatio;

    // 上方文字是否生效
    private boolean mTopTxtEnable;
    // 上方文字大小
    private int mTopTxtSize;
    // 上方文字颜色
    private int mTopTxtColor;
    // 上方文字距离节点的距离
    private int mTopTxtGap;
    // 上方文字的样式
    private int mTopTxtStyle;

    // 下方文字是否生效
    private boolean mBottomTxtEnable;
    // 下方文字的大小
    private int mBottomTxtSize;
    // 下方文字的颜色
    private int mBottomTxtColor;
    // 下方文字距离节点的距离
    private int mBottomTxtGap;
    // 下方文字的样式
    private int mBottomTxtStyle;

    // 下方提示文字的颜色(失败的节点)
    private int mBottomWarnTxtColor;
    // 相仿提示文字的样式(失败的节点使用)
    private int mBottomWarnTxtStyle;

    // 连接线的宽度
    private int mLineWidth;
    // 已到达的连接线颜色
    private int mReachedLineColor;
    // 未到达的连接线的颜色
    private int mUnreachedLineColor;

    // 节点区域横向宽度
    private int mRegionWidth;

    // 上方文字画笔
    private Paint mPaintTopTxt;
    // 底部文字画笔
    private Paint mPaintBottomTxt;
    // 底部提示文字的画笔
    private Paint mPaintBottomWarnTxt;
    // 节点画笔
    private Paint mPaintNode;
    // 未到达连线画笔
    private Paint mPaintUnreachedLine;
    // 已到达连线画笔
    private Paint mPaintReachedLine;

    // 未到达节点Bitmap
    private Bitmap mNodeUnreachedBitmap;
    // 已到达节点Bitmap
    private Bitmap mNodeReachedBitmap;
    // 失败节点Bitmap
    private Bitmap mNodeFailedBitmap;
    // 已完成节点Bitmap
    private Bitmap mNodeFinishedBitmap;

    // 上方文字的中心坐标列表
    private List<Location> mTopTxtLocationList;
    // 中间节点的中心文字坐标列表
    private List<Location> mNodeLocationList;
    // 下方文字的中心坐标列表
    private List<Location> mBottomTxtLocationList;

    private List<Node> mNodeList = new ArrayList<>();

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

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

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

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.NodeProgressBar);

        mNodeCount = ta.getInt(R.styleable.NodeProgressBar_nodeCount, 0);
        mNodeWidth = ta.getDimensionPixelSize(R.styleable.NodeProgressBar_nodeWidth, 0);
        mNodeHeight = ta.getDimensionPixelSize(R.styleable.NodeProgressBar_nodeHeight, 0);
        mNodeUnreached = ta.getResourceId(R.styleable.NodeProgressBar_nodeUnreached, R.drawable.node_unreached);
        mNodeReached = ta.getResourceId(R.styleable.NodeProgressBar_nodeReached, R.drawable.node_unreached);
        mNodeFinished = ta.getResourceId(R.styleable.NodeProgressBar_nodeFinished, R.drawable.node_unreached);
        mNodeFailed = ta.getResourceId(R.styleable.NodeProgressBar_nodeFailed, R.drawable.node_unreached);
        mNodeRatio = ta.getFloat(R.styleable.NodeProgressBar_nodeRatio, 1.0F);

        mTopTxtEnable = ta.getBoolean(R.styleable.NodeProgressBar_topTxtEnable, false);
        mTopTxtSize = ta.getDimensionPixelSize(R.styleable.NodeProgressBar_topTxtSize, 0);
        mTopTxtColor = ta.getColor(R.styleable.NodeProgressBar_topTxtColor, Color.TRANSPARENT);
        mTopTxtGap = ta.getDimensionPixelSize(R.styleable.NodeProgressBar_topTxtGap, 0);
        mTopTxtStyle = ta.getInteger(R.styleable.NodeProgressBar_topTxtStyle, TxtStyle.BOLD);

        mBottomTxtEnable = ta.getBoolean(R.styleable.NodeProgressBar_bottomTxtEnable, false);
        mBottomTxtSize = ta.getDimensionPixelSize(R.styleable.NodeProgressBar_bottomTxtSize, 0);
        mBottomTxtColor = ta.getColor(R.styleable.NodeProgressBar_bottomTxtColor, Color.TRANSPARENT);
        mBottomTxtGap = ta.getDimensionPixelSize(R.styleable.NodeProgressBar_bottomTxtGap, 0);
        mBottomTxtStyle = ta.getInteger(R.styleable.NodeProgressBar_bottomTxtStyle, TxtStyle.COMMON);

        mBottomWarnTxtColor = ta.getColor(R.styleable.NodeProgressBar_bottomWarnTxtColor, Color.TRANSPARENT);
        mBottomWarnTxtStyle = ta.getInteger(R.styleable.NodeProgressBar_bottomWarnTxtStyle, TxtStyle.COMMON);

        mLineWidth = ta.getDimensionPixelSize(R.styleable.NodeProgressBar_lineWidth, 0);
        mReachedLineColor = ta.getColor(R.styleable.NodeProgressBar_reachedLineColor, Color.TRANSPARENT);
        mUnreachedLineColor = ta.getColor(R.styleable.NodeProgressBar_unreachedLineColor, Color.TRANSPARENT);

        mRegionWidth = ta.getDimensionPixelSize(R.styleable.NodeProgressBar_regionWidth, 0);

        ta.recycle();

        configBitmaps(context);
        configPaints();
    }

    /**
     * 配置画笔
     */
    private void configPaints() {
        // 上方文字画笔属性设置
        configTopTxtPaint();
        // 下方文字画笔属性设置
        configBottomTxtPaint();
        // 下方提示文字画笔属性设置
        configBottomWarnTxtPaint();
        // 节点画笔属性设置
        configNodePaint();
        // 未到达连接线画笔属性设置
        configUnreachedLinePaint();
        // 已到达连接线画笔属性设置
        configReachedLinePaint();
    }

    /**
     * 已到达连接线画笔属性设置
     */
    private void configReachedLinePaint() {
        mPaintReachedLine = new Paint();
        mPaintReachedLine.setColor(mReachedLineColor);
        mPaintReachedLine.setStrokeWidth(mLineWidth);
        mPaintReachedLine.setStyle(Paint.Style.FILL);
        mPaintReachedLine.setAntiAlias(true);
    }

    /**
     * 未到达连接线画笔属性设置
     */
    private void configUnreachedLinePaint() {
        mPaintUnreachedLine = new Paint();
        mPaintUnreachedLine.setColor(mUnreachedLineColor);
        mPaintUnreachedLine.setStrokeWidth(mLineWidth);
        mPaintUnreachedLine.setStyle(Paint.Style.FILL);
        mPaintUnreachedLine.setAntiAlias(true);
    }

    /**
     * 节点画笔属性设置
     */
    private void configNodePaint() {
        mPaintNode = new Paint();
        mPaintNode.setAntiAlias(true);
    }

    /**
     * 下方提示文字画笔属性设置
     */
    private void configBottomWarnTxtPaint() {
        mPaintBottomWarnTxt = new Paint();
        mPaintBottomWarnTxt.setTextSize(mBottomTxtSize);
        mPaintBottomWarnTxt.setColor(mBottomWarnTxtColor);
        mPaintBottomWarnTxt.setTextAlign(Paint.Align.CENTER);
        mPaintBottomWarnTxt.setAntiAlias(true);
        if (TxtStyle.COMMON == mBottomWarnTxtStyle) {
            mPaintBottomWarnTxt.setTypeface(Typeface.DEFAULT);
        } else if (TxtStyle.BOLD == mBottomWarnTxtStyle) {
            mPaintBottomWarnTxt.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
        } else if (TxtStyle.ITALIC == mBottomWarnTxtStyle) {
            mPaintBottomWarnTxt.setTypeface(Typeface.defaultFromStyle(Typeface.ITALIC));
        }
    }

    /**
     * 下方文字画笔属性设置
     */
    private void configBottomTxtPaint() {
        mPaintBottomTxt = new Paint();
        mPaintBottomTxt.setTextSize(mBottomTxtSize);
        mPaintBottomTxt.setColor(mBottomTxtColor);
        mPaintBottomTxt.setTextAlign(Paint.Align.CENTER);
        mPaintBottomTxt.setAntiAlias(true);
        if (TxtStyle.COMMON == mBottomTxtStyle) {
            mPaintBottomTxt.setTypeface(Typeface.DEFAULT);
        } else if (TxtStyle.BOLD == mBottomTxtStyle) {
            mPaintBottomTxt.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
        } else if (TxtStyle.ITALIC == mBottomTxtStyle) {
            mPaintBottomTxt.setTypeface(Typeface.defaultFromStyle(Typeface.ITALIC));
        }
    }

    /**
     * 上方文字画笔属性设置
     */
    private void configTopTxtPaint() {
        mPaintTopTxt = new Paint();
        mPaintTopTxt.setTextSize(mTopTxtSize);
        mPaintTopTxt.setColor(mTopTxtColor);
        mPaintTopTxt.setTextAlign(Paint.Align.CENTER);
        mPaintTopTxt.setAntiAlias(true);
        if (TxtStyle.COMMON == mTopTxtStyle) {
            mPaintTopTxt.setTypeface(Typeface.DEFAULT);
        } else if (TxtStyle.BOLD == mTopTxtStyle) {
            mPaintTopTxt.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
        } else if (TxtStyle.ITALIC == mTopTxtStyle) {
            mPaintTopTxt.setTypeface(Typeface.defaultFromStyle(Typeface.ITALIC));
        }
    }

    /**
     * 配置bitmap
     *
     * @param context
     */
    private void configBitmaps(Context context) {
        Resources resources = context.getResources();
        mNodeUnreachedBitmap = BitmapFactory.decodeResource(resources, R.drawable.node_unreached);
        mNodeReachedBitmap = BitmapFactory.decodeResource(resources, R.drawable.node_reached);
        mNodeFailedBitmap = BitmapFactory.decodeResource(resources, R.drawable.node_failed);
        mNodeFinishedBitmap = BitmapFactory.decodeResource(resources, R.drawable.node_finished);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mWidth = MeasureSpec.getSize(widthMeasureSpec);
        mHeight = MeasureSpec.getSize(heightMeasureSpec);
//        if (mTopTxtEnable && mBottomTxtEnable) {// 上下文字都展示
//            mHeight = mTopTxtSize + mTopTxtGap + mNodeHeight + mBottomTxtGap + mBottomTxtSize;
//        } else if (mTopTxtEnable) {// 仅上方文字展示
//            mHeight = mTopTxtSize + mTopTxtGap + mNodeHeight;
//        } else if (mBottomTxtEnable) {// 仅下方文字展示
//            mHeight = mNodeHeight + mBottomTxtGap + mBottomTxtSize;
//        } else {// 不展示上下文字
//            mHeight = mNodeHeight;
//        }
        // 上线各加1dp的余量,防止个别情况下展示不全
        setMeasuredDimension(mWidth, mHeight);
    }

    @Override
    @SuppressLint("DrawAllocation")
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (mNodeCount <= 0 || mNodeList == null || mNodeList.isEmpty() || mNodeList.size() != mNodeCount) {
            return;
        }

        // 初始化位置列表
        initLocationLists();

        // 测量坐标
        measureLocations();

        // 绘制上方文字
        if (mTopTxtEnable) {
            for (int i = 0; i < mNodeCount; i++) {
                Node node = mNodeList.get(i);
                if (TextUtils.isEmpty(node.topTxt)) {
                    continue;
                }
                Paint.FontMetrics metrics = mPaintTopTxt.getFontMetrics();
                int x = mTopTxtLocationList.get(i).x;
                int y = (int) (mTopTxtLocationList.get(i).y + Math.abs(mPaintTopTxt.ascent() + mPaintTopTxt.descent() / 2));
                canvas.drawText(node.topTxt, x, y, mPaintTopTxt);
            }
        }

        // 绘制连线
        for (int i = 0; i < mNodeCount; i++) {
            Node node = mNodeList.get(i);

            if (i == mNodeCount - 1) {
                break;
            }

            int x1 = mNodeLocationList.get(i).x;
            int y1 = mNodeLocationList.get(i).y;
            int x2 = mNodeLocationList.get(i + 1).x;
            int y2 = mNodeLocationList.get(i + 1).y;
            if (Node.LineState.UNREACHED == node.nodeAfterLineState) {
                canvas.drawLine(x1, y1, x2, y2, mPaintUnreachedLine);
            } else if (Node.LineState.REACHED == node.nodeAfterLineState) {
                canvas.drawLine(x1, y1, x2, y2, mPaintReachedLine);
            } else {
                canvas.drawLine(x1, y1, x2, y2, mPaintUnreachedLine);
            }
        }

        // 绘制节点
        for (int i = 0; i < mNodeCount; i++) {
            Node node = mNodeList.get(i);

            int x = mNodeLocationList.get(i).x;
            int y = mNodeLocationList.get(i).y;
            if (Node.NodeState.UNREACHED == node.nodeState) {
                Rect rect = new Rect(0, 0, mNodeUnreachedBitmap.getWidth(), mNodeUnreachedBitmap.getHeight());
                RectF rectF = new RectF(x - mNodeRatio * mNodeWidth / 2, y - mNodeRatio * mNodeHeight / 2, x + mNodeRatio * mNodeWidth / 2, y + mNodeRatio * mNodeHeight / 2);
                canvas.drawBitmap(mNodeUnreachedBitmap, rect, rectF, mPaintNode);
            } else if (Node.NodeState.REACHED == node.nodeState) {
                Rect rect = new Rect(0, 0, mNodeUnreachedBitmap.getWidth(), mNodeUnreachedBitmap.getHeight());
                RectF rectF = new RectF(x - mNodeRatio * mNodeWidth / 2, y - mNodeRatio * mNodeHeight / 2, x + mNodeRatio * mNodeWidth / 2, y + mNodeRatio * mNodeHeight / 2);
                canvas.drawBitmap(mNodeReachedBitmap, rect, rectF, mPaintNode);
            } else if (Node.NodeState.FAILED == node.nodeState) {
                Rect rect = new Rect(0, 0, mNodeUnreachedBitmap.getWidth(), mNodeUnreachedBitmap.getHeight());
                RectF rectF = new RectF(x - 1.0F * mNodeWidth / 2, y - 1.0F * mNodeHeight / 2, x + 1.0F * mNodeWidth / 2, y + mNodeHeight / 2);
                canvas.drawBitmap(mNodeFailedBitmap, rect, rectF, mPaintNode);
            } else if (Node.NodeState.FINISHED == node.nodeState) {
                Rect rect = new Rect(0, 0, mNodeUnreachedBitmap.getWidth(), mNodeUnreachedBitmap.getHeight());
                RectF rectF = new RectF(x - 1.0F * mNodeWidth / 2, y - 1.0F * mNodeHeight / 2, x + 1.0F * mNodeWidth / 2, y + 1.0F * mNodeHeight / 2);
                canvas.drawBitmap(mNodeFinishedBitmap, rect, rectF, mPaintNode);
            }
        }

        // 绘制下方文字
        if (mBottomTxtEnable) {
            for (int i = 0; i < mNodeCount; i++) {
                Node node = mNodeList.get(i);
                if (TextUtils.isEmpty(node.bottomTxt)) {
                    continue;
                }
                int x = mBottomTxtLocationList.get(i).x;
                int y = (int) (mBottomTxtLocationList.get(i).y + Math.abs(mPaintBottomTxt.ascent() + mPaintBottomTxt.descent() / 2));
                if (Node.NodeState.FAILED != node.nodeState) {
                    canvas.drawText(node.bottomTxt, x, y, mPaintBottomTxt);
                } else {
                    canvas.drawText(node.bottomTxt, x, y, mPaintBottomWarnTxt);
                }
            }
        }
    }

    private void initLocationLists() {
        if (mTopTxtLocationList != null) {
            mTopTxtLocationList.clear();
        } else {
            mTopTxtLocationList = new ArrayList<>();
        }

        if (mNodeLocationList != null) {
            mNodeLocationList.clear();
        } else {
            mNodeLocationList = new ArrayList<>();
        }

        if (mBottomTxtLocationList != null) {
            mBottomTxtLocationList.clear();
        } else {
            mBottomTxtLocationList = new ArrayList<>();
        }
    }

    /**
     * 测量元素的中心坐标
     */
    private void measureLocations() {
        if (mNodeCount == 1) {
            // 上方文字的中心坐标
            if (mTopTxtEnable) {
                Location topTxtLoc = new Location();
                topTxtLoc.x = mWidth / 2;
                topTxtLoc.y = mTopTxtSize / 2;
                mTopTxtLocationList.add(topTxtLoc);
            }

            // 节点的中心坐标
            if (mTopTxtEnable) {
                Location nodeLoc = new Location();
                nodeLoc.x = mWidth / 2;
                nodeLoc.y = mTopTxtSize + mTopTxtGap + mNodeHeight / 2;
                mNodeLocationList.add(nodeLoc);
            } else {
                Location nodeLoc = new Location();
                nodeLoc.x = mWidth / 2;
                nodeLoc.y = mNodeHeight / 2;
                mNodeLocationList.add(nodeLoc);
            }

            // 下方文字的中心坐标
            if (mTopTxtEnable && mBottomTxtEnable) {
                Location bottomTxtLoc = new Location();
                bottomTxtLoc.x = mWidth / 2;
                bottomTxtLoc.y = mTopTxtSize + mTopTxtGap + mNodeHeight + mBottomTxtGap + mBottomTxtSize / 2;
                mBottomTxtLocationList.add(bottomTxtLoc);
            } else if (mBottomTxtEnable) {
                Location bottomTxtLoc = new Location();
                bottomTxtLoc.x = mWidth / 2;
                bottomTxtLoc.y = mNodeHeight + mBottomTxtGap + mBottomTxtSize / 2;
                mBottomTxtLocationList.add(bottomTxtLoc);
            }
            return;
        }

        int space = (mWidth - mRegionWidth * mNodeCount) / (mNodeCount - 1);
        for (int i = 0; i < mNodeCount; i++) {
            // 上方文字的中心坐标
            if (mTopTxtEnable) {
                Location topTxtLoc = new Location();
                topTxtLoc.x = mRegionWidth / 2 + i * space + i * mRegionWidth;
                topTxtLoc.y = mTopTxtSize / 2;
                mTopTxtLocationList.add(topTxtLoc);
            }

            // 节点的中心坐标
            if (mTopTxtEnable) {
                Location nodeLoc = new Location();
                nodeLoc.x = mRegionWidth / 2 + i * space + i * mRegionWidth;
                nodeLoc.y = mTopTxtSize + mTopTxtGap + mNodeHeight / 2;
                mNodeLocationList.add(nodeLoc);
            } else {
                Location nodeLoc = new Location();
                nodeLoc.x = mRegionWidth / 2 + i * space + i * mRegionWidth;
                nodeLoc.y = mNodeHeight / 2;
                mNodeLocationList.add(nodeLoc);
            }

            // 下方文字的中心坐标
            if (mTopTxtEnable && mBottomTxtEnable) {
                Location bottomTxtLoc = new Location();
                bottomTxtLoc.x = mRegionWidth / 2 + i * space + i * mRegionWidth;
                bottomTxtLoc.y = mTopTxtSize + mTopTxtGap + mNodeHeight + mBottomTxtGap + mBottomTxtSize / 2;
                mBottomTxtLocationList.add(bottomTxtLoc);
            } else if (mBottomTxtEnable) {
                Location bottomTxtLoc = new Location();
                bottomTxtLoc.x = mRegionWidth / 2 + i * space + i * mRegionWidth;
                bottomTxtLoc.y = mNodeHeight + mBottomTxtGap + mBottomTxtSize / 2;
                mBottomTxtLocationList.add(bottomTxtLoc);
            }
        }
    }

    /**
     * 上方文字是否生效
     *
     * @param mTopTxtEnable
     */
    public void setTopTxtEnable(boolean mTopTxtEnable) {
        this.mTopTxtEnable = mTopTxtEnable;
        invalidate();
    }

    /**
     * 下方文字是否生效
     *
     * @param mBottomTxtEnable
     */
    public void setBottomTxtEnable(boolean mBottomTxtEnable) {
        this.mBottomTxtEnable = mBottomTxtEnable;
        invalidate();
    }

    /**
     * 设置节点信息
     *
     * @param mNodeList
     */
    public void setNodeList(@NonNull List<Node> mNodeList) {
        this.mNodeList = mNodeList;
        this.mNodeCount = mNodeList.size();
        invalidate();
    }

    /**
     * 中心坐标
     */
    private static class Location {
        int x;
        int y;
    }

    /**
     * 节点对象
     */
    public static class Node {
        public interface NodeState {
            int UNREACHED = 1;
            int REACHED = 2;
            int FINISHED = 3;
            int FAILED = 4;
        }

        public interface LineState {
            int REACHED = 0;
            int UNREACHED = 1;
        }

        // 节点上方文字
        public String topTxt;
        // 节点下方文字
        public String bottomTxt;
        // 节点状态
        public int nodeState;
        // 节点后连线状态
        public int nodeAfterLineState;
    }

    /**
     * 字体
     */
    public interface TxtStyle {
        int COMMON = 0;
        int BOLD = 1;
        int ITALIC = 2;
    }
}

五.使用示例

5.1 activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.openld.nodeprogressbar.view.NodeProgressBar
        android:id="@+id/node_progress_bar1"
        android:layout_width="0dp"
        android:layout_height="100dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginRight="8dp"
        app:bottomTxtColor="#444444"
        app:bottomTxtEnable="true"
        app:bottomTxtGap="5dp"
        app:bottomTxtSize="14sp"
        app:bottomTxtStyle="common"
        app:bottomWarnTxtColor="@color/colorOrange"
        app:bottomWarnTxtStyle="italic"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:lineWidth="2dp"
        app:nodeCount="5"
        app:nodeFailed="@drawable/node_failed"
        app:nodeFinished="@drawable/node_finished"
        app:nodeHeight="20dp"
        app:nodeRatio="0.8"
        app:nodeReached="@drawable/node_reached"
        app:nodeUnreached="@drawable/node_unreached"
        app:nodeWidth="20dp"
        app:reachedLineColor="@color/colorAccent"
        app:regionWidth="48dp"
        app:topTxtColor="#000000"
        app:topTxtEnable="true"
        app:topTxtGap="15dp"
        app:topTxtSize="16sp"
        app:topTxtStyle="bold"
        app:unreachedLineColor="#AAAAAA" />

    <com.openld.nodeprogressbar.view.NodeProgressBar
        android:id="@+id/node_progress_bar2"
        android:layout_width="0dp"
        android:layout_height="100dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginRight="8dp"
        app:bottomTxtColor="#444444"
        app:bottomTxtEnable="true"
        app:bottomTxtGap="5dp"
        app:bottomTxtSize="14sp"
        app:bottomTxtStyle="common"
        app:bottomWarnTxtColor="@color/colorOrange"
        app:bottomWarnTxtStyle="italic"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/node_progress_bar1"
        app:lineWidth="2dp"
        app:nodeCount="5"
        app:nodeFailed="@drawable/node_failed"
        app:nodeFinished="@drawable/node_finished"
        app:nodeHeight="20dp"
        app:nodeRatio="0.8"
        app:nodeReached="@drawable/node_reached"
        app:nodeUnreached="@drawable/node_unreached"
        app:nodeWidth="20dp"
        app:reachedLineColor="@color/colorAccent"
        app:regionWidth="48dp"
        app:topTxtColor="#000000"
        app:topTxtEnable="true"
        app:topTxtGap="15dp"
        app:topTxtSize="16sp"
        app:topTxtStyle="bold"
        app:unreachedLineColor="#AAAAAA" />
</androidx.constraintlayout.widget.ConstraintLayout>

5.2 MainActivity.java

package com.openld.nodeprogressbar;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

import com.openld.nodeprogressbar.view.NodeProgressBar;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    private NodeProgressBar mNodeProgressBar1;
    private NodeProgressBar mNodeProgressBar2;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mNodeProgressBar1 = findViewById(R.id.node_progress_bar1);
        mNodeProgressBar2 = findViewById(R.id.node_progress_bar2);

        initNodeProgressBar1();
        initNodeProgressBar2();
    }

    private void initNodeProgressBar2() {
        List<NodeProgressBar.Node> nodeList = new ArrayList<>();

        NodeProgressBar.Node node1 = new NodeProgressBar.Node();
        node1.nodeState = NodeProgressBar.Node.NodeState.REACHED;
        node1.nodeAfterLineState = NodeProgressBar.Node.LineState.REACHED;
        node1.topTxt = "青铜";
        node1.bottomTxt = "入门";
        nodeList.add(node1);

        NodeProgressBar.Node node2 = new NodeProgressBar.Node();
        node2.nodeState = NodeProgressBar.Node.NodeState.REACHED;
        node2.nodeAfterLineState = NodeProgressBar.Node.LineState.REACHED;
        node2.topTxt = "白银";
        node2.bottomTxt = "初级";
        nodeList.add(node2);

        NodeProgressBar.Node node3 = new NodeProgressBar.Node();
        node3.nodeState = NodeProgressBar.Node.NodeState.REACHED;
        node3.nodeAfterLineState = NodeProgressBar.Node.LineState.REACHED;
        node3.topTxt = "黄金";
        node3.bottomTxt = "中级";
        nodeList.add(node3);

        NodeProgressBar.Node node4 = new NodeProgressBar.Node();
        node4.nodeState = NodeProgressBar.Node.NodeState.REACHED;
        node4.nodeAfterLineState = NodeProgressBar.Node.LineState.REACHED;
        node4.topTxt = "钻石";
        node4.bottomTxt = "高级";
        nodeList.add(node4);

        NodeProgressBar.Node node5 = new NodeProgressBar.Node();
        node5.nodeState = NodeProgressBar.Node.NodeState.FINISHED;
        node5.nodeAfterLineState = NodeProgressBar.Node.LineState.REACHED;
        node5.topTxt = "星耀";
        node5.bottomTxt = "专家";
        nodeList.add(node5);

        mNodeProgressBar2.setNodeList(nodeList);

    }

    private void initNodeProgressBar1() {
        List<NodeProgressBar.Node> nodeList = new ArrayList<>();

        NodeProgressBar.Node node1 = new NodeProgressBar.Node();
        node1.nodeState = NodeProgressBar.Node.NodeState.REACHED;
        node1.nodeAfterLineState = NodeProgressBar.Node.LineState.REACHED;
        node1.topTxt = "青铜";
        node1.bottomTxt = "入门";
        nodeList.add(node1);

        NodeProgressBar.Node node2 = new NodeProgressBar.Node();
        node2.nodeState = NodeProgressBar.Node.NodeState.REACHED;
        node2.nodeAfterLineState = NodeProgressBar.Node.LineState.REACHED;
        node2.topTxt = "白银";
        node2.bottomTxt = "初级";
        nodeList.add(node2);

        NodeProgressBar.Node node3 = new NodeProgressBar.Node();
        node3.nodeState = NodeProgressBar.Node.NodeState.FAILED;
        node3.nodeAfterLineState = NodeProgressBar.Node.LineState.REACHED;
        node3.topTxt = "黄金";
        node3.bottomTxt = "中级";
        nodeList.add(node3);

        NodeProgressBar.Node node4 = new NodeProgressBar.Node();
        node4.nodeState = NodeProgressBar.Node.NodeState.UNREACHED;
        node4.nodeAfterLineState = NodeProgressBar.Node.LineState.UNREACHED;
        node4.topTxt = "钻石";
        node4.bottomTxt = "高级";
        nodeList.add(node4);

        NodeProgressBar.Node node5 = new NodeProgressBar.Node();
        node5.nodeState = NodeProgressBar.Node.NodeState.UNREACHED;
        node5.nodeAfterLineState = NodeProgressBar.Node.LineState.UNREACHED;
        node5.topTxt = "星耀";
        node5.bottomTxt = "专家";
        nodeList.add(node5);

        mNodeProgressBar1.setNodeList(nodeList);
    }
}

六.最终的效果

在这里插入图片描述

源码下载

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android自定义节点进度条NodeProgressBar 的相关文章

随机推荐

  • 基于Matlab的Robotics Toolbox工具箱的机器人仿真函数介绍(运动学)

    前言 随着我们了解到机器人如何建立运动学模型和动力学模型之后 xff0c 我们可以使用Matlab中的仿真工具箱内来对模型的准确性进行验证 xff0c 并且可以通过内置的函数进行简单的轨迹规划和可视化观察 xff0c 本节涉及到的工具箱是M
  • 基于Matlab的Robotics Toolbox工具箱的机器人仿真函数介绍(空间位姿表示与动力学)

    文章目录 前言一 空间位姿描述1 二维空间2 三维空间3 旋转的不同表示方法1 xff09 欧拉角2 xff09 RPY角3 xff09 双向量表示4 xff09 轴与旋转角5 xff09 四元数表示 二 动力学1 动力学参数2 正动力学函
  • 平面2R机器人的运动学/动力学建模实例

    文章目录 前言平面2R机器人1 问题假设2 建立运动学模型1 xff09 DH参数法2 xff09 指数积方法3 xff09 雅可比矩阵 xff08 微分变换法 xff09 4 xff09 逆运动学求解 xff08 几何法 xff09 3
  • 2023最新jmeter接口测试入门到精通实战讲解,手把手教学

    一 线程组 线程组元件是任何一个测试计划的开始点 在一个测试计划中的所有元件都必须在某个线程组下 所有的任务都是基于线程组 xff1a 通俗理解 xff1a 线程组 xff1a 就是一个线程组 xff0c 里面有若干个请求 xff1b 线程
  • 2023最新全方面了解接口自动化,看完还不会你锤我

    一 自动化分类 现在流行的是金字塔状的分层测试 xff0c 将测试从上到下分为UI测试层 接口测试层 单元测试层三层 在传统的UI自动化的基础之上更多实施基于代码的低级别自动化测试 xff0c 而不仅仅通过用户界面进行端到端的测试 按照测试
  • pytest接口自动化测试框架搭建

    fixture 特点 xff1a 命令灵活 xff1a 对于setup xff0c teardown可以省略 数据共享 xff1a 在conftest py配置里写方法可以实现数据共享 xff0c 不需要import导入 xff0c 可以跨
  • App自动化测试怎么做?实战分享App自动化测试全流程

    一 什么是app测试 xff1f 什么是app自动化测试 xff1f 概念 xff1a 所谓app测试也称之为移动测试 xff0c 通俗易懂的理解就是测试我们平时手机使用的程序 那什么是app自动化测试呢 xff1f 通常情况下是随app产
  • 究极版-计算机四级错题集【操作系统单选题】

    计算机四级究极版错题集 1 解析 按照资源管理的观点 xff0c 操作系统的功能主要可以分为进程线程管理 xff08 处理器管理 xff09 存储管理 文件管理 作业管理和设备管理 2 若用户编程需要打印输出 xff0c 需要系统调用wri
  • 【干货分享】2023美团软件测试面试题汇总

    前言 本篇分享的软件测试面试题内容主要包括 xff1a 测试总体 需求分析 测试计划 测试策略 测试用例 缺陷报告 测试总结报告 白盒测试 单元测试 集成测试 系统测试 验收测试等等26个模块 https www bilibili com
  • 三面百度软件测试岗,都被问到自闭

    1 HR已读不回问题分析以及如何解决 哔哩哔哩 bilibili 1 HR已读不回问题分析以及如何解决是2023最新软件测试面试大全看完offer拿到手软的第1集视频 xff0c 该合集共计21集 xff0c 视频收藏或关注UP主 xff0
  • 抖音软件测试三面,21个面试题复盘

    2023最新软件测试面试大全看完offer拿到手软 哔哩哔哩 bilibili 2023最新软件测试面试大全看完offer拿到手软共计21条视频 xff0c 包括 xff1a 1 HR已读不回问题分析以及如何解决 2 HR已读不回之针对性进
  • 大环境不好难找工作?三面阿里,幸好做足了准备,已拿offer

    这边推荐你去看一下这套专门讲解面试和简历的视频 xff0c 主打面试题 xff0c 接口 web app全套视频面试题 xff0c 还有配套的笔记 xff01 这个视频可以说是B站百万播放全网第一的面试教程 xff0c 同时在线人数到达10
  • 接口自动化测试面试题大全(合适各级软件测试人员)

    这边推荐你去看一下这套专门讲解面试和简历的视频 xff0c 主打面试题 xff0c 接口 web app全套视频面试题 xff0c 还有配套的笔记 xff01 这个视频可以说是B站百万播放全网第一的面试教程 xff0c 同时在线人数到达10
  • 阿里90道常问面试题(软件测试岗位)

    这边推荐你去看一下这套专门讲解面试和简历的视频 xff0c 主打面试题 xff0c 接口 web app全套视频面试题 xff0c 还有配套的笔记 xff01 这个视频可以说是B站百万播放全网第一的面试教程 xff0c 同时在线人数到达10
  • curl 发送json格式数据 请求

    curl H 34 Content Type application json 34 X POST data 39 34 userID 34 10001 39 http localhost 8085 GetUserInfo
  • 在keil中使用头文件实现多文件编程

    如上图所示 xff0c 在这里 xff0c MAX7219driver c为将被包含的源文件 xff0c max7219 h为对应MAX7219driver c的头文件 xff0c 而 xff08 驱动测试 xff09 显示PZ 12234
  • PHP中使用CURL之php curl详细解析和常见大坑

    这篇文章主要介绍了PHP中使用CURL之php curl详细解析和常见大坑 xff0c 现在分享给大家 xff0c 也给大家做个参考 一起跟随小编过来看看吧 七夕啦 xff0c 作为开发 xff0c 妹子没得撩就 撩 下服务器吧 xff0c
  • KMP字符串

    给定一个字符串 S xff0c 以及一个模式串 P xff0c 所有字符串中只包含大小写英文字母以及阿拉伯数字 模式串 P 在字符串 S 中多次作为子串出现 求出模式串P在字符串S中所有出现的位置的起始下标 输入格式 第一行输入整数 N x
  • 一个简单实用的分离器件锂电池充电电路

    下面推荐一个由分离器件搭建的锂电池充电电路 xff0c 如下图 简单说明一下各器件的功能及电路原理 xff1a 简单说明一下各器件的功能及电路原理 F 43 为充电器的正极 xff0c BT 43 为电池正极 xff0c CH与单片机的一个
  • Android自定义节点进度条NodeProgressBar

    NodeProgressBar 一 简介 Android日常开发中我们可能会遇到开发一个带节点的进度条的需求 xff0c 这个需求看似简单 xff0c 实际上可以挖掘出不少东西 做的好的话也可以做成相对通用的自定义组件 二 自定义属性 sp