Flutter-循環滾動、首尾相連、停留在中間的視圖

圖片描述

天長地久有時盡
此恨綿綿無絕期

前言

設計來自項目中搜索模塊的更多篩選功能,篩選宜居人數
主要功能:
支持循環滾動、且每次都停留在屏幕中間位置
首尾相連
點擊滾動到屏幕中間位置

默認樣式

圖片描述

滾動之後樣式
圖片描述
圖片描述

設計思路

ListView.builder滾動視圖
NotificationListener監聽開始滾動和結束滾動時候的位置
ScrollController控制視圖滾動到中間位置

核心邏輯一

NotificationListener監聽ListView滾動,
ScrollController滾動到視圖中間位置,
isScrollEndNotification解決由於內部_controller.jumpTo方法會無限調用滾動結束事件

    // 監聽事件
    NotificationListener<ScrollNotification>(
              child: ListView.builder(
                controller: _controller,
                itemCount: _list.length * 10000,// 初始化10000個item
                itemExtent: width / 7,
                itemBuilder: (BuildContext context, int index) {
                   // index % _list.length 無限輪播
                  return _listViewItem(_list[index % _list.length], index, singleItemWidth);
                },
                scrollDirection: Axis.horizontal,
              ),
              onNotification: (ScrollNotification notification) {
                  //   開始滾動的監聽事件
                if(notification is ScrollStartNotification) {
                  isScrollEndNotification = false;
                  _startLocation = notification.metrics.pixels;
                }
                //   滾動結束的監聽事件
                if (notification is ScrollEndNotification && !isScrollEndNotification) {
                  _endLocation = notification.metrics.pixels;
                  isScrollEndNotification = true;
                  double differ = _endLocation-_startLocation;
                  double offset = 0;
                  if(differ>0) {
                    offset = (differ.abs()~/singleItemWidth)*singleItemWidth;
                    if(differ%singleItemWidth >= singleItemWidth/2) {
                      offset += singleItemWidth;
                    }
                    // _controller滾到中間的位置,
                    _controller.jumpTo(_startLocation + offset);
                  } else if(differ<0){
                    differ = differ.abs();
                    offset = ((differ~/singleItemWidth)*singleItemWidth);
                    if((differ%singleItemWidth) >= (singleItemWidth/2)) {
                      offset += singleItemWidth;
                    }
                    // _controller滾到中間的位置
                    _controller.jumpTo(_startLocation - offset);
                  }
                }
                double result = notification.metrics.pixels/singleItemWidth;
                int round = result.round();// 四捨五入
                // 計算索引並返回給外部
                widget.slideAction(round%12);// 取餘之後返回索引
                return true;
              },
            ),

核心邏輯二

每個item對應的widget
點擊item滾動到視圖中間位置

    Widget _listViewItem(String title, int index, double singleItemWidth) {
        return GestureDetector(
          onTap: (){
            // 滾動到中間位置 
            double offset = (index-3)*singleItemWidth;
            _controller.jumpTo(offset);
            widget.slideAction((index-3)%12);
          },
          child: Container(
            color: Colors.white,
            alignment: Alignment.center,
            child: Text(
              title,
              style: TextStyle(
                color: ColorUtil.color('#212121'),
                fontSize: 12,
              ),
            ),
          ),
        );
      }

全部源碼

import 'package:flutter/material.dart';
// 一個顏色的三方插件
import 'package:flutter_color_plugin/flutter_color_plugin.dart';

class HousePerson extends StatefulWidget {
  final int selectIndex;// 外部傳入默認選擇第幾個
  final Function slideAction; // 滾動停止的回調方法,給外部傳選中的索引值
  bool clearData; // 支持清楚數據功能,true代表恢復默認樣式
  HousePerson({this.slideAction, this.selectIndex, this.clearData});
  @override
  _HousePersonState createState() => _HousePersonState();
}

class _HousePersonState extends State<HousePerson> {
  List<String> _list = [
    '9人',
    '10人',
    '10+',
    '不限',
    '1人',
    '2人',
    '3人',
    '4人',
    '5人',
    '6人',
    '7人',
    '8人',
  ];

