前言
無論做什么應(yīng)用,總是有需要輸入框的地方蜒什,總的來說,輸入框都不是單獨(dú)存在的疤估,一般會(huì)需要標(biāo)題灾常、輸入框(一般需要自定義邊框/下劃線/光標(biāo)顏色長度等)、左右圖標(biāo)铃拇,密碼類型的輸入框還可能需要密碼可見按鈕钞瀑,如果自己寫布局,布局文件會(huì)比較臃腫慷荔,使用也不方便(因?yàn)槟阈枰獑为?dú)控制text雕什、hint、icon、title等等對應(yīng)的view)贷岸。為了解決這個(gè)問題壹士,于是想到寫一個(gè)輸入框控件
先附上github地址,不想看我多bb的直接用起來就是
https://github.com/NotSeriousCoder/FormInputView
控件長這樣
思路
輸入框類型
一般在表單里面偿警,用到的輸入框類型如下:
- 普通文本躏救,啥都能輸入
- 單行文本框
- 數(shù)字文本框
- 密碼輸入框(文本/純數(shù)字)
- 點(diǎn)擊選擇框(即無法手動(dòng)輸入,一般是收到回調(diào)之后彈一個(gè)窗口螟蒸,用戶選擇)
布局
想清楚類型落剪,就可以開始思考布局(就是上面效果圖那個(gè)樣子啦)∧蚵總的來說,最外層一個(gè)FrameLayout呢堰,最外層是邊框抄瑟,里面一個(gè)LinearLayout用來放圖標(biāo)和輸入框。其他都好說枉疼,關(guān)鍵的地方皮假,在于邊框的繪制,由于要做成顏色可變骂维,而且要要給title的位置留白惹资,那么就不能使用固定圖片來做background,要手動(dòng)繪制航闺。
實(shí)現(xiàn)
1.需要仿寫一部分EditText的常用方法(因?yàn)椴皇侵苯永^承EditText)褪测,比如setText(text)、setHint(hint)之類潦刃,另外要提供getEditText()
2.通過onMeasure()計(jì)算內(nèi)部控件的寬度(要留出左右邊框的位置)侮措,高度則保持自適應(yīng)即可(上下邊框位置由padding控制)
3.繪制邊框及title
4.控制內(nèi)部子控件的擺放位置,防止內(nèi)部子控件放置在視野之外
具體實(shí)現(xiàn)
這里只講重點(diǎn)乖杠,也就是視圖的測量分扎、布局、繪制胧洒,其他都是些簡單的內(nèi)容畏吓,不贅述。
View的生成流程卫漫,我們可利用的菲饼,就是
測量--onMeasure(int widthMeasureSpec, int heightMeasureSpec)
布局--onLayout(boolean changed, int left, int top, int right, int bottom)
繪制--onDraw(Canvas canvas)
想深入學(xué)習(xí),可以看這篇文章(郭霖大神列赎,想必學(xué)Android的都不陌生吧)
Android LayoutInflater原理分析巴粪,帶你一步步深入了解View
測量--onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setupPadding();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setupRadius();
//子控件的寬度=父控件的寬度-2*邊框筆觸寬度
//這樣就不會(huì)踩線
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
(int) (MeasureSpec.getSize(widthMeasureSpec) - 2.2 * Math.max(borderWidth, radius)),
MeasureSpec.EXACTLY
);
//給每個(gè)子控件設(shè)置上
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.measure(
childWidthMeasureSpec,
heightMeasureSpec
);
}
setupIconSize();
}
首先,是設(shè)置上下Padding,給上下邊框+title留出位置肛根,因?yàn)闆]有去細(xì)究padding的設(shè)置是否會(huì)對子控件的測量產(chǎn)生影響(其實(shí)我覺得會(huì))辫塌,所以保險(xiǎn)起見,我選擇在測量前先設(shè)置
padding存在兩種情況
??1.文字高度>邊框筆觸寬度
??2.文字高度<邊框筆觸寬度
??取較大值為topPadding派哲, bottomPadding則固定是邊框的筆觸寬度即可
然后設(shè)置子控件寬度臼氨,沒啥好講,看代碼即可
布局--onLayout
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
//左右將需要的空間空出來(寬度取邊框筆觸寬度和邊框弧度的較大值)
left = (int) (1.1 * Math.max(borderWidth, radius));
right = (int) (getMeasuredWidth() - 1.1 * Math.max(borderWidth, radius));
top = getPaddingTop();
bottom = top + child.getMeasuredHeight();
child.layout(
left,
top,
right,
bottom
);
}
}
這里需要注意芭届,并不會(huì)因?yàn)槲覀冊O(shè)置了paddingTop和paddingBottom储矩,onLayout的top和bottom值就按照padding過后的值給過來,所以我們要自己計(jì)算
繪制--onDraw
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//設(shè)置空心褂乍,因?yàn)橐嬁招膱A角矩形
paint.setStyle(Paint.Style.STROKE);
//設(shè)置筆畫粗細(xì)度
paint.setStrokeWidth(borderWidth);
paint.setColor(borderColor);
//開啟抗鋸齒
paint.setAntiAlias(true);
canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
/**
* 因?yàn)槔L畫的時(shí)候持隧,設(shè)定的xy點(diǎn)是筆觸的中點(diǎn)
* 所以left right bottom都需要+-筆觸寬度的一半(borderWidth / 2),保證畫出來的邊框不會(huì)有一邊在視線以外并且貼邊
* top是例外逃片,因?yàn)橛形淖致挪Γ砸猤etPaddingTop()/2
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
canvas.drawRoundRect(borderWidth / 2, getPaddingTop() / 2, getMeasuredWidth() - borderWidth / 2, getMeasuredHeight() - borderWidth / 2, radius, radius, paint);
} else {
canvas.drawRoundRect(new RectF(borderWidth / 2, getPaddingTop() / 2, getMeasuredWidth() - borderWidth / 2, getMeasuredHeight() - borderWidth / 2), radius, radius, paint);
}
if (!TextUtils.isEmpty(title)) {
titleRect = Utils.getTextSize(textSizeTitle, title);
/**
* 畫一條線(顏色{@link #getBgColor()}),作為空白處用來畫title
* 線的起始點(diǎn)X值:筆觸寬度+弧度值+用戶自定義標(biāo)題偏移量
*/
int lineStartX = borderWidth + radius + titleOffset;
//線的終點(diǎn)X值褥实,則需要計(jì)算文字寬度來決定
int lineEndX = lineStartX;
//這里計(jì)算的是控件剩余可用于繪制的寬度呀狼,如果根本沒地方了,直接結(jié)束繪制
int availableWidth = getMeasuredWidth() - borderWidth - radius - lineStartX;
if (availableWidth <= 0) {
return;
}
int textWidth = titleRect.width();
//如果標(biāo)題過寬损离,則裁剪標(biāo)題
//-8dp是因?yàn)樾枰A(yù)留空白哥艇,防止標(biāo)題和邊框過于靠近
if (textWidth > availableWidth - UnitConverter.dip2px(getContext(), 8)) {
title = getSuitableText(title, textSizeTitle, availableWidth - UnitConverter.dip2px(getContext(), 6));
titleRect = Utils.getTextSize(textSizeTitle, title);
textWidth = titleRect.width();
}
if (textWidth * 0.2f > UnitConverter.dip2px(getContext(), 4)) {
lineEndX += textWidth + UnitConverter.dip2px(getContext(), 4);
} else {
lineEndX += textWidth * 1.2f;
}
//標(biāo)題起始點(diǎn)X值=空白線起始值X +( 空白線長度 - 標(biāo)題長度 )/2
//( 空白線長度 - 標(biāo)題長度 )/2 就是留白的長度
int textStartX = lineStartX + (lineEndX - lineStartX - textWidth) / 2;
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(borderWidth);
paint.setColor(getBgColor());
canvas.drawLine(lineStartX, getPaddingTop() / 2, lineEndX, getPaddingTop() / 2, paint);
paint.setTextSize(textSizeTitle);
paint.setColor(textColorTitle);
Paint.FontMetricsInt fm = paint.getFontMetricsInt();
//文字的繪制,并不是以字高的中點(diǎn)為(x,y)僻澎,而是以baseLine為準(zhǔn)貌踏,所以位置并不好算,這里也是瞎蒙
canvas.drawText(title, textStartX, ((fm.bottom - fm.ascent) / 3 + getPaddingTop() / 2), paint);
}
}
這里有一點(diǎn)需要注意窟勃,由于畫筆的筆觸是有寬度的(比如10dp這么寬)哩俭,那么你在繪制圖形的時(shí)候就要注意,startY并不是筆觸的最上方位置拳恋,比如說:
canvas.drawLine(lineStartX, 0, lineEndX, 0, paint);
如果這樣畫凡资,很可能這條線有一半會(huì)在視線之外