自定义 ProgressBar – 热爱改变生活
我的GitHub GitHub |     登录
  • If you can't fly, then run; if you can't run, then walk; if you can't walk, then crawl
  • but whatever you do, you have to keep moving forward。
  • “你骗得了我有什么用,这是你自己的人生”
  • 曾有伤心之地,入梦如听 此歌

自定义 ProgressBar

Android控件 sinvader 4812℃ 0评论

完成后的样式

ProgressBarDemo_1

分析

首先我们从这张图的表面就可以看到三样:

  • 1. 左边的进度条
  • 2. 中间的文字
  • 3. 右边的进度条

其实除了这些,还有一个

  • 文字与左右的边距


当我们将这个 View 编写完成,交给其他编程人员使用时,他们肯定希望在 xml 中定义的时候就可以设置以上的那些属性值,方便他们的布局以及样式的定义。所以

  • 一. 首先我们应该编写 attrs

attrs 编写完成之后,我们就应该编写我们的 view 了

  • 二. 创建 view,继承 ProgressBar,实现构造方法

构造方法实现了,在我们画出我们想要的图形之前,我们应该先获得用户在 xml 中对上面那些属性的自定义的值,如果没有这些值,我们应该给它一个默认值

  • 三. 获取自定义属性

属性获取完成之后,开始测量控件的大小

  • 四. 测量控件

最后,画出我们想要的图形

  • 五. 画出进度条

编写 attrs

根据上面所写出的属性,我们可以分析并编写出以下 attr,其中 unreach 表示未到达的进度条,也就是文字后面的进度条,reach 表示已经到达的进度条,也就是文字前面的进度条,color 和 height 分别代表了进度条的颜色以及它的高度(宽度),text 则是中间的文字,color 和 size 分别代表了文字的颜色和文字的大小。text_offset 代表文字与两边进度条的总间距。(注意:是总间距,一遍的间距就是总间距的一半)

<attr name="progress_unreach_color" format="color"></attr>
<attr name="progress_unreach_height" format="dimension"></attr>
<attr name="progress_reach_color" format="color"></attr>
<attr name="progress_reach_height" format="dimension"></attr>
<attr name="progress_text_color" format="color"></attr>
<attr name="progress_text_size" format="dimension"></attr>
<attr name="progress_text_offset" format="dimension"></attr>

<declare-styleable name="HorizontalProgressBar">
    <attr name="progress_unreach_color"></attr>
    <attr name="progress_unreach_height"></attr>
    <attr name="progress_reach_color"></attr>
    <attr name="progress_reach_height"></attr>
    <attr name="progress_text_color"></attr>
    <attr name="progress_text_size"></attr>
    <attr name="progress_text_offset"></attr>
</declare-styleable>

创建 view

public class HorizontalProgressBar extends ProgressBar {
    
    public HorizontalProgressBar(Context context) {
        this(context, null);
    }

    public HorizontalProgressBar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public HorizontalProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

这一步之后,至少我们现在是一个 ProgressBar 了

获取自定义属性

现在我们要获取用户对各属性定义的值了,但是我们要想,如果用户没有定义这个值,那么我们就必须为这个属性设置一个默认值

下面就先给上面说的那 7 个属性设置默认值

private static final int DEFAULT_TEXT_SIZE = 10;//文字大小
private static final int DEFAULT_TEXT_COLOR = 0xffFC00D1;//文字颜色
private static final int DEFAULT_Color_UNREACH = 0xFFD3D6DA;//文字右侧进度条颜色
private static final int DEFAULT_HEIGHT_UNREACH = 2;//文字右侧进度条高度
private static final int DEFAULT_COLOR_REACH = DEFAULT_TEXT_COLOR;//文字左侧进度条颜色
private static final int DEFAULT_HEIGHT_REACH = 2;//文字右侧进度条高度
private static final int DEFAULT_TEXT_OFFSET = 10;//文字两边的间距的总和

上面定义的这些默认值,有的是 sp(文字的大小),有的是 dp(各种间距以及高度),我们需要将它们统一转为 px

private int spToPx(int spValue) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, getResources().getDisplayMetrics());
}

private int dpToPx(int dpValue) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, getResources().getDisplayMetrics());
}

通过上面的两个方法,我们把默认值进行转换