  bool isScrollEndNotification = false;
  ScrollController _controller;
  double _startLocation = 0;
  double _endLocation = 0;
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    double screenWidth = MediaQuery.of(context).size.width;
    int select = widget.selectIndex > 0 ? widget.selectIndex : 0;
    _controller = ScrollController(
      initialScrollOffset: (3000+select) * (screenWidth - 40) / 7,
    );
  }

  @override
  void dispose() {
    //爲了避免內存泄露,需要調用_controller.dispose
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final screenWidth = MediaQuery.of(context).size.width;
    if(widget.clearData) {
      _controller.jumpTo(3000 * (screenWidth - 40) / 7);
      widget.clearData = false;
    }

    return Container(
      color: Colors.white,
      child: Column(
        children: <Widget>[
          Row(
            children: <Widget>[
              SizedBox(
                width: 20,
              ),
              Text(
                '最多宜居',
                style: TextStyle(
                  fontSize: 15,
                  color: ColorUtil.color('#212121'),
                  fontFamily: 'PingFangSC-Semibold',
                  fontWeight: FontWeight.bold,
                ),
              ),
            ],
          ),
          SizedBox(
            height: 15,
          ),
          _line(screenWidth - 40),
          Container(
            width: screenWidth - 40,
            color: Colors.white,
            height: 50,
            child: _listView(screenWidth - 40),
          ),
          _line(screenWidth - 40),
        ],
      ),
    );
  }

  Widget _line(double width) {
    return Container(
      width: width,
      height: 0.5,
      color: ColorUtil.color('#BDBDBD'),
    );
  }

  Widget _listView(double width) {
    double singleItemWidth = width/7;

    return Stack(
      children: <Widget>[
        NotificationListener<ScrollNotification>(
          child: ListView.builder(
            controller: _controller,
            itemCount: _list.length * 10000,
            itemExtent: width / 7,
            itemBuilder: (BuildContext context, int index) {
              return _listViewItem(_list[index % _list.length], index, singleItemWidth);
            },
            scrollDirection: Axis.horizontal,
          ),
          onNotification: (ScrollNotification notification) {
            if(notification is ScrollStartNotification) {
              isScrollEndNotification = false;
              _startLocation = notification.metrics.pixels;
            }
            if (notification is ScrollEndNotification && !isScrollEndNotification) {
              _endLocation = notification.metrics.pixels;
              isScrollEndNotification = true;
              double differ = _endLocation-_startLocation;
              double offset = 0;
              if(differ>0) {
                offset = (differ.abs()~/singleItemWidth)*singleItemWidth;
                if(differ%singleItemWidth >= singleItemWidth/2) {
                  offset += singleItemWidth;
                }
                _controller.jumpTo(_startLocation + offset);
              } else if(differ<0){
                differ = differ.abs();
                offset = ((differ~/singleItemWidth)*singleItemWidth);
                if((differ%singleItemWidth) >= (singleItemWidth/2)) {
                  offset += singleItemWidth;
                }
                _controller.jumpTo(_startLocation - offset);
              }
            }
            double result = notification.metrics.pixels/singleItemWidth;
            int round = result.round();// 四捨五入
            widget.slideAction(round%12);// 取餘之後返回索引
            return true;
          },
        ),
        Positioned(
          left: width / 2 - 15,
          top: 0,
          child: Container(
            width: 30,
            height: 3,
            color: ColorUtil.color('#FED836'),
          ),
        ),
        Positioned(
          left: width / 2 - 15,
          bottom: 0,
          child: Container(
            width: 30,
            height: 3,
            color: ColorUtil.color('#FED836'),
          ),
        ),
      ],
    );
  }

  Widget _listViewItem(String title, int index, double singleItemWidth) {
    return GestureDetector(
      onTap: (){
        // 滾動到中間位置
        double offset = (index-3)*singleItemWidth;
        _controller.jumpTo(offset);
        widget.slideAction((index-3)%12);
      },
      child: Container(
        color: Colors.white,
        alignment: Alignment.center,
        child: Text(
          title,
          style: TextStyle(
            color: ColorUtil.color('#212121'),
            fontSize: 12,
          ),
        ),
      ),
    );
  }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章