Android Custom View - A refreshing, compact and flexible multi-node progress bar

Preface

Recently, there is a small need for a node progress bar in the project. When it is finished, I want to share it with the students who need it.

Real machine effect

Custom View Complete Code

Out-of-the-box~, notes have been detailed

/**
 * @description: Node Progress Bar
 * @author: DMingO
 * @date: 2020/4/15
 */
public class PointProcessBar extends View {

    /**
     * Connection Brush When Not Selected
     */
    private Paint mLinePaint;
    /**
     * Connection Brush When Selected
     */
    private Paint mLineSelectedPaint;
    /**
     * Text Brush When Not Selected
     */
    private Paint mTextPaint;
    /**
     * Text Brush When Selected
     */
    private Paint mTextSelPaint;

    /**
     * Solid Circle Brush When Not Selected
     */
    private Paint mCirclePaint;
    /**
     * Internal Solid Circle Brush When Selected
     */
    private Paint mCircleSelPaint;
    /**
     * Border Circle Brush When Selected
     */
    private Paint mCircleStrokeSelPaint;

    /**
     * Line, Node Circle Color when Not Selected
     */
    private int mColorUnselected  = Color.parseColor("#1ca8b0d9");
    /**
     * Color when selected
     */
    private int mColorSelected = Color.parseColor("#61A4E4");
    /**
     * Unselected Text Color
     */
    private int mColorTextUnselected  = Color.parseColor("#5c030f09");

    /**
     * Number of nodes drawn, controlled by the number of bottom node titles
     */
    int circleCount ;

    /**
     * Height of the connection
     */
    float mLineHeight = 7f;

    //The diameter of a circle
    float mCircleHeight = 50f;
    float mCircleSelStroke = 8f;
    float mCircleFillRadius = 15f;

    //Text size
    float mTextSize  = 35f;

    //Distance of text from top
    float mMarginTop = 40f;
    /**
     * The distance from the first circle to the center
     */
    float marginLeft = 30f;

    /**
     * The distance from the last circle to the center
     */
    float marginRight = marginLeft;

    /**
     * Distance between each node
     */
    float divideWidth;

    int defaultHeight;

    /**
     * Text list at the bottom of the node
     */
    List<String> textList = new ArrayList<>();

    /**
     * A rectangle of the same width and height as text, used to measure text
     */
    List<Rect> mBounds;
    /**
     * Stores x-coordinate values of node circles with each center on the same straight line
     */
    List<Float> circleLineJunctions = new ArrayList<>();
    /**
     * Selected Items Collection
     */
    Set<Integer> selectedIndexSet = new HashSet<>();

    public PointProcessBar(Context context) {
        super(context);
    }