private int mTextSize = spToPx(DEFAULT_TEXT_SIZE);
private int mTextColor = DEFAULT_TEXT_COLOR;
private int mUnreachHeight = dpToPx(DEFAULT_HEIGHT_UNREACH);
private int mUnreachColor = DEFAULT_Color_UNREACH;
private int mReachHeight = dpToPx(DEFAULT_HEIGHT_REACH);
private int mReachColor = DEFAULT_COLOR_REACH;
private int mTextOffeset = dpToPx(DEFAULT_TEXT_OFFSET);

接着,我们就可以获取用户自定义的那些属性了,这里我们创建了一个方法,在三个参数的构造方法中调用

private void obtainStyleAttrs(AttributeSet attrs) {
    TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.HorizontalProgressBar);
    mTextColor = ta.getColor(R.styleable.HorizontalProgressBar_progress_text_color, mTextColor);
    mTextSize = (int) ta.getDimension(R.styleable.HorizontalProgressBar_progress_text_size, mTextSize);
    mReachColor = ta.getColor(R.styleable.HorizontalProgressBar_progress_reach_color, mReachColor);
    mReachHeight = (int) ta.getDimension(R.styleable.HorizontalProgressBar_progress_reach_height, mReachHeight);
    mUnreachColor = ta.getColor(R.styleable.HorizontalProgressBar_progress_unreach_color, mUnreachColor);
    mUnreachHeight = (int) ta.getDimension(R.styleable.HorizontalProgressBar_progress_unreach_height, mUnreachHeight);
    ta.recycle();
}

这样完成之后,我们就获得了用户定义的各属性的值,用户没有定义的值,我们也给它用默认值填上了。
万事具备

测量控件

我们知道,当我们的父元素要开始绘制我们的时候,它会来问我们:你需要多大的地方,你告诉我,我把你画出来,同时给了我们两个东西,一个是 widthMeasureSpec,另一个是 heightMeasureSpec。从这两个参数中,我们就可以获得它的模式以及它的大小。我们以高度为例,来测量下他的真实高度:

private int measureHeight(int heightMeasureSpec) {
    int result = 0;
    int mode = MeasureSpec.getMode(heightMeasureSpec);
    int size = MeasureSpec.getSize(heightMeasureSpec);
    if (mode == MeasureSpec.EXACTLY) {
        result = size;
    } else {
        int textSize = (int) (mPaint.descent() - mPaint.ascent());
        result = getPaddingTop() + getPaddingBottom() + Math.max(Math.max(mUnreachHeight, mReachHeight), Math.abs(textSize));
        if (mode == MeasureSpec.AT_MOST) {
            result = Math.min(size, result);
        }
    }
    return result;
}

可以看到,我们在进入这个测量方法的时候,首先就先获得了在高度上它的模式是什么,如果它的模式是 EXACTLY, 也就是绝对布局,那么我们获得什么高度就给他设置什么高度就可以了,这种是最简单的。除了 EXACTLY 之外,还有另外两种模式:UNSPECIFIED 以及 AT_MOST,下面是他们的介绍

  1. UNSPECIFIED(未指定), 父元素部队自元素施加任何束缚,子元素可以得到任意想要的大小;
  2. EXACTLY(完全),父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;
  3. AT_MOST(至多),子元素至多达到指定大小的值。

除了 EXACTLY 是由用户自定义了高度之外,其他的都需要我们去测量高度。在高度上,我们分成了三部分,我们需要知道文字的高度,左侧进度条的高度,右侧进度条的高度,并从这三者中获得最高的那个作为我们测量出来的结果高度。

文字的高度我们通过来计算,具体看图

paint_text_info

1. 基准点是 baseline
2.ascent:是 baseline 之上至字符最高处的距离
3.descent:是 baseline 之下至字符最低处的距离
4.leading:是上一行字符的 descent 到下一行的 ascent 之间的距离, 也就是相邻行间的空白距离
5.top:是指的是最高字符到 baseline 的值, 即 ascent 的最大值
6.bottom:是指最低字符到 baseline 的值, 即 descent 的最大值

int textSize = (int) (mPaint.descent() - mPaint.ascent());

这样我们就获得了文字的高度,通过 Math 的 max 方法,找到三者中最大的值,同时不要忘了加上它的 paddingTop 以及 paddingBottom

