序文
アドレス帳、連絡先リスト、都市選択リストなど、大量のデータを含む長いリスト ページでは、製品設計により、ユーザーがページの右側に縦方向のアルファベット順のインデックス リストが表示されることによく気づきます。クリックして選択します。長いリスト内で指定されたインデックス位置をすばやく見つけることができるため、ユーザーはフィルターしたいデータをすぐに見つけることができ、それによってユーザー エクスペリエンスが向上します。今日は、そのようなエクスペリエンス効果がどのように達成されるかを分析する例を取り上げます
城市列表
Flutter
技術的な実装分析
- ページの都市リスト レイアウトは、ListView ネストされた ListView を使用します。この中で、外側の ListView は現在の都市のグループ文字情報を表示する役割を果たし、内側の ListView は現在の文字インデックス グループにあるすべての都市を表示する役割を果たします。
- ListView を使用して、右側の縦方向のアルファベット インデックス リストを表示します。右側のアルファベット インデックスのいずれかをクリックすると、都市リストの現在のアルファベット インデックスのコーナー インデックス値が動的に計算され、クリックされた文字インデックスが計算されます。都市リストの高さの値は、
ScrollController
都市リストの文字をクリックすることで都市リストの位置を動的に配置する連動効果を実現できるようにすることで、計算された高さの位置に自動的に配置されます。
効果は以下の通りです
まず、上記の効果に関連するコードを参考までに掲載します。次に、コード内の位置の計算に関係するコア コードについて詳しく説明します。このコードには、ネットワーク リクエストとネットワーク ツール クラスのカプセル化が含まれますが、ここでは説明しません。読者は、Flutter (Seventeen) の入門レベルでの 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 データ形式に対応し、対応するエンティティ クラスを解析して、データがビューに正しくバインドできることを確認します。
難易度分析
記事の冒頭で、右側の文字インデックスの 1 つをクリックすると、都市リスト内の現在の文字インデックスの下付きインデックス値が動的に計算され、都市リスト内のクリックされた文字インデックスの高さの値が計算されます。計算されScrollController
た高さの位置に都市リストを自動配置することで、文字をクリックすることで都市リストの位置を動的に配置するという連動効果を実現します。
上の図を分析すると、配置する必要がある指定された位置をクリックしたときに、現在クリックされている文字インデックスの前にあるすべての項目の累積高さの値を計算し、指定された高さの値までスライドするだけでよいと結論付けることができます。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 Advanced Journey Column」を参照してください。