GitHub地址:github.com/yumi0629/Fl…git
(寫的比較急,代碼還沒整理好,很凌亂,emmm,果真仍是元旦以後再整理吧,→_→)github
(本方案暫時只支持數字,不支持英文字母、中文等)
canvas
國際慣例先上效果圖:bash
這個驗證碼輸入框的需求來源近日日羣裏有人提出了這麼一個問題:像下面這種的控件該怎麼寫? ide
乍一看這就是一個TextField,但彷佛又有那麼點不太同樣?我冷靜思考了一下,腦子裏有兩套解決方案:工具
這兩種方案,看着就以爲腦袋疼啊。
先說第一種,點進源碼咱們能夠看到TextField的實現鏈式關係爲:TextField——>EditableText——>_Editable——>RenderEditable
,而主要的繪製都集中在了RenderEditable
中的paint()
方法:佈局
@override
void paint(PaintingContext context, Offset offset) {
······
if (_hasVisualOverflow)
context.pushClipRect(needsCompositing, offset, Offset.zero & size, _paintContents);
else
// 具體繪製內容,包括cursor和文字
_paintContents(context, offset);
}
void _paintContents(PaintingContext context, Offset offset) {
······
// 繪製cursor
if (_selection.isCollapsed && _showCursor.value && cursorColor != null) {
_paintCaret(context.canvas, effectiveOffset);
} else if (!_selection.isCollapsed && _selectionColor != null) {
······
_paintSelection(context.canvas, effectiveOffset);
}
// 繪製文字
_textPainter.paint(context.canvas, effectiveOffset);
}
複製代碼
雖然說生命不息,魔改不止,可是這一套魔改下來,emmmm,我選擇拒絕!
再說第二種,4個控件組合,這種方式在佈局上確實會簡單不少,可是,致命的問題在於,要本身處理用戶的手勢輸入,以及cursor的位置移動等等,這個過程是十分複雜的,並且容易出錯。
衆所周知,我小拉麪是一個懶人,能走對角線的我絕對不拐彎,寫代碼信奉「曲線救國」原則,怎麼簡單怎麼來,上面兩種方案明顯不適合我。
那麼怎麼辦呢?我凝視這設計稿,emmm,這果真仍是一個TextField嘛,何須搞這麼複雜嘛。你不信的話,我加幾筆給你分析下: 字體
letterSpace
,用來作數字間距很方便;
TextField
自帶直線的
UnderlineInputBorder
,那咱們換成虛線的不就好了?虛線的dash值就是字體寬度和letterSpace交替。這個方案,絕對比上面兩個要簡單的多得多得多,嗯,很是適合我。
字體寬度的測量一直都是一個痛點,由於,它跟textSize確定不相等,真的很差量啊······字體大小爲textSize時,字體寬度並非下圖中的藍色框,而是紅色框。ui
RenderEditable
的源碼能夠發現,
TextField
中字體的繪製最終是經過
TextPainter
來完成的,而
TextPainter
的繪製核心則是
canvas.drawParagraph(_paragraph, offset);
,因此
Paragraph
就是肯定文字位置的最重要的類之一。
Paragraph
中有一個
minIntrinsicWidth
,這個值就是咱們須要的文字寬度。
Paragraph
能夠經過
ParagraphBuilder
來建立,
ParagraphBuilder
能夠接收一個
ParagraphStyle
,其中包含了字體樣式、字體類型、字體方向等等各類信息。至於
minIntrinsicWidth
什麼時候生效,源碼文檔中寫得很清楚,
Valid only after [layout] has been called.
,因此咱們layout以後就能夠拿到
minIntrinsicWidth
啦:
double calcTrueTextSize(double textSize) {
// 測量單個數字實際長度
var paragraph = ui.ParagraphBuilder(ui.ParagraphStyle(fontSize: textSize))
..addText("0");
var p = paragraph.build()
..layout(ui.ParagraphConstraints(width: double.infinity));
return p.minIntrinsicWidth;
}
複製代碼
上面的代碼就是測量數字「0」的方法,在Flutter默認數字字體中,0~9這十個數字所佔的實際繪製寬度都是同樣的,所以咱們測量數字「0」就是測量了全部數字。可是,若是換成英文,那就不同了,英文的a~z這26個字母,即便都是小寫,測量出來的寬度也是每一個字母都不同的,因此是無法用在TextField上面的,由於咱們無法事先知曉用戶會輸入哪一個字母。而至於中文,emmm,就比較坑了,測量值跟實際繪製的寬度徹底不同,會小一點。spa
自定義一個UnderlineInputBorder十分簡單,繼承一下而後重寫paint()
方法便可:
@override
void paint(
Canvas canvas,
Rect rect, {
double gapStart,
double gapExtent = 0.0,
double gapPercentage = 0.0,
TextDirection textDirection,
}) {
Path path = Path();
path.moveTo(rect.bottomLeft.dx , rect.bottomLeft.dy);
path.lineTo(rect.bottomLeft.dx + (textWidth + spaceWidth) * textLength,
rect.bottomRight.dy);
path = dashPath.dashPath(path,
dashArray: dashPath.CircularIntervalList<double>([
textWidth,
spaceWidth,
]));
canvas.drawPath(path, borderSide.toPaint());
}
複製代碼
父的paint()
方法會給咱們一個rect
,這個值就是咱們border的可繪製區域。Flutter默認不支持虛線,咱們能夠藉助一下別人寫好的工具 dash_path.dart,dashPath()
會返回給咱們一個虛線Path
,這個工具類跟方便,走過路過不要錯過,建議收藏。
TextField
部分代碼以下:
var underLineBorder = CustomUnderlineInputBorder(
spaceWidth: 30.0,
textWidth: calcTrueTextSize(50.0),
textLength: 4,
borderSide: BorderSide(color: Colors.black26, width: 2.0));
TextField(
maxLength: 4,
keyboardType: TextInputType.number,
style: TextStyle(
fontSize: 50.0,
color: Colors.black87,
letterSpacing: 30.0),
decoration: InputDecoration(
hintText: ' Please input verification code',
hintStyle: TextStyle(fontSize: 14.0, letterSpacing: 0.0),
enabledBorder: underLineBorder,
focusedBorder: underLineBorder),
);
複製代碼
運行一下代碼,你會發現,樣式仍是有點差別,border總體向左偏移了:
letterSpacing
屬性後,
TextField
的第一個字符左邊會空出一半的
letterSpacing
的距離,因此咱們在繪製border的時候將左起點往右偏移一段距離便可:
// startOffset = letterSpacing*0.5
path.moveTo(rect.bottomLeft.dx + startOffset, rect.bottomLeft.dy);
複製代碼
到此爲止,咱們就,畫好啦~~~哈哈哈是否是真的超級簡單呀~~~
既然Border能夠在paint()
中爲所欲爲地想怎麼畫就怎麼畫,那麼,理論上咱們能夠繪製任意樣式的Border。
好比畫個方框:
@override
void paint(
Canvas canvas,
Rect rect, {
double gapStart,
double gapExtent = 0.0,
double gapPercentage = 0.0,
TextDirection textDirection,
}) {
double curStartX = rect.left + startOffset - offsetX;
for (int i = 0; i < textLength; i++) {
Rect r = Rect.fromLTWH(curStartX, rect.top + offsetY,
textWidth + offsetX * 2, rect.height - offsetY * 2);
canvas.drawRect(r, borderSide.toPaint());
curStartX += (textWidth + spaceWidth);
}
}
複製代碼
好比畫個愛心:
@override
void paint(
Canvas canvas,
Rect rect, {
double gapStart,
double gapExtent = 0.0,
double gapPercentage = 0.0,
TextDirection textDirection,
}) {
double width = rect.height - offsetX;
double radius = width * 0.25;
// 1:editable.dart _kCaretGap
double curStartX = startOffset - radius - offsetX - 1;
print(
'rect.height:${rect.height},curStartX:$curStartX,offsetX:$offsetX,startOffset:$startOffset');
if (curStartX < 0) {
throw ArgumentError(
'No enough space to paint border! LetterSpace is too small.');
}
double top = rect.center.dy - radius * 2;
double bottom = rect.center.dy + radius * 2;
Path path = Path();
for (int i = 0; i < textLength; i++) {
path.moveTo(curStartX + radius * 2, top + radius);
path.arcTo(
Rect.fromCircle(
center: Offset(curStartX + radius, top + radius), radius: radius),
degToRad(180.0 - angleOffset),
degToRad(180.0 + angleOffset),
true);
double sinLength = radius * sin(degToRad(angleOffset));
double cosLength = radius * cos(degToRad(angleOffset));
path.moveTo(curStartX + radius - cosLength, top + radius + sinLength);
path.lineTo(curStartX + radius * 2, bottom);
path.lineTo(curStartX + radius * 3 + cosLength, top + radius + sinLength);
path.arcTo(
Rect.fromCircle(
center: Offset(curStartX + radius * 3, top + radius),
radius: radius),
degToRad(angleOffset),
degToRad(-180.0 - angleOffset),
true);
curStartX += (textWidth + spaceWidth);
}
canvas.drawPath(path, borderSide.toPaint());
}
複製代碼
甚至畫個背景圖:
@override
void paint(
Canvas canvas,
Rect rect, {
double gapStart,
double gapExtent = 0.0,
double gapPercentage = 0.0,
TextDirection textDirection,
}) {
double curStartX = rect.left;
for (int i = 0; i < textLength; i++) {
canvas.drawImage(image, Offset(curStartX, 0.0), Paint());
curStartX += (textWidth + spaceWidth);
}
}
複製代碼
爲何是繼承自UnderlineInputBorder
,而不是InputBorder
?
直接繼承自InputBorder
須要重寫一大推方法,getInnerPath()
、getOuterPath()
等等,不必從新計算,直接拿UnderlineInputBorder
中算好的值就能夠了。
爲何明明是方框的border,卻不是繼承自OutlineInputBorder
呢?
其實只是爲了計算統一,由於UnderlineInputBorder
和OutlineInputBorder
傳遞給子的rect
參數會有所不一樣,因此若是你繼承自OutlineInputBorder
也是很OK的。