Android 自定义View实现多节点进度条功能

来自:网络
时间:2020-10-14
阅读:

前言

最近项目有一个节点进度条的小需求,完成后,想分享出来希望可以帮到有需要的同学。

真机效果图

Android 自定义View实现多节点进度条功能

Android 自定义View实现多节点进度条功能

自定义View完整代码

开箱即用~,注释已经炒鸡详细了

/**
 * @description: 节点进度条
 * @author: DMingO
 * @date: 2020/4/15
 */
public class PointProcessBar extends View {
  /**
   * 未选中时的连线画笔
   */
  private Paint mLinePaint;
  /**
   * 选中时的连线画笔
   */
  private Paint mLineSelectedPaint;
  /**
   * 未选中时的文字画笔
   */
  private Paint mTextPaint;
  /**
   * 选中时的文字画笔
   */
  private Paint mTextSelPaint;
  /**
   * 未选中时的实心圆画笔
   */
  private Paint mCirclePaint;
  /**
   * 选中时的内部实心圆画笔
   */
  private Paint mCircleSelPaint;
  /**
   * 选中时的边框圆画笔
   */
  private Paint mCircleStrokeSelPaint;
  /**
   * 未选中时的线,节点圆的颜色
   */
  private int mColorUnselected = Color.parseColor("#1ca8b0d9");
  /**
   * 选中时的颜色
   */
  private int mColorSelected = Color.parseColor("#61A4E4");
  /**
   * 未选中的文字颜色
   */
  private int mColorTextUnselected = Color.parseColor("#5c030f09");
  /**
   * 绘制的节点个数,由底部节点标题数量控制
   */
  int circleCount ;
  /**
   * 连线的高度
   */
  float mLineHeight = 7f;
  //圆的直径
  float mCircleHeight = 50f;
  float mCircleSelStroke = 8f;
  float mCircleFillRadius = 15f;
  //文字大小
  float mTextSize = 35f;
  //文字离顶部的距离
  float mMarginTop = 40f;
  /**
   * 首个圆向中心偏移的距离
   */
  float marginLeft = 30f;
  /**
   * 最后一个圆向中心偏移的距离
   */
  float marginRight = marginLeft;
  /**
   * 每个节点相隔的距离
   */
  float divideWidth;
  int defaultHeight;
  /**
   * 节点底部的文字列表
   */
  List<String> textList = new ArrayList<>();
  /**
   * 文字同宽高的矩形,用来测量文字
   */
  List<Rect> mBounds;
  /**
   * 存储每个圆心在同一直线上的节点圆的 x 坐标值
   */
  List<Float> circleLineJunctions = new ArrayList<>();
  /**
   * 选中项集合
   */
  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);
  }
  /**
   * 初始化画笔属性
   */
  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);
    //设置填充
    mLinePaint.setStyle(Paint.Style.FILL);
    //笔宽像素
    mLinePaint.setStrokeWidth(mLineHeight);
    //锯齿不显示
    mLinePaint.setAntiAlias(true);
    mLineSelectedPaint.setColor(mColorSelected);
    mLineSelectedPaint.setStyle(Paint.Style.FILL);
    mLineSelectedPaint.setStrokeWidth(mLineHeight);
    mLineSelectedPaint.setAntiAlias(true);
    mCirclePaint.setColor(mColorDef);
    //设置填充
    mCirclePaint.setStyle(Paint.Style.FILL);
    //笔宽像素
    mCirclePaint.setStrokeWidth(1);
    //锯齿不显示
    mCirclePaint.setAntiAlias(true);
    //选中时外框空心圆圈画笔
    mCircleStrokeSelPaint.setColor(mColorSelected);
    mCircleStrokeSelPaint.setStyle(Paint.Style.STROKE);
    mCircleStrokeSelPaint.setStrokeWidth(mCircleSelStroke);
    mCircleStrokeSelPaint.setAntiAlias(true);
    //选中时的内部填充圆画笔
    mCircleSelPaint.setStyle(Paint.Style.FILL);
    mCircleSelPaint.setStrokeWidth(1);
    mCircleSelPaint.setAntiAlias(true);
    mCircleSelPaint.setColor(mColorSelected);
    //普通状态的文本 画笔
    mTextPaint.setTextSize(mTextSize);
    mTextPaint.setColor(mColorTextDef);
    mTextPaint.setAntiAlias(true);
    mTextPaint.setTextAlign(Paint.Align.CENTER);
    //选中后的文本画笔
    mTextSelPaint.setTextSize(mTextSize);
    mTextSelPaint.setColor(mColorSelected);
    mTextSelPaint.setAntiAlias(true);
    mTextSelPaint.setTextAlign(Paint.Align.CENTER);
  }
  /**
   * 测量文字的长宽,将文字视为rect矩形
   */
  private void measureText(){
    mBounds = new ArrayList<>();
    for(String name : textList){
      Rect mBound = new Rect();
      mTextPaint.getTextBounds(name, 0, name.length(), mBound);
      mBounds.add(mBound);
    }
  }

  /**
   * 测量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);
    //宽高都设置为wrap_content
    if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
      //宽设置为wrap_content
      setMeasuredDimension(widthSpecSize,defaultHeight);
    }else if(widthSpecMode == MeasureSpec.AT_MOST){
      setMeasuredDimension(widthSpecSize,heightSpecSize);
    }else if(heightSpecMode == MeasureSpec.AT_MOST){
      //高设置为wrap_content
      setMeasuredDimension(widthSpecSize, defaultHeight);
    }else{
      //宽高都设置为match_parent或具体的dp值
      setMeasuredDimension(widthSpecSize, heightSpecSize);
    }
  }
  @Override
  protected void onDraw(Canvas canvas) {
    //若未设置节点标题或者选中项的列表,则取消绘制
    if (textList == null || textList.isEmpty() ||
        selectedIndexSet == null || selectedIndexSet.isEmpty() ||
        mBounds == null || mBounds.isEmpty()) {
      return;
    }
    //画灰色圆圈的个数
    circleCount=textList.size();
    //每个圆相隔的距离(重要),可以通过这个调节节点间距
    divideWidth = (getWidth() - mCircleHeight ) / (circleCount - 1);
    //绘制文字和圆形
    for (int i=0; i < circleCount ;i++){
      float cx;
      float cy;
      float textX;
      if (i==0){
        //第一个节点,圆心需要向右偏移
        cx = mCircleHeight / 2 + i * divideWidth + marginLeft;
        cy = mCircleHeight / 2 + mCircleSelStroke;
        textX = cx;
        circleLineJunctions.add(cx + mCircleHeight / 2);
      }else if (i==textList.size()-1){
        //最后一个节点,圆心需要向左偏移
        cx = mCircleHeight / 2 + i * divideWidth - marginRight;
        cy = mCircleHeight / 2 + mCircleSelStroke;
        textX = cx;
        circleLineJunctions.add(cx - mCircleHeight / 2);
      }else {
        //中间部分的节点
        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)){
        //若当前位置节点被包含在选中项Set中,判定此节点被选中
        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 {
        //若当前位置节点没有被包含在选中项Set中,判定此节点没有被选中
        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);
      }
    }
  }
  /**
   * 供外部调用,显示控件
   * @param titles 底部标题内容列表
   * @param indexSet 选中项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();
    //绘制
    invalidate();
  }
  /**
   * 更新底部节点标题内容
   * @param textList 节点标题内容列表
   */
  public void refreshTextList(List<String> textList) {
    this.textList = textList;
    measureText();
    measureHeight();
    invalidate();
  }
  /**
   * 获取节点选中状态
   * @return 节点选中状态列表
   */
  public Set<Integer> getSelectedIndexSet() {
    return selectedIndexSet;
  }
  /**
   * 更新选中项
   * @param set 选中项Set
   */
  public void refreshSelectedIndexSet(Set<Integer> set) {
    this.selectedIndexSet = set;
    invalidate();
  }
}

注意点

控件的节点总个数是与传入的节点底部标题列表中元素个数控制(相同)的,简而言之就是传入的标题列表中有多少个标题,节点就会绘制多少个 控件通过show方法进行View的初始化和显示内容,传入节点标题列表和节点选中项集合,控制View的选中状态和显示的内容 控件初始化显示后,可以通过refreshTextList(),refreshSelectedIndexSet() 更新标题和选中项 具体不同的颜色,大小可以具体在View中调整

可以看到效果不复杂,因此自定义View的代码行数不多,也很容易看懂,直接拿走代码即可在项目中食用啦。

由于不同项目设计稿会有不同,这里也仅仅给有需要的同学一个思路,可以改造具体实现代码~

返回顶部
顶部