前言
在像通訊錄,聯系人列表,城市選擇列表等數據量比較多的長列表頁面中,我們經常會留意到產品設計會在頁面的右側區域提供一個豎向的字母索引列表,供用戶點擊選擇快速定位到長列表中的指定索引位置,以便于用戶快速定位到自己要篩選的數據,從而提升用戶體驗,今天我們就以
城市列表
為例,來剖析一下,這樣的體驗效果如果用Flutter
來實現
技術實現分析
- 頁面城市列表布局采用ListView嵌套ListView,其中外層ListView負責顯示當前城市的分組字母信息,內層Listview負責顯示當前字母索引分組下的所有城市
- 右側豎向字母索引列表采用ListView展示,當點擊右側字母索引的某一個時,動態的計算出,當前字母索引在城里列表的角標索引值,然后計算得出被點擊的字符索引在城里列表里的高度值,通過
ScrollController
讓城里列表自動定位到計算出的高度位置,從而實現,點擊字母所以動態定位城市列表位置的聯動效果
效果如下
我先把上述效果的相關代碼貼出來供大家參考,然后再把代碼里涉及到計算位置的核心代碼做下詳細的講解,代碼里涉及到網絡請求,以及網絡工具類的封裝這里就不再贅述了,讀者可以查看我往期博客關于Flutter入門進階之旅(十七)Flutter dio網絡請求里的具體講解,或者從github上直接找到源代碼閱讀https://github.com/xiedong11/flutter_app
核心代碼
/**
* @desc 選擇城市地區聯動索引頁
* @author xiedong
* @date 2020-04-30.
*/
class PhoneCountryCodePage extends StatefulWidget {
@override
State<StatefulWidget> createState() => PageState();
}
class PageState extends State<PhoneCountryCodePage> {
var GET_PHONE_COUNTRY_CODE_URL =
"https://raw.githubusercontent.com/xiedong11/flutter_app/master/static/phoneCode.json";
List<String> letters = [];
List<PhoneCountryCodeData> data;
ScrollController _scrollController = ScrollController();
int _currentIndex = 0;
@override
void initState() {
super.initState();
getPhoneCodeDataList();
}
getPhoneCodeDataList() async {
var response = await DioUtils.getInstance().get(GET_PHONE_COUNTRY_CODE_URL);
var resultEntity = new PhoneCountryCodeEntity.fromJson(json.decode(response));
if(resultEntity.code==200){
this.setState(() {
data = resultEntity.data;
for (int i = 0; i < data.length; i++) {
letters.add(data[i].name.toUpperCase());
}
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("城市地區選擇"),
centerTitle: true,
),
body: Stack(
children: <Widget>[
data == null || data.length == 0
? Text("")
: Padding(
padding: EdgeInsets.only(left: 20),
child: ListView.builder(
controller: _scrollController,
itemCount: data.length,
itemBuilder: (BuildContext context, int index) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
PhoneCodeIndexName(data[index].name.toUpperCase()),
ListView.builder(
itemBuilder:
(BuildContext context, int index2) {
return Container(
height: 46,
child: GestureDetector(
// behavior: HitTestBehavior.translucent,
child: Padding(
padding:
EdgeInsets.symmetric(vertical: 10),
child: Row(
children: <Widget>[
Text(
"${data[index].listData[index2].name}",
style: TextStyle(
fontSize: 16,
color: Color(0xff434343))),
Margin(width: 10),
Text(
"+${data[index].listData[index2].code}",
style: TextStyle(
fontSize: 16,
color: Color(0xffD6D6D6)),
)
],
),
),
onTap: () {
Navigator.of(context).pop(
data[index].listData[index2].code);
},
),
);
},
itemCount: data[index].listData.length,
shrinkWrap: true,
physics:
NeverScrollableScrollPhysics()) //禁用滑動事件),
],
);
}),
),
Align(
alignment: new FractionalOffset(1.0, 0.5),
child: SizedBox(
width: 25,
child: Padding(
padding: EdgeInsets.only(top: 20),
child: ListView.builder(
itemCount: letters.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
child: Text(
letters[index],
style: TextStyle(color: Colors.black),
),
onTap: () {
setState(() {
_currentIndex = index;
});
var height = index * 45.0;
for (int i = 0; i < index; i++) {
height += data[i].listData.length * 46.0;
}
_scrollController.jumpTo(height);
},
);
},
),
),
),
)
],
),
);
}
}
class PhoneCodeIndexName extends StatelessWidget {
String indexName;
PhoneCodeIndexName(this.indexName);
Widget build(BuildContext context) {
return Container(
height: 45,
child: Padding(
child: Text(indexName,
style: TextStyle(fontSize: 20, color: Color(0xff434343))),
padding: EdgeInsets.symmetric(vertical: 10),
),
);
}
}
Json數據映射實體類
class PhoneCountryCodeEntity {
int code;
List<PhoneCountryCodeData> data;
String message;
PhoneCountryCodeEntity({this.code, this.data, this.message});
PhoneCountryCodeEntity.fromJson(Map<String, dynamic> json) {
code = json['code'];
if (json['data'] != null) {
data = new List<PhoneCountryCodeData>();
(json['data'] as List).forEach((v) {
data.add(new PhoneCountryCodeData.fromJson(v));
});
}
message = json['message'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['code'] = this.code;
if (this.data != null) {
data['data'] = this.data.map((v) => v.toJson()).toList();
}
data['message'] = this.message;
return data;
}
}
class PhoneCountryCodeData {
List<PhoneCountryCodeDataListdata> listData;
String name;
PhoneCountryCodeData({this.listData, this.name});
PhoneCountryCodeData.fromJson(Map<String, dynamic> json) {
if (json['listData'] != null) {
listData = new List<PhoneCountryCodeDataListdata>();
(json['listData'] as List).forEach((v) {
listData.add(new PhoneCountryCodeDataListdata.fromJson(v));
});
}
name = json['name'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
if (this.listData != null) {
data['listData'] = this.listData.map((v) => v.toJson()).toList();
}
data['name'] = this.name;
return data;
}
}
class PhoneCountryCodeDataListdata {
String code;
String name;
int id;
String groupCode;
PhoneCountryCodeDataListdata({this.code, this.name, this.id, this.groupCode});
PhoneCountryCodeDataListdata.fromJson(Map<String, dynamic> json) {
code = json['code'];
name = json['name'];
id = json['id'];
groupCode = json['groupCode'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['code'] = this.code;
data['name'] = this.name;
data['id'] = this.id;
data['groupCode'] = this.groupCode;
return data;
}
}
博客中Json數據的測試地址為:https://raw.githubusercontent.com/xiedong11/flutter_app/master/static/phoneCode.json 讀者測試代碼時如果自己的json數據格式跟我的不一樣,應該對應自己的json數據格式去解析對應的實體類,確保數據能正確的綁定到視圖上。
難點分析
文章開頭當點擊右側字母索引的某一個時,動態的計算出當前字母索引在城里列表的角標索引值,然后計算得出被點擊的字符索引在城里列表里的高度值,通過ScrollController
讓城里列表自動定位到計算出的高度位置,從而實現,點擊字母所以動態定位城市列表位置的聯動效果。
我們通過分析上圖可以得出,當我們點擊需要定位到的指定位置時,我們只需要計算出當前被點擊的字母索引之前的所有item的累加高度值,然后通過ListView
中的ScrollController.jumpTo(double value)
滑動到指定的高度值,就完成了這整個聯動效果。
這里,我們通過遍歷我們從網絡上獲取數據值,很容易計算出需要滑動的高度累加值
核心代碼如下
var height = index * 45.0; //45.0 字母分組的高度
for (int i = 0; i < index; i++) {
height += data[i].listData.length * 46.0; //46.0 每個Item的高度
}
_scrollController.jumpTo(height);
至此整個實現效果已經完成,完整的項目跟代碼請參閱:Flutter進階之旅專欄