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

六.最終的效果

在這裏插入圖片描述

源碼下載

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