最近項(xiàng)目有個(gè)找回密碼的狀態(tài)進(jìn)度條顯示需求:
以前遇到這種情況,基本上都是直接讓美工幫忙實(shí)現(xiàn)的盖灸,你懂的蚁鳖,現(xiàn)在想研究一下自定義View,然后就開(kāi)始挖坑填坑了赁炎,不過(guò)總體感覺(jué)這個(gè)UI其實(shí)實(shí)現(xiàn)原理比較簡(jiǎn)單醉箕,最后還是很不愉快地實(shí)現(xiàn)了,并且可以動(dòng)態(tài)擴(kuò)展徙垫,支持動(dòng)態(tài)配置步驟的數(shù)量讥裤,看一下實(shí)現(xiàn)的效果
最終實(shí)現(xiàn)的效果:
關(guān)于自定義View的知識(shí)這里就不再多說(shuō)了,這個(gè)View沒(méi)有子控件姻报,所以選擇集成系統(tǒng)的View己英,整個(gè)界面其實(shí)看上去比較簡(jiǎn)單,就是畫(huà)圓吴旋,畫(huà)線损肛,畫(huà)文字厢破,只是想分享一下自己在實(shí)現(xiàn)的過(guò)程中遇到的一些問(wèn)題,雖然界面上有好多需求治拿。
實(shí)現(xiàn)步驟
1.自定義屬性
因?yàn)樾枰L制的控件比較多摩泪,所以涉及到的顏色跟間距需要進(jìn)行屬性聲明,然后由于考慮到控件的擴(kuò)展性劫谅,以后可能會(huì)涉及到更多的步驟见坑,所以步驟的數(shù)量也是動(dòng)態(tài)配置的,并沒(méi)有寫(xiě)死捏检,只需要配置好相應(yīng)的數(shù)據(jù)源荞驴,就可以動(dòng)態(tài)地生成相應(yīng)的控件,屬性的命名比較規(guī)范未檩,看名字就能知道:
<!--自定義屬性集合:ProgressView-->
<declare-styleable name="PasswordView">
<attr name="circle_color_checked" format="color"/>
<attr name="circle_color_unchecked" format="color"/>
<attr name="number_color_checked" format="color"/>
<attr name="number_color_unchecked" format="color"/>
<attr name="line_color" format="color"/>
<attr name="text_color" format="color"/>
<attr name="text_size" format="dimension"/>
<attr name="text_padding" format="dimension"/>
<attr name="circle_radius" format="dimension"/>
<attr name="edge_line_width" format="dimension"/>
<attr name="center_line_width" format="dimension"/>
<attr name="topName" format="reference"/>
<attr name="bottomName" format="reference"/>
<attr name="checkedNumber" format="integer"/>
</declare-styleable>
需要說(shuō)明的是bottomName和topName這兩個(gè)屬性戴尸,對(duì)應(yīng)的是數(shù)組id,也就是上面的數(shù)字?jǐn)?shù)組跟下面的文字?jǐn)?shù)組:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="topNames">
<item>1</item>
<item>2</item>
<item>3</item>
</string-array>
<string-array name="bottomNames">
<item>驗(yàn)證手機(jī)</item>
<item>重設(shè)密碼</item>
<item>重設(shè)成功</item>
</string-array>
</resources>
然后是在布局文件中引用
app:topName="@array/topNames"
app:bottomName="@array/bottomNames"
2.在代碼中獲取相應(yīng)的屬性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PasswordView);
mCircleColorChecked = typedArray.getColor(R.styleable.PasswordView_circle_color_checked, Color.RED);
mCircleColorUnchecked = typedArray.getColor(R.styleable.PasswordView_circle_color_unchecked, Color.RED);
mNumberColorChecked = typedArray.getColor(R.styleable.PasswordView_number_color_checked, Color.RED);
mNumberColorUnchecked = typedArray.getColor(R.styleable.PasswordView_number_color_unchecked, Color.RED);
mTextPadding = toPx(typedArray.getDimension(R.styleable.PasswordView_text_padding, toPx(12.0f)));
mLineColor = typedArray.getColor(R.styleable.PasswordView_line_color, Color.RED);
mTextColor = typedArray.getColor(R.styleable.PasswordView_text_color, Color.RED);
mCircleRadius = toPx(typedArray.getDimension(R.styleable.PasswordView_circle_radius, 0));
mTextSize = toPx(typedArray.getDimension(R.styleable.PasswordView_text_size, 0));
mEdgeLineWidth = toPx(typedArray.getDimension(R.styleable.PasswordView_edge_line_width, 0));
mCenterLineWidth = toPx(typedArray.getDimension(R.styleable.PasswordView_center_line_width, 0));
mCheckedNumber = typedArray.getInteger(R.styleable.PasswordView_checkedNumber, 0);
int topNamesId = typedArray.getResourceId(R.styleable.PasswordView_topName, 0);
if (topNamesId != 0)
mTopNames = getResources().getStringArray(topNamesId);
int bottomNamesId = typedArray.getResourceId(R.styleable.PasswordView_bottomName, 0);
if (bottomNamesId != 0)
mBottomNames = getResources().getStringArray(bottomNamesId);
childNumbers = mBottomNames.length;
typedArray.recycle();
3.onMeasure
在此方法中獲取View的寬高,這里有幾點(diǎn)要說(shuō)明一下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(measuredWidth(widthMeasureSpec), measuredHeight(heightMeasureSpec));
}
寬度測(cè)量
- 當(dāng)width為match_parent或者具體的長(zhǎng)度
此時(shí)的測(cè)量模式為EXACTLY冤狡,View的寬度為父容器的寬度或者指定的寬度孙蒙,這個(gè)時(shí)候需要計(jì)算的就是mCenterLineWidth,也就是中間的那幾條線段的寬度悲雳,這個(gè)寬度用來(lái)定位中間的圓的橫坐標(biāo)挎峦,它們的寬度就是整個(gè)View的寬度減去已知控件的長(zhǎng)度之后除以中間線段條數(shù)的平均值。
- 當(dāng)width為wrap_content的時(shí)候
此時(shí)的測(cè)量模式為AT_MOST,View的寬度就是所有已知控件的長(zhǎng)度之和合瓢,此時(shí)必須指定中間線的寬度坦胶,不然沒(méi)有辦法計(jì)算View的總寬度,具體計(jì)算方式見(jiàn)下面代碼
//測(cè)量寬度
private int measuredWidth(int widthMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int mode = MeasureSpec.getMode(widthMeasureSpec);
int result = 0;
switch (mode) {
case MeasureSpec.EXACTLY://width為match或者具體的長(zhǎng)度
result = width;
//通過(guò)均分來(lái)計(jì)算中間分發(fā)現(xiàn)的寬度
mCenterLineWidth = (result - getPaddingLeft() - getPaddingRight() - 2 * mEdgeLineWidth - 2 * mCircleRadius * childNumbers) / (childNumbers - 1);
break;
case MeasureSpec.AT_MOST://width為wrap
//通過(guò)自定義屬性來(lái)計(jì)算測(cè)量的寬度
int realWidth = getPaddingLeft() + getPaddingLeft() + 2 * mEdgeLineWidth + 2 * mCircleRadius * childNumbers + mCenterLineWidth * (childNumbers - 1);
result = Math.min(realWidth, width);
break;
}
return result;
}
高度測(cè)量
- 當(dāng)height為match_parent或者具體的長(zhǎng)度
此時(shí)的測(cè)量模式為EXACTLY晴楔,View的高度為父容器的高度或者指定的高度
- 當(dāng)width為wrap_content的時(shí)候
此時(shí)的測(cè)量模式為AT_MOST,View的高度就是所有已知控件的長(zhǎng)度之和顿苇,此時(shí)必須指定mTextPadding的高度,不然沒(méi)有辦法計(jì)算View的總高度税弃,具體計(jì)算方式見(jiàn)下面代碼
//測(cè)量高度
private int measuredHeight(int heightMeasureSpec) {
int height = MeasureSpec.getSize(heightMeasureSpec);
int mode = MeasureSpec.getMode(heightMeasureSpec);
int result = 0;
switch (mode) {
case MeasureSpec.EXACTLY://height為match或者具體的長(zhǎng)度
result = height;
break;
case MeasureSpec.AT_MOST://height為wrap_content
int realHeight = getPaddingTop() + getPaddingBottom() + 2 * mCircleRadius + mTextPadding + mTextHeight;
result = Math.min(height, realHeight);
break;
}
return result;
}
4.onDraw
這里有一點(diǎn)需要注意的纪岁,就是要先畫(huà)線,再畫(huà)圓则果,而且都需要是實(shí)心圓幔翰,這樣線只需要畫(huà)一次,因?yàn)閳A可以把線給蓋住西壮,左端點(diǎn)到右端點(diǎn)遗增,不然需要繪制好幾段,特別麻煩款青,所以要注意策略的選擇做修,由于步驟的數(shù)量的不確定性導(dǎo)致圓心以及文字的起始坐標(biāo)都需要?jiǎng)討B(tài)計(jì)算,動(dòng)態(tài)繪制,這里需要簡(jiǎn)單計(jì)算一下缓待,也不是很麻煩蚓耽,
- float cx = getPaddingLeft() + mEdgeLineWidth + mCircleRadius + i * (mCenterLineWidth + 2 * mCircleRadius);//圓心橫坐標(biāo)
- float cy = getPaddingTop() + mCircleRadius;//圓心縱坐標(biāo)
圓心確定了,周?chē)目丶捕急容^好確定了旋炒,坐標(biāo)訂好了步悠,就直接進(jìn)行繪制了,感覺(jué)自定義控件就是各種計(jì)算padding瘫镇,margin之類(lèi)鼎兽,真正需要繪制的并不是很復(fù)雜,當(dāng)然可能我這個(gè)自定義控件比較簡(jiǎn)單铣除,只有UI效果谚咬,沒(méi)有交互邏輯,基本上到這兒已經(jīng)完成了尚粘,放一下繪制的代碼:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float startX = getPaddingLeft();
float endX = getMeasuredWidth() - getPaddingRight();
float startY = getPaddingTop() + mCircleRadius;
canvas.drawLine(startX,startY,endX,startY,mPaint);
//畫(huà)圓及圓中的數(shù)字
for (int i = 0; i < mTopNames.length; i++) {
float cx = getPaddingLeft() + mEdgeLineWidth + mCircleRadius + i * (mCenterLineWidth + 2 * mCircleRadius);//圓心橫坐標(biāo)
float cy = getPaddingTop() + mCircleRadius;//圓心縱坐標(biāo)
float baseNumberX = cx - mNumberWidth / 2;//數(shù)字文本框的左上定點(diǎn)
float baseNumberY = cy + mNumberHeight / 2 - mFontMetrics.bottom;//文字文本框的基線
float baseTextX = cx - mTextWidth / 2;//文字文本框的左上定點(diǎn)
float baseTextY = getHeight() - getPaddingBottom() - mFontMetrics.bottom;//文字文本框的基線
if (i == mCheckedNumber) {
//畫(huà)實(shí)心圓
mPaint.setColor(mCircleColorChecked);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(cx, cy, mCircleRadius, mPaint);
//描邊
mPaint.setColor(mCircleColorChecked);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(2);
canvas.drawCircle(cx, cy, mCircleRadius, mPaint);
//畫(huà)數(shù)字
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mNumberColorChecked);
canvas.drawText(mTopNames[i], baseNumberX, baseNumberY, mPaint);
} else {
//畫(huà)空心圓
mPaint.setColor(mCircleColorUnchecked);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setStrokeWidth(2);
canvas.drawCircle(cx, cy, mCircleRadius, mPaint);
//描邊
mPaint.setColor(mNumberColorUnchecked);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(2);
canvas.drawCircle(cx, cy, mCircleRadius, mPaint);
//畫(huà)數(shù)字
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mNumberColorUnchecked);
canvas.drawText(mTopNames[i], baseNumberX, baseNumberY, mPaint);
}
//畫(huà)文字
mPaint.setColor(mTextColor);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawText(mBottomNames[i], baseTextX, baseTextY, mPaint);
mPaint.setColor(mLineColor);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setStrokeWidth(3);
}
}
5.使用
<com.fatchao.passwordview.PasswordView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:paddingBottom="@dimen/dp_10"
android:paddingLeft="@dimen/dp_10"
android:paddingRight="@dimen/dp_10"
android:paddingTop="@dimen/dp_15"
app:bottomName="@array/bottomNames"
app:checkedNumber="1"
app:circle_color_checked="@color/grey"
app:circle_color_unchecked="@color/white"
app:circle_radius="15dp"
app:edge_line_width="30dp"
app:line_color="@color/grey"
app:number_color_checked="@color/white"
app:number_color_unchecked="@color/black"
app:text_color="@color/grey"
app:text_padding="@dimen/dp_12"
app:text_size="14sp"
app:topName="@array/topNames"
/>