Flutter系列的文章我會持續更新一個月左右,力求利用1個月帶大家入門Flutter,抓住這波技術風口,歡迎大家關注。同時如果覺得這里代碼排版不是很舒服的讀者可以關注我的微信公眾號“IT工匠”,我會同步更新,另外微信公眾號上還有很多互聯網必備資源(涉及算法、數據結構、java、深度學習、計算機網絡、python、Android等互聯網技術資料),歡迎大家關注、交流。
文的主要內容:
Flutter中按照是否自身可直接響應用戶交互可以將Widget分為兩類:
由于第一類比較簡單,本文重點介紹一下第二類,即如何為非交互性(不能直接響應用戶交互)的Widget添加交互性, 具體來說,我們將通過創建一個自定義的Statful Widget來讓Icon具有交互性。
在上一篇文章中我們介紹了如何構建一個下面這樣的UI頁面:
當這個app第一次運行的時候那個星星是紅色的,代表這個屏幕中展示的那個圖片被用戶點擊了喜歡,星星后面的數字47代表一共有47個用戶點擊了喜歡。本文將實現,點擊星星后移除喜歡狀態,用空心星星替換實心星星并減少星星后面的計數。 再次點擊空心星星代表添加喜歡,會繪制一顆實心的星星并增加星星后的數字。
要實現此功能,您將創建一個包含星星和計數的自定義Widget, 點擊星星會更改兩個子Widget的狀態,因此自定義的Widget應該同時管理這兩個子Widget(星星和計數)。
首先我將會介紹一點前備知識,如果你只對最終的代碼實現感興趣,你可以直接跳到第2步:創建StatefulWidget的子類,如果你想嘗試其他的管理狀態的方法,可以直接跳到管理狀態一節。
一個Widget要么是有狀態(stateful)的,要么是無狀態(stateless)的,如果一個Widget是可改變的,比如當用戶與其交互的時候其會產生變化,這個Widget就是有狀態的(stateful)。
一個無狀態(stateless)的Widget是永遠不會發生改變的,Icon、IconButton、Text都是典型的無狀態的Widget,無狀態(stateless)的Widget都是StatelessWidget的子類。
一個有狀態(stateful)的Widget是動態的,比如它可以更改其外觀以響應用戶交互或接收數據時觸發的事件。CheckBox、Radio、Slider、InkWell、Form、TextField都是典型的有狀態的Widget,有狀態(stateful)的Widget都是StatefulWidget的子類。
Widget的狀態都是保存在State對象中的,從外觀上分析小部件的狀態。 狀態由可以更改的值組成,例如滑塊(slider)的當前值、是否選中復選框(CheckBox)。 當Widget的狀態發生變化時,State對象調用會setState()方法來告訴框架重繪該Widget。
明確幾點概念:
本節將創建一個自定義的有狀態(Stateful)的Widget,我們將用我們自定義的包含一個IconButton和一個Text的Widget來替代原有的紅色星星Widget和計數Widget。
實現一個自定義的Widget需要創建2個類:
我們通過簡單的幾步來構建一個名為FavoriteWidget的自定義Widget:
第1步:決定由那個對象來管理Widget的狀態(State)
Widget的狀態(State)可以有多種管理方式,在此處由于切換星星的狀態(實心還是空心)是一個獨立的操作,不會影響父Widget或UI的其余部分,所以我們讓Widget自己管理自己的狀態(State)。
關于詳細的狀態管理的內容,我會在后面的管理狀態一節介紹。
<span id="subclass-statefulWidget">第2步:創建StatefulWidget的子類</span>
由于第1步我們已經決定了FavoriteWidget自己管理自己的狀態(State),所以我們應該重寫createState()方法來創建一個State對象。Flutter框架會在構建Widget的時候調用對應Widget的createState()方法。在這個例子中,我們應該在createState()方法中返回一個我們將在下一步定義的_FavoriteWidgetState類的實例對象:
class FavoriteWidget extends StatefulWidget { @override _FavoriteWidgetState createState()=> _FavoriteWidgetState(); }
注意:這里的_開頭指的是定義的對應類是私有的。
第3步:創建State類的子類
我們定義一個_FavoriteWidgetState類來存儲會在Widget不同生命周期變化的數據,當app第一次運行的時候,UI界面應該展示紅色的實心星星,代表當前已經選擇了”喜歡”狀態,并且傍邊展示的文字為”41”,我們本別使用bool _isFavorited和int _favoriteCount變量來存儲這兩個狀態:
class _FavoriteWidgetState extends State<FavoriteWidget> { bool _isFavorited=true; int _favoriteCount=41; // ··· }
_FavoriteWidgetState類同樣也定義了一個build()方法,在該方法中創建一個Row(行),Row中包含有一個Iconbutton和一個Text,我們使用Iconbutton而不是Icon的原因是IconButton有onPressed屬性,我們可以通過這個onPressed屬性定義處理點擊事件的回調函數(_toggleFavorite),我們將在后面具體定義這個_toggleFavorite函數:
class _FavoriteWidgetState extends State<FavoriteWidget> { // ··· @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( padding: EdgeInsets.all(0), child: IconButton( icon: (_isFavorited ? Icon(Icons.star) : Icon(Icons.star_border)), color: Colors.red[500], onPressed: _toggleFavorite, ), ), SizedBox( width: 18, child: Container( child: Text('$_favoriteCount'), ), ), ], ); } }
注意:我們這里將Text作為子Widget放置在了SizedBox中,并且設置了SizedBox的寬度,這樣做的作用是固定Text的寬度,設想一下,當Text中只顯示1位數字的時候Text的寬度和顯示2位數字的寬度一定是不一樣的,如果不固定Text的寬度,當數字變化的時候就會出現Text寬度發生跳變的情況,導致視覺效果很不好。
當IconButton被點擊的時候將會調用_toggleFavorite()方法,我們在_toggleFavorite()方法中調用setstate()方法并更新狀態,這樣Flutter框架就會知道需要重新繪制當前Widget了,從而達到更新界面的效果:
void _toggleFavorite() { setState(() { if (_isFavorited) { _favoriteCount -=1; _isFavorited=false; } else { _favoriteCount +=1; _isFavorited=true; } }); }
setState()方法中的代碼邏輯很簡單,首先判斷當前_isFavorited的狀態,然后對_isFavorited和_isFavorited的值進行更新。
第4步:將我們自定義的Stateful Widget加入到Widget樹中
我們應該在app的build()方法中將我們自定義的Stateful Widget加入到Widget 樹中,首先找到原先Icon和Text的位置,然后刪除原來的代碼,加入新的我們創建的Stateful Widget:
Widget titleSection=Container( padding: const EdgeInsets.all(32), child: Row( children: [ Expanded( /*1*/ child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ /*2*/ Container( padding: const EdgeInsets.only(bottom: 8), child: Text( 'Oeschinen Lake Campground', style: TextStyle( fontWeight: FontWeight.bold, ), ), ), Text( 'Kandersteg, Switzerland', style: TextStyle( color: Colors.grey[500], ), ), ], ), ), FavoriteWidget(), ], ), );
然后運行代碼(推薦使用熱更新),可以看到效果圖:
在我們的設計中,到底應該由誰來管理Widget的狀態(State)?是Widget本身?是Widget的父Widget?還是二者共同管理?還是另一個對象來管理? 事實上有不止一種有效的方法可以使你的Widget小部件具有交互性, 作為Widget的設計者,你可以根據預期的Widget的使用方式做出決策。 以下是幾種最常用的管理狀態的方法:
你可能會有疑問,你應該如何決定具體使用哪一種狀態管理方法?這里提供幾個原則供你參考:
如果你不太確定自己的場景屬于以上哪種,可以直接使用父級Widget管理的方法,因為這個方法是通用的。
接下來我將通過創建三個簡單示例(TapboxA,TapboxB和TapboxC)來舉例說明管理狀態的不同方法。 這幾個示例的工作方式類似: 每個都創建了一個Container,當點擊時,可以在綠色或灰色框之間切換, _active布爾值確定顏色:true代表綠色,false代表灰色。
Widget自己管理自己本身的State
有時,由Widget自己管理自己的狀態可以產生很強大的功能。例如,ListView在其內容的總尺寸超出其最大渲染框的尺寸時會自動進行滾動,這個滾動的狀態是由ListView自己管理的,不需要我們開發人員去手動設置它什么時候應該開始滾動、什么時候應該停止滾動。
我們通過一個示例來進行說明,我們創建一個_TapboxAState類:
代碼如下:
// TapboxA 自己管理自己的狀態 //------------------------- TapboxA ---------------------------------- class TapboxA extends StatefulWidget { TapboxA({Key key}) : super(key: key); @override _TapboxAState createState()=> _TapboxAState(); } class _TapboxAState extends State<TapboxA> { bool _active=false; void _handleTap() { setState(() { _active=!_active; }); } Widget build(BuildContext context) { return GestureDetector( onTap: _handleTap, child: Container( child: Center( child: Text( _active ? 'Active' : 'Inactive', style: TextStyle(fontSize: 32.0, color: Colors.white), ), ), width: 200.0, height: 200.0, decoration: BoxDecoration( color: _active ? Colors.lightGreen[700] : Colors.grey[600], ), ), ); } } //------------------------- MyApp ---------------------------------- class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', home: Scaffold( appBar: AppBar( title: Text('Flutter Demo'), ), body: Center( child: TapboxA(), ), ), ); } }
運行效果如下圖所示:
Widget的父級Widget管理其State
父Widget管理子Widget狀態的最大用處是在合適的時機通知子Widget進行UI更新。 例如,IconButton允許你將Icon視為可點擊的按鈕, IconButton是一個無狀態的Widget,所以我們應該通過父Widget來確定Iconutton是否已被點擊。
在以下例子中,TapboxB將其狀態回調給父Widget,因為TapboxB不管理任何狀態,所以它是StatelessWidget的子類。
在這個示例中我們應該實現2個類:ParentWidgetState(代表父Widget)、TapboxB(代表子Widget)
ParentWidgetState的主要功能:
TapboxB的主要功能:
代碼實現如下:
// ParentWidget為TapboxB管理狀態. //------------------------ ParentWidget -------------------------------- class ParentWidget extends StatefulWidget { @override _ParentWidgetState createState()=> _ParentWidgetState(); } class _ParentWidgetState extends State<ParentWidget> { bool _active=false; void _handleTapboxChanged(bool newValue) { setState(() { _active=newValue; }); } @override Widget build(BuildContext context) { return Container( child: TapboxB( active: _active, onChanged: _handleTapboxChanged, ), ); } } //------------------------- TapboxB ---------------------------------- class TapboxB extends StatelessWidget { TapboxB({Key key, this.active: false, @required this.onChanged}) : super(key: key); final bool active; final ValueChanged<bool> onChanged; void _handleTap() { onChanged(!active); } Widget build(BuildContext context) { return GestureDetector( onTap: _handleTap, child: Container( child: Center( child: Text( active ? 'Active' : 'Inactive', style: TextStyle(fontSize: 32.0, color: Colors.white), ), ), width: 200.0, height: 200.0, decoration: BoxDecoration( color: active ? Colors.lightGreen[700] : Colors.grey[600], ), ), ); } }
代碼的運行效果如下:
混合使用前兩種管理方法
對于某些Widget,使用混合的方法管理其狀態很有有意義。 在這種情況下,有狀態(stateful)的Widget和其父Widget分別管理其一部分狀態(State)。
在TapboxC示例中,在點擊時,框周圍會出現深綠色邊框,點擊后,邊框消失,框的顏色也會改變。 TapboxC將其 _active狀態導出到其父Widget,在內部管理只其 _highlight狀態,所以 此示例有兩個State對象,_ParentWidgetState和_TapboxCState:
_ParentWidgetState的功能:
_TapboxCState的功能:
//---------------------------- ParentWidget ---------------------------- class ParentWidget extends StatefulWidget { @override _ParentWidgetState createState()=> _ParentWidgetState(); } class _ParentWidgetState extends State<ParentWidget> { bool _active=false; void _handleTapboxChanged(bool newValue) { setState(() { _active=newValue; }); } @override Widget build(BuildContext context) { return Container( child: TapboxC( active: _active, onChanged: _handleTapboxChanged, ), ); } } //----------------------------- TapboxC ------------------------------ class TapboxC extends StatefulWidget { TapboxC({Key key, this.active: false, @required this.onChanged}) : super(key: key); final bool active; final ValueChanged<bool> onChanged; _TapboxCState createState()=> _TapboxCState(); } class _TapboxCState extends State<TapboxC> { bool _highlight=false; void _handleTapDown(TapDownDetails details) { setState(() { _highlight=true; }); } void _handleTapUp(TapUpDetails details) { setState(() { _highlight=false; }); } void _handleTapCancel() { setState(() { _highlight=false; }); } void _handleTap() { widget.onChanged(!widget.active); } Widget build(BuildContext context) { // This example adds a green border on tap down. // On tap up, the square changes to the opposite state. return GestureDetector( onTapDown: _handleTapDown, // Handle the tap events in the order that onTapUp: _handleTapUp, // they occur: down, up, tap, cancel onTap: _handleTap, onTapCancel: _handleTapCancel, child: Container( child: Center( child: Text(widget.active ? 'Active' : 'Inactive', style: TextStyle(fontSize: 32.0, color: Colors.white)), ), width: 200.0, height: 200.0, decoration: BoxDecoration( color: widget.active ? Colors.lightGreen[700] : Colors.grey[600], border: _highlight ? Border.all( color: Colors.teal[700], width: 10.0, ) : null, ), ), ); } }
運行效果如下所示:
替代實現可能已將高亮狀態導出到父級,同時保持活動狀態為內部,但如果您要求某人使用該分接框,他們可能會抱怨它沒有多大意義。 開發人員關心該框是否處于活動狀態。 開發人員可能并不關心如何管理突出顯示,并且更喜歡點按框處理這些細節。
Flutter提供了很多按鈕和類似的交互式Widget。 這些Widget中的大多數都實現了Material Design準則,該準則定義了一組具有固定用戶界面的組件。
如果您愿意,可以使用GestureDetector在任何自定義的Widget中構建交互性。 您可以在管理狀態一節中找到GestureDetector的使用示例。
提示:Flutter還提供了一些IOS風格的Widget,稱之為Cupertino,具體地址:https://api.flutter.dev/flutter/cupertino/cupertino-library.html
當您需要交互性時,最簡單的方法是使用Flutter已經給我提供好的Widget,下面是一個部分列表:
標準庫中的Widget
Material庫中的Widget
TML 中使用 <input> 元素表示單行輸入框和 <textarea> 元素表示多行文本框。
HTML中使用的 <input> 元素在 JavaScript 中對應的是 HTMLInputElement 類型。HTMLInputElement 繼承自 HTMLElement 接口:
interface HTMLInputElement extends HTMLElement {
...
}
HTMLInputElement 類型有一些獨有的屬性和方法:
而在上述介紹 HTMLInputElement 類型中的屬性時,type 屬性要特別關注一下,因為根據 type 屬性的改變,可以改變<input>的屬性。
類型 | 描述 |
text | 文本輸入 |
password | 密碼輸入 |
submit | 表單數據提交 |
button | 按鈕 |
radio | 單選框 |
checkbox | 復選框 |
file | 文件 |
hidden | 隱藏的字段 |
image | 定義圖像作為提交按鈕 |
reset | 重置按鈕 |
省略 type 屬性與 type="text"效果一樣, <input> 元素顯示為文本框。
當 type 的值為text/password/number/時,會有以下屬性對 <input> 元素有效。
屬性 | 類型 | 描述 |
autocomplete | string | 字符串on或off,表示<input>元素的輸入內容可以被瀏覽器自動補全。 |
maxLength | long | 指定<input>元素允許的最多字符數。 |
size | unsigned long | 表示<input>元素的寬度,這個寬度是以字符數來計量的。 |
pattern | string | 表示<input>元素的值應該滿足的正則表達式 |
placeholder | string | 表示<input>元素的占位符,作為對元素的提示。 |
readOnly | boolean | 表示用戶是否可以修改<input>的值。 |
min | string | 表示<input>元素的最小數值或日期。 |
max | string | 表示<input>元素的最大數值或日期。 |
selectionStart | unsigned long | 表示選中文本的起始位置。如果沒有選中文本,返回光標在<input>元素內部的位置。 |
selectionEnd | unsigned long | 表示選中文本的結束位置。如果沒有選中文本,返回光標在<input>元素內部的位置。 |
selectionDirection | string | 表示選中文本的方向。可能的值包括forward、backward、none。 |
下面創建一個 type="text" ,一次顯示 25 個字符,但最多允許顯示 50 個字符的文本框:
<input type="text" size="25" maxlength="50" value="initial value">
HTML 使用的 <textarea> 元素在 JavaScript 中對應的是 HTMLTextAreaElement 類型。HTMLTextAreaElement類型繼承自 HTMLElement 接口:
interface HTMLTextAreaElement extends HTMLElement {
...
}
HTMLTextAreaElement 類型有一些獨有的屬性和方法:
下面創建一個高度為 25,寬度為 5 的 <textarea> 多行文本框。它與 <input> 不同的是,初始值顯示在 <textarea>...</textarea> 之間:
<textarea rows="25" cols="5">initial value</textarea>
注意:處理文本框值的時候最好不要使用 DOM 方法,而應該使用 value 屬性。
<input> 與 <textarea> 都支持 select() 方法,該方法用于選中文本框中的所有內容。該方法的語法為:
select(): void
下面看一個示例:
let textbox=document.forms[0].elements["input-box"];
textbox.select();
也可以在文本框獲得焦點時,選中文本框的內容:
textbox.addEventListener("focus", (event)=> {
event.target.select();
});
當選中文本框中的文本或使用 select() 方法時,會觸發 select 事件。
let textbox=document.forms[0].elements["textbox1"];
textbox.addEventListener("select", (event)=> {
console.log(`Text selected: ${textbox.value}`);
});
HTML5 對 select 事件進行了擴展,通過 selectionStart 和 selectionEnd 屬性獲取文本選區的起點偏移量和終點偏移量。如下所示:
function getSelectedText(textbox){
return textbox.value.substring(textbox.selectionStart,
textbox.selectionEnd);
}
注意:在 IE8 及更早版本不支持這兩個屬性。
HTML5 提供了 setSelectionRange() 方法用于選中部分文本:
setSelectionRange(start, end, direction): void;
下面看一個例子:
<input type="text" id="text-sample" size="20" value="Hello World!">
<button onclick="selectText()">選中部分文本</button>
<script>
function selectText() {
let input=document.getElementById("text-sample");
input.focus();
input.setSelectionRange(4, 8); // o Wo
}
</script>
如果想要看到選中效果,必須讓文本框獲得焦點。
不同文本框經常需要保證輸入特定類型或格式的數據,或許數據需要包含特定字符或必須匹配某個特定模式。而文本框并未提供驗證功能,因此要配合 JavaScript 腳本實現輸入過濾功能。
有些輸入框需要出現或不出現特定字符。如果想要將輸入框變成只讀的,只需要使用 preventDefault()方法將按鍵都屏蔽:
input.addEventListener("keypress", (event)=> {
event.preventDefault();
});
而要屏蔽特定字符,就需要檢查事件的 charCode 屬性。如下所示,使用正則表達式實現只允許輸入數字的輸入框:
input.addEventListener("keypress", (event)=> {
if (!/\d/.test(event.key)) {
event.preventDefault();
}
});
還有一個問題需要處理:復制、粘貼及涉及Ctrl 鍵的其他功能。在除IE 外的所有瀏覽器中,前面代碼會屏蔽快捷鍵Ctrl+C、Ctrl+V 及其他使用Ctrl 的組合鍵。因此,最后一項檢測是確保沒有按下Ctrl鍵,如下面的例子所示:
textbox.addEventListener("keypress", (event)=> {
if (!/\d/.test(String.fromCharCode(event.charCode)) &&
event.charCode > 9 &&
!event.ctrlKey){
event.preventDefault();
}
});
最后這個改動可以確保所有默認的文本框行為不受影響。這個技術可以用來自定義是否允許在文本框中輸入某些字符。
IE 是第一個實現了剪切板相關的事件以及通過JavaScript訪問剪切板數據的瀏覽器,其它瀏覽器在后來也都支持了相同的事件和剪切板的訪問,后來 HTML5 將其納入了規范。以下是與剪切板相關的 6 個事件:
剪切板事件的行為及相關對象會因瀏覽器而異。在 Safari、Chrome 和 Firefox 中,beforecopy、beforecut 和 beforepaste 事件只會在顯示文本框的上下文菜單時觸發,但 IE 不僅在這種情況下觸發,也會在 copy、cut 和 paste 事件在所有瀏覽器中都會按預期觸發。
在實際的事件發生之前,通過beforecopy、beforecut 和 beforepaste 事件可以在向剪貼板發送或從中檢索數據前修改數據。不過,取消這些事件并不會取消剪貼板操作。要阻止實際的剪貼板操作,必須取消 copy、cut和 paste 事件。
剪貼板的數據通過 clipboardData 對象來獲取,且clipboardData 對象提供 3 個操作數據的方法:
而 clipboardData 對象在 IE 中使用 window 獲取,在 Firefox、Safari 和 Chrome 中使用 event 獲取。為防止未經授權訪問剪貼板,只能在剪貼板事件期間訪問 clipboardData 對象;IE 會在任何時候都暴露 clipboardData 對象。因此,要兼容兩者,最好在剪貼板事件期間使用該對象。
function getClipboardText(event){
var clipboardData=(event.clipboardData || window.clipboardData);
return clipboardData.getData("text");
}
function setClipboardText (event, value){
if (event.clipboardData){
return event.clipboardData.setData("text/plain", value);
} else if (window.clipboardData){
return window.clipboardData.setData("text", value);
}
}
如果文本框只有數字,那剪貼時,就需要使用paste事件檢查剪貼板上的文本是否無效。如果無效,可以取消默認行為:
input.addEventListener("paste", (event)=> {
let text=getClipboardText(event);
if (!/^\d*$/.test(text)){
event.preventDefault();
}
});
注意:Firefox、Safari和Chrome只允許在onpaste事件中訪問getData()方法。
在 JavaScript 中,可以用在當前字段完成時自動切換到下一個字段的方式來增強表單字段的易用性。比如,常用手機號分為國家好加手機號。因此,我們設置 2 個文本框:
<form>
<input type="text" name="phone1" id="phone-id-1" maxlength="4">
<input type="text" name="phone2" id="phone-id-2" maxlength="11">
</form>
當文本框輸入到最大允許字符數后,就把焦點移到下一個文本框,這樣可以增加表單的易用性并加速數據輸入。如下所示:
<script>
function tabForward(event){
let target=event.target;
if (target.value.length==target.maxLength){
let form=target.form;
for (let i=0, len=form.elements.length; i < len; i++) {
if (form.elements[i]==target) {
if (form.elements[i+1]) {
form.elements[i+1].focus();
}
return;
}
}
}
}
let inputIds=["phone-id-1", "phone-id-2"];
for (let id of inputIds) {
let textbox=document.getElementById(id);
textbox.addEventListener("keyup", tabForward);
}
</script>
這里,tabForward() 函數通過比較用戶輸入文本的長度與 maxLength 屬性的值來檢測輸入是否達到了最大長度。如果兩者相等,就通過循環表中的元素集合找到當前文本框,并把焦點設置到下一個元素。
注意:上面的代碼只適用于之前既定的標記,沒有考慮可能存在的隱藏字段。
HTML5 新增了一些表單提交前,瀏覽器會基于指定的規則進行驗證,并在出錯時顯示適當的錯誤信息。而驗證會基于某些條件應用到表單字段中。
表單字段中添加 required 屬性,用于標注該字段是必填項,不填則無法提交。該屬性適用于<input>、<textarea>和<select>。如下所示:
<input type="text" name="account" required>
也可以通過 JavaScript 檢測對應元素的 required 屬性來判斷表單字段是否為必填項:
let isRequired=document.forms[0].elements["account"].required;
也可以檢測瀏覽器是否支持 required 屬性:
let isRequiredSupported="required" in document.createElement("input");
注意:不同瀏覽器處理必填字段的機制不同。Firefox、Chrome、IE 和Opera 會阻止表單提交并在相應字段下面顯示有幫助信息的彈框,而Safari 什么也不做,也不會阻止提交表單。
HTML5 為 <input> 元素增加了幾個新的 type 值。如下所示:
類型 | 描述 |
number | 數字值的輸入 |
date | 日期輸入 |
color | 顏色輸入 |
range | 一定范圍內的值的輸入 |
month | 允許用戶選擇月份和年份 |
week | 允許用戶選擇周和年份 |
time | 允許用戶選擇時間(無時區) |
datetime | 允許用戶選擇日期和時間(有時區) |
datetime-local | 允許用戶選擇日期和時間(無時區) |
電子郵件地址的輸入 | |
search | 搜索(表現類似常規文本) |
tel | 電話號碼的輸入 |
url | URL地址的輸入 |
這些輸入表名字段應該輸入的數據類型,并且提供了默認驗證。如下所示:
<input type="email" name="email">
<input type="url" name="homepage">
要檢測瀏覽器是否支持新類型,可以在 JavaScript 中創建 <input> 并設置 type 屬性,之后讀取它即可。老版本中會將我只類型設置為 text,而支持的會返回正確的值。如下所示:
let input=document.createElement("input");
input.type="email";
let isEmailSupported=(input.type=="email");
而上面介紹的幾個如 number/range/datetime/datetime-local/date/month/week/time 幾個填寫數字的類型,都可以指定 min/max/step 等幾個與數值有關的屬性。step 屬性用于規定合法數字間隔,如 step="2",則合法數字應該為 0、2、4、6,依次類推。如下所示:
<input type="number" min="0" max="100" step="5" name="count">
上面的例子是<input>中只能輸入從 0 到 100 中 5 的倍數。
也可以使用 stepUp() 和 stepDown() 方法對 <input> 元素中的值進行加減,它倆會接收一個可選參數,用于表示加減的數值。如下所示:
input.stepUp(); // 加1
input.stepUp(5); // 加5
input.stepDown(); // 減1
input.stepDown(10); // 減10
HTML5 還為文本添加了 pattern 屬性,用于指定一個正則表達式。這樣就可以自己設置 <input> 元素的輸入模式了。如下所示:
<input type="text" pattern="\d+" name="count">
注意模式的開頭和末尾分別假設有^和$。這意味著輸入內容必須從頭到尾都嚴格與模式匹配。
與新增的輸入類型一樣,指定 pattern 屬性也不會阻止用戶輸入無效內容。模式會應用到值,然后瀏覽器會知道值是否有效。通過訪問 pattern 屬性可以讀取模式:
let pattern=document.forms[0].elements["count"].pattern;
使用如下代碼可以檢測瀏覽器是否支持pattern 屬性:
let isPatternSupported="pattern" in document.createElement("input");
HTML5 新增了 checkValidity() 方法,用來檢測表單中任意給定字段是否有效。而判斷的條件是約束條件,因此必填字段如果沒有值會被視為無效,字段值不匹配 pattern 屬性也會被視為無效。如下所示:
if (document.forms[0].elements[0].checkValidity()){
// 字段有效,繼續
} else {
// 字段無效
}
要檢查整個表單是否有效,可以直接在表單上調用checkValidity()方法。這個方法會在所有字段都有效時返回true,有一個字段無效就會返回false:
if(document.forms[0].checkValidity()){
// 表單有效,繼續
} else {
// 表單無效
}
validity 屬性會返回一個ValidityState 對象,表示 <input> 元素的校驗狀態。返回的對象包含一些列的布爾值的屬性:
因此,通過 validity 屬性可以檢查表單字段的有效性,從而獲取更具體的信息,如下所示:
if (input.validity && !input.validity.valid){
if (input.validity.valueMissing){
console.log("請指定值.")
} else if (input.validity.typeMismatch){
console.log("請指定電子郵件地址.");
} else {
console.log("值無效.");
}
}
通過指定 novalidate 屬性可以禁止對表單進行任何驗證:
<form method="post" action="/signup" novalidate>
<!-- 表單元素 -->
</form>
也可以在 JavaScript 通過 noValidate 屬性設置,為 true 表示屬性存在,為 false 表示屬性不存在:
document.forms[0].noValidate=true; // 關閉驗證
如果一個表單中有多個提交按鈕,那么可以給特定的提交按鈕添加formnovalidate 屬性,指定通過該按鈕無需驗證即可提交表單:
<form method="post" action="/foo">
<!-- 表單元素 -->
<input type="submit" value="注冊提交">
<input type="submit" formnovalidate name="btnNoValidate"
value="沒有驗證的提交按鈕">
</form>
也可以使用 JavaScript 設置 formNoValidate 屬性:
// 關閉驗證
document.forms[0].elements["btnNoValidate"].formNoValidate=true;
以上總結了 <input> 和 <textarea> 兩個元素的一些功能,主要是 <input> 元素可以通過設置 type 屬性獲取不同類型的輸入框,可以通過監聽鍵盤事件并檢測要插入的字符來控制文本框的內容。
還有一些與剪貼板相關的事件,并對剪貼的內容進行檢測。還介紹了一些 HTML5 新增的屬性和方法和新增的更多的 <input> 元素的類型,和一些與驗證相關的屬性和方法。
adio、checkbox和switch應該是一個比較常用的html標簽,尤其是在中后臺ERP系統里面更為常見。不過瀏覽器自帶的樣式不怎么好看,而且不同瀏覽器效果也不一樣。出于美化和統一視覺效果的需求,自定義樣式就被提出來了。
純css實現的主要手段是利用label標簽的模擬功能。label的for屬性可以關聯一個具體的input元素,即使這個input本身不可被用戶可見,有個與它對應的label后,用戶可以直接通過和label標簽交互來替代原生的input——而這給我們的樣式模擬留下了空間。簡而言之就是:
隱藏原生input,樣式定義的過程留給label (那為什么不直接改變checkbox的樣式?因為checkbox作為瀏覽器默認組件,樣式更改上并沒有label那么方便,很多屬性對checkbox都是不起作用的,比如background,而label在樣式上基本和div一樣'任人宰割')
而在選擇事件上,由于css的“相鄰選擇符(E+F)”的存在,讓我們可以直接利用html的默認checkbox,免去了js模擬選擇的麻煩。
DEMO的部分CSS3屬性只寫了webkit前綴,所以建議用webkit內核的瀏覽器查看本頁案例,當然只要你給樣式補上對應的瀏覽器前綴,就可以實現更多樣式匹配
HTML代碼:
<!-- input的id必須有,這個是label進行元素匹配所必需的 --> <!-- 可以看到每個input的id和label的“for”屬性對應同一字符串 --> <input type="checkbox" id="checkbox01" /> <label for="checkbox01"></label> <input type="checkbox" id="checkbox02" /> <label for="checkbox02"></label> <input type="checkbox" id="checkbox03" /> <label for="checkbox03"></label> <input type="checkbox" id="checkbox04" /> <label for="checkbox04"></label>
HTML構建完成,接下來是對應的css:
/* 隱藏所有checkbox */ input[type='checkbox'] { display: none; } /* 對label進行模擬.背景圖片隨便拼湊的,不要吐槽品味*/ /* transition效果是做個背景切換效果,這里單純演示而已,實際上這個過渡不加更自然*/ label { display: inline-block; width: 60px; height: 60px; position: relative; background: url(//www.chitanda.me/images/blank.png); background-position: 0 0px; -webkit-transition: background 0.5s linear; } /* 利用相鄰選擇符和checkbox`:checked`的狀態偽類來模擬默認選中效果(就是點擊后那個勾號的效果) */ /*如果這段代碼注釋,點擊后將沒有任何反饋給用戶*/ /*因為label本身是沒有點擊后被選中的狀態的,checkbox被隱藏后,這個狀態只能手動模擬*/ input[type='checkbox']:checked+label { background-position: 0 -60px; }
上面代碼看起來好像也可以了。不過仔細想想,貌似缺了點什么:選項對應的提示文字
對css不了解的新人可能這時候第一反應就是在label后面用p標簽或者span標簽來添加文字。不過這種方式都不怎么優雅。個人建議用css的::before和::after偽元素(::before和:before是一個東西。不過為了把“偽元素”和“偽類”區分出來,W3C建議的寫法是偽元素用::而偽類用:)
/* 偽元素的生效很簡單,定義`content`就好,其余的屬性和普通div一樣 */ label::after { content: attr(data-name); /*利用attr可以減少css代碼量,data-name寫在html部分的label屬性里*/ display: inline-block; position: relative; width: 120px; height: 60px; left: 100%; vertical-align: middle; margin: 10px; }
當然既然可以用::after模擬label的文字,那也就可以用::before模擬label的checkbox樣式,這里就不做解析了。
這里提一下偽類和偽元素的區分:
1)偽類:存在的意義是為了通過選擇器找到那些不存在于DOM樹中的信息以及不能被常規CSS選擇器獲取到的信息。 偽類由一個冒號:開頭,冒號后面是偽類的名稱和包含在圓括號中的可選參數。
常用的偽類:
:active 向被激活的元素添加樣式。 :focus 向擁有鍵盤輸入焦點的元素添加樣式。 :hover 當鼠標懸浮在元素上方時,向元素添加樣式。 :link 向未被訪問的鏈接添加樣式。 :visited 向已被訪問的鏈接添加樣式。 :first-child 向元素的第一個子元素添加樣式。 :checked 向選中的控件元素添加樣式
2)偽元素:偽元素在DOM樹中創建了一些抽象元素,這些抽象元素是不存在于文檔語言里的(可以理解為html源碼);
注意: css3為了區分偽類和偽元素,規定偽類前面有一個冒號,偽元素前面有兩個冒號
常用偽元素:
關于偽元素的講解,可以參考CSS偽類與偽元素總是傻傻分不清,這份總結夠面試用了
::before 為作用元素的第一個子節點插入dom中 ::after 為作用元素的最后一個子節點插入dom中
自定義radio
html代碼:
<input type="radio" id="radio"> <label for="radio"></label>
css代碼:
input{ display:none; } label { display: inline-block; width: 30px; height: 30px; border: 1px solid #333; border-radius: 50%; position: relative; } label::after { -webkit-transition: all .5s ease; -moz-transition: all .5s ease; -o-transition: all .5s ease; -ms-transition: all .5s ease; transition: all .5s ease; cursor: pointer; position: absolute; width: 16px; height: 16px; border-radius: 50%; top: 50%; left: 50%; margin-top:-8px; margin-left:-8px; z-index: 1; content: ''; border:1px solid #333; } input:checked+label::after{ background:red; }
實現效果:
點擊前和點擊后:
自定義checkbox
漂亮的checkbox長這樣的,看著就很可愛
我們可以不追求那么完美的樣式,可以實現下面簡單好看的樣式就可以
html代碼:
<input type="checkbox" id="checkbox"> <label for="checkbox"></label>
css代碼:
input{ display:none; } label { display: inline-block; width: 30px; height: 30px; border: 1px solid #333; position: relative; } label::after { -webkit-transition: opacity .5s ease; -moz-transition: opacity .5s ease; -o-transition: opacity .5s ease; -ms-transition: opacity .5s ease; transition: opacity .5s ease; cursor: pointer; position: absolute; content: ''; opacity: 0; } input:checked+label::after{ border: 2px solid #d73d32; border-top: none; border-right: none; -webkit-transform: rotate(-45deg); -ms-transform: rotate(-45deg); transform: rotate(-45deg); width:20px; height:10px; top:50%; margin-top:-8px; left:50%; margin-left:-10px; opacity: 1.0; }
實現效果:
點擊前和點擊后:
自定義switch
繼續分享一個iOS風格的switch開關按鈕,樣子也非常常見,如圖:
主要是使用了<input ?type="checkbox">來模擬實現,具體的HTML:
html 代碼:
<label><input class="mui-switch" type="checkbox"> 默認未選中</label> <label><input class="mui-switch" type="checkbox" checked> 默認選中</label> <label><input class="mui-switch mui-switch-animbg" type="checkbox"> 默認未選中,簡單的背景過渡效果,加mui-switch-animbg類即可</label> <label><input class="mui-switch mui-switch-animbg" type="checkbox" checked> 默認選中</label> <label><input class="mui-switch mui-switch-anim" type="checkbox"> 默認未選中,過渡效果,加 mui-switch-anim 類即可</label> <label><input class="mui-switch mui-switch-anim" type="checkbox" checked> 默認選中</label>
在實際的使用中后來又增加了兩個過渡效果,分別加?mui-switch-animbg和mui-switch-anim?類即可,具體效果查看下面的demo頁面。
CSS代碼(SCSS導出的,排版有些奇怪):
css 代碼:
剩下部分
這里給出具體的css,方便大家復制本地實現
<style> .mui-switch { width: 52px; height: 31px; position: relative; border: 1px solid #dfdfdf; background-color: #fdfdfd; box-shadow: #dfdfdf 0 0 0 0 inset; border-radius: 20px; border-top-left-radius: 20px; border-top-right-radius: 20px; border-bottom-left-radius: 20px; border-bottom-right-radius: 20px; background-clip: content-box; display: inline-block; -webkit-appearance: none; user-select: none; outline: none; } .mui-switch:before { content: ''; width: 29px; height: 29px; position: absolute; top: 0px; left: 0; border-radius: 20px; border-top-left-radius: 20px; border-top-right-radius: 20px; border-bottom-left-radius: 20px; border-bottom-right-radius: 20px; background-color: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); } .mui-switch:checked { border-color: #64bd63; box-shadow: #64bd63 0 0 0 16px inset; background-color: #64bd63; } .mui-switch:checked:before { left: 21px; } .mui-switch.mui-switch-animbg { transition: background-color ease 0.4s; } .mui-switch.mui-switch-animbg:before { transition: left 0.3s; } .mui-switch.mui-switch-animbg:checked { box-shadow: #dfdfdf 0 0 0 0 inset; background-color: #64bd63; transition: border-color 0.4s, background-color ease 0.4s; } .mui-switch.mui-switch-animbg:checked:before { transition: left 0.3s; } .mui-switch.mui-switch-anim { transition: border cubic-bezier(0, 0, 0, 1) 0.4s, box-shadow cubic-bezier(0, 0, 0, 1) 0.4s; } .mui-switch.mui-switch-anim:before { transition: left 0.3s; } .mui-switch.mui-switch-anim:checked { box-shadow: #64bd63 0 0 0 16px inset; background-color: #64bd63; transition: border ease 0.4s, box-shadow ease 0.4s, background-color ease 1.2s; } .mui-switch.mui-switch-anim:checked:before { transition: left 0.3s; } /*# sourceMappingURL=mui-switch.css.map */ </style>
如果你喜歡scss,那么代碼更加簡潔
@mixin borderRadius($radius:20px) { border-radius: $radius; border-top-left-radius: $radius; border-top-right-radius: $radius; border-bottom-left-radius: $radius; border-bottom-right-radius: $radius; } $duration: .4s; $checkedColor: #64bd63; .mui-switch { width: 52px; height: 31px; position: relative; border: 1px solid #dfdfdf; background-color: #fdfdfd; box-shadow: #dfdfdf 0 0 0 0 inset; @include borderRadius(); background-clip: content-box; display: inline-block; -webkit-appearance: none; user-select: none; outline: none; &:before { content: ''; width: 29px; height: 29px; position: absolute; top: 0px; left: 0; @include borderRadius(); background-color: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); } &:checked { border-color: $checkedColor; box-shadow: $checkedColor 0 0 0 16px inset; background-color: $checkedColor; &:before { left: 21px; } } &.mui-switch-animbg { transition: background-color ease $duration; &:before { transition: left 0.3s; } &:checked { box-shadow: #dfdfdf 0 0 0 0 inset; background-color: $checkedColor; transition: border-color $duration, background-color ease $duration; &:before { transition: left 0.3s; } } } &.mui-switch-anim { transition: border cubic-bezier(0, 0, 0, 1) $duration, box-shadow cubic-bezier(0, 0, 0, 1) $duration; &:before { transition: left 0.3s; } &:checked { box-shadow: $checkedColor 0 0 0 16px inset; background-color: $checkedColor; transition: border ease $duration, box-shadow ease $duration, background-color ease $duration*3; &:before { transition: left 0.3s; } } } }
鏈接文章
https://www.html.cn/archives/9274
https://segmentfault.com/a/1190000003711140
*請認真填寫需求信息,我們會在24小時內與您取得聯系。