    public PointProcessBar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }

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

    public PointProcessBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    /**
     * Initialize Brush Properties
     */
    private void initPaint(){

        mLinePaint = new Paint();
        mLineSelectedPaint = new Paint();
        mCirclePaint = new Paint();
        mTextPaint = new Paint();
        mCircleStrokeSelPaint = new Paint();
        mTextSelPaint=new Paint();
        mCircleSelPaint = new Paint();

        mLinePaint.setColor(mColorDef);
        //Set Fill
        mLinePaint.setStyle(Paint.Style.FILL);
        //Pen Width Pixels
        mLinePaint.setStrokeWidth(mLineHeight);
        //Sawtooth not shown
        mLinePaint.setAntiAlias(true);

        mLineSelectedPaint.setColor(mColorSelected);
        mLineSelectedPaint.setStyle(Paint.Style.FILL);
        mLineSelectedPaint.setStrokeWidth(mLineHeight);
        mLineSelectedPaint.setAntiAlias(true);

        mCirclePaint.setColor(mColorDef);
        //Set Fill
        mCirclePaint.setStyle(Paint.Style.FILL);
        //Pen Width Pixels
        mCirclePaint.setStrokeWidth(1);
        //Sawtooth not shown
        mCirclePaint.setAntiAlias(true);

        //Outline Hollow Circle Brush When Selected
        mCircleStrokeSelPaint.setColor(mColorSelected);
        mCircleStrokeSelPaint.setStyle(Paint.Style.STROKE);
        mCircleStrokeSelPaint.setStrokeWidth(mCircleSelStroke);
        mCircleStrokeSelPaint.setAntiAlias(true);
        //Inside Fill Circle Brush When Selected
        mCircleSelPaint.setStyle(Paint.Style.FILL);
        mCircleSelPaint.setStrokeWidth(1);
        mCircleSelPaint.setAntiAlias(true);
        mCircleSelPaint.setColor(mColorSelected);

        //Text Brush in Normal State
        mTextPaint.setTextSize(mTextSize);
        mTextPaint.setColor(mColorTextDef);
        mTextPaint.setAntiAlias(true);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
        //Selected Text Brush
        mTextSelPaint.setTextSize(mTextSize);
        mTextSelPaint.setColor(mColorSelected);
        mTextSelPaint.setAntiAlias(true);
        mTextSelPaint.setTextAlign(Paint.Align.CENTER);
    }

    /**
     * Measure the length and width of the text as a rect rectangle
     */
    private void measureText(){
        mBounds = new ArrayList<>();
        for(String name : textList){
            Rect mBound = new Rect();
            mTextPaint.getTextBounds(name, 0, name.length(), mBound);
            mBounds.add(mBound);
        }
    }




    /**
     * Measure the height of the view
     */
    private void measureHeight(){
        if (mBounds!=null && mBounds.size()!=0) {
            defaultHeight = (int) (mCircleHeight + mMarginTop + mCircleSelStroke + mBounds.get(0).height()/2);
        } else {
            defaultHeight  = (int) (mCircleHeight + mMarginTop+mCircleSelStroke);
        }
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        //Set width and height to wrap_content
       if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            //Width set to wrap_content
            setMeasuredDimension(widthSpecSize,defaultHeight);
        }else if(widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize,heightSpecSize);
        }else if(heightSpecMode == MeasureSpec.AT_MOST){
            //Set high to wrap_content
            setMeasuredDimension(widthSpecSize, defaultHeight);
        }else{
            //Set both width and height to match_parent or specific dp value
            setMeasuredDimension(widthSpecSize, heightSpecSize);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //Cancel drawing if no node title or list of selected items is set
        if (textList == null || textList.isEmpty() ||
                selectedIndexSet == null || selectedIndexSet.isEmpty() ||
                mBounds == null || mBounds.isEmpty()) {
            return;
        }
        //Number of gray circles drawn
        circleCount=textList.size();
        //The distance between each circle (important), through which you can adjust the spacing between nodes
        divideWidth = (getWidth() - mCircleHeight ) / (circleCount - 1);
        //Draw text and circles
        for (int i=0; i < circleCount ;i++){
            float cx;
            float cy;
            float textX;
            if (i==0){
                //First node, center needs to be offset to right
                cx = mCircleHeight / 2 + i * divideWidth + marginLeft;
                cy = mCircleHeight / 2 + mCircleSelStroke;
                textX = cx;
                circleLineJunctions.add(cx + mCircleHeight / 2);
            }else if (i==textList.size()-1){
                //Last node, center needs to be offset to the left
                cx = mCircleHeight / 2 + i * divideWidth - marginRight;
                cy = mCircleHeight / 2 + mCircleSelStroke;
                textX = cx;
                circleLineJunctions.add(cx - mCircleHeight / 2);
            }else {
                //Nodes in the middle
                cx = mCircleHeight / 2 + i * divideWidth;
                cy = mCircleHeight / 2+mCircleSelStroke;
                textX = cx;
                circleLineJunctions.add(cx - mCircleHeight / 2);
                circleLineJunctions.add(cx + mCircleHeight / 2);
            }
            if (getSelectedIndexSet().contains(i)){
                //If the current location node is included in the selected item Set, determine that the node is selected
                canvas.drawCircle(cx , cy, mCircleHeight / 2, mCircleStrokeSelPaint);
                canvas.drawCircle(cx, cy, mCircleFillRadius, mCircleSelPaint);
                canvas.drawText(textList.get(i), textX, (float) (mCircleHeight + mMarginTop +mCircleSelStroke+mBounds.get(i).height()/2.0), mTextSelPaint);
            }else {
                //If the current location node is not included in the selected item Set, determine that the node is not selected
                canvas.drawCircle(cx , cy, mCircleHeight / 2, mCirclePaint);
                canvas.drawText(textList.get(i), textX, (float) (mCircleHeight + mMarginTop +mCircleSelStroke+mBounds.get(i).height()/2.0), mTextPaint);
            }
        }
        for(int i = 1 , j = 1 ; j <= circleLineJunctions.size() && ! circleLineJunctions.isEmpty()  ; ++i , j=j+2){
            if(getSelectedIndexSet().contains(i)){
                canvas.drawLine(circleLineJunctions.get(j-1),mCircleHeight/2+mCircleSelStroke,
                        circleLineJunctions.get(j) ,mCircleHeight/2+mCircleSelStroke,mLineSelectedPaint);
            }else {
                canvas.drawLine(circleLineJunctions.get(j-1),mCircleHeight/2+mCircleSelStroke,
                        circleLineJunctions.get(j) ,mCircleHeight/2+mCircleSelStroke,mLinePaint);
            }
        }
    }

    /**
     * For external calls, display controls
     * @param titles Bottom Title Content List
     * @param indexSet Selected Set
     */
    public void show(List<String> titles , Set<Integer> indexSet){
        if(titles != null && ! titles.isEmpty()){
            this.textList = titles;
        }
        if(indexSet != null  && ! indexSet.isEmpty()){
            this.selectedIndexSet = indexSet;
        }
        measureText();
        measureHeight();
        //Draw
        invalidate();
    }

    /**
     * Update bottom node header content
     * @param textList Node Title Content List
     */
    public void refreshTextList(List<String> textList) {
        this.textList = textList;
        measureText();
        measureHeight();
        invalidate();
    }

    /**
     * Get Node Selected State
     * @return Node Selection Status List
     */
    public Set<Integer> getSelectedIndexSet() {
        return selectedIndexSet;
    }

    /**
     * Update Selected Items
     * @param set Selected Set
     */
    public void refreshSelectedIndexSet(Set<Integer> set) {
        this.selectedIndexSet = set;
        invalidate();
    }


}

Points of Attention

  1. The total number of nodes in the control is the same as the number of elements in the heading list at the bottom of the incoming node. In short, how many headings are in the heading list, how many nodes are drawn
  2. The control initializes and displays the View through the show method, passes in a list of node titles and a collection of node selected items, and controls the selected state of the View and the content displayed.
  3. After the control initializes the display, you can update the title and selection through refreshTextList(),refreshSelectedIndexSet()
  4. Specific colors, sizes can be adjusted in View

summary

You can see that the effect is not complex, so there are not many lines of code to customize the View and it is easy to understand. You can simply take the code away and eat it in the project.

Because different project design drafts will be different, here is only a way of thinking for students in need, you can transform the specific implementation code~

Thank you for reading here ~Welcome to discuss and communicate with me

Tags: Android

Posted on Tue, 05 May 2020 16:55:12 -0400 by egorig