完成后的样式
分析
首先我们从这张图的表面就可以看到三样:
- 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,下面是他们的介绍
- UNSPECIFIED(未指定), 父元素部队自元素施加任何束缚,子元素可以得到任意想要的大小;
- EXACTLY(完全),父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;
- AT_MOST(至多),子元素至多达到指定大小的值。
除了 EXACTLY 是由用户自定义了高度之外,其他的都需要我们去测量高度。在高度上,我们分成了三部分,我们需要知道文字的高度,左侧进度条的高度,右侧进度条的高度,并从这三者中获得最高的那个作为我们测量出来的结果高度。
文字的高度我们通过来计算,具体看图
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" />
兄弟篇下
完
转载请注明:热爱改变生活.cn » 自定义 ProgressBar
本博客只要没有注明“转”,那么均为原创。 转载请注明链接:sumile.cn » 自定义 ProgressBar