Android自定义节点进度条NodeProgressBar

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);
    }
}

六.最终的效果

在这里插入图片描述

源码下载

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章