现在我们获得了我们测量出来的值,如果模式是 AT_MOST, 这就表示,我给你设置了一个值,如果你的值大于我预设的这个值,你必须得按我的走

if (mode == MeasureSpec.AT_MOST) {
       result = Math.min(size, result);
}

这样我们就测量出来了高度。
最后我们通过 setMeasuredDimension 方法告诉父控件,就按照这个给我绘制吧。
最后我们可以获得到控件真正的宽度,下面会用到。

mRealWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();

画出进度条

所有的都完成之后,我们开始绘制进度条,进度条绘制在 onDraw 中

首先,我们对画布进行操作

canvas.save();
canvas.translate(getPaddingLeft(), getHeight() / 2);

然后我们来绘制文字左侧的进度条。

我们可以想象,现在我们要绘制的进度条分成了三个部分,左侧进度条,文字,右侧进度条,这三个部分加起来应该是等于 mRealWidth,文字两边的边距,我们要分别从左侧进度条和右侧进度条中去扣掉。那么现在我们可以很容易的获得现在 progressBar 的进度的百分比,进而获得左侧进度条实际上占据了 mRealWidth 中的多少

float radio = getProgress() * 1.0f / getMax();
float progressX = radio * mRealWidth;

这里我们要真正理解 progressX 指的是什么,progressX 指的是左侧进度条+文字的左边距,那么,如果这个 progressX – 文字的左边距小于 0,这个左侧进度条就不用显示了

下面的代码计算了 progressX(进度条与间距的和)- mTextOffset(间距)= 进度条真正显示的长度,如果这个长度是大于 0 的,那么这个进度条才需要显示,才需要被画出来

float endX = progressX - mTextOffset / 2;
if (endX > 0) {
    mPaint.setColor(mReachColor);
    mPaint.setStrokeWidth(mReachHeight);
    canvas.drawLine(0, 0, endX, 0, mPaint);
}

想明白了这个,再想一下,等进度到了 99%,这个时候左侧进度条和文字的宽度加起来应该已经和 mRealWidth 差不多宽了,我们当然不需要绘制右侧的进度条了

所以,在这里我们要计算一下,如果左侧进度条的宽度加上文字的宽度已经大于或者等于了 mRealWidth,那么我们就设置一个标志位,不再绘制右侧的进度条,同时,左侧进度条的宽度恒定为 mRealWidth 减去文字的宽度,这个结果实际上包括了左侧的间距

String text = getProgress() + "%";
float textWidth = mPaint.measureText(text);
if (textWidth + progressX >= mRealWidth) {
    progressX = mRealWidth - textWidth;
    noNeedUnreach = true;
}

左侧进度条绘制完成之后开始绘制文字

mPaint.setColor(mTextColor);
        mPaint.setStrokeWidth(mTextSize);
        int y = (int) (Math.abs(mPaint.descent() + mPaint.ascent()) / 2) ;
        canvas.drawText(text, progressX, y, mPaint);

最后绘制右侧的进度条,根据我们的标志位来获得。开始绘制的位置 = 左侧进度条显示长度 + 文字长度 + 右侧间距,当然这个值有可能超过 mRealWidth,所以我们取两者之间小的

if (!noNeedUnreach) {
            float start = progressX + mTextOffset / 2 + textWidth;
            mPaint.setColor(mUnreachColor);
            mPaint.setStrokeWidth(mUnreachHeight);
            canvas.drawLine(Math.min(start, mRealWidth), 0, mRealWidth, 0, mPaint);
        }

最后不要忘了

canvas.restore();

使用

在 xml 中定义如下

<cn.sumile.progress.HorizontalProgressBar
    android:id="@+id/progress"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="30dp"
    sumile:progress_text_color="#000000"
    sumile:progress_unreach_color="#169711"
    sumile:progress_reach_color="#1a36be"
    android:padding="5dp"
    android:progress="35"
    android:indeterminate="false" />

兄弟篇下

自定义圆形进度条

视频观看: 慕课网
GitHub 地址

¥ 有帮助么?打赏一下~

转载请注明:热爱改变生活.cn » 自定义 ProgressBar


本博客只要没有注明“转”,那么均为原创。 转载请注明链接:sumile.cn » 自定义 ProgressBar

喜欢 (0)
发表我的评论
取消评论
表情

如需邮件形式接收回复,请注册登录

Hi,你需要填写昵称和邮箱~

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址