【譯】定製Flutter滾動效果

原文地址:medium.com/flutter-com…git

[譯者注:ScrollPhysics 很是強大好用,能夠定製各類滑動效果,經過設置阻尼係數等等實現]github

在這篇文章中咱們將定製ScrollPhysics來改變ListView的滾動行爲

KISS (保持簡單....別想多了,哈哈)

在一個多頁面集合或者幻燈片集合循環訪問,是一個常常出現的場景。spring

實現這個效果的代碼很是簡單,咱們只須要使用PageView的默認屬性就能夠了。bash

import 'dart:math';

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final List<int> pages = List.generate(4, (index) => index);
@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: PageView.builder(
          itemCount: pages.length,
          itemBuilder: (context, index) {
            return Container(
              color: randomColor,
              margin: const EdgeInsets.all(20.0),
            );
          },
        ),
      ),
    );
  }
Color get randomColor =>
      Color((Random().nextDouble() * 0xFFFFFF).toInt() << 0).withOpacity(1.0);
}
複製代碼

源碼引用至GitHubapp

很是酷炫. 可是.less

有時候,咱們想要給用戶一些提示;或者咱們滾動的列表中的元素不是真的全頁面。這種狀況下,若是當前的頁面只填充視圖的一部分,讓咱們能看到下個元素(或者上個元素)那就太好了。dom

不用擔憂,在Flutter中使用PageController就能作到。ide

代碼依然很是簡單。咱們只須要把想要的視圖的百分比設置到viewportFraction屬性就能夠了。

class MyHomePage extends StatelessWidget {
  final List<int> pages = List.generate(4, (index) => index);
  final _pageController = PageController(viewportFraction: 0.8);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: PageView.builder(
          controller: _pageController,
          itemCount: pages.length,
          itemBuilder: (context, index) {
            return Container(
              color: randomColor,
              margin: const EdgeInsets.all(20.0),
            );
          },
        ),
      ),
    );
  }
複製代碼

很是酷炫. 可是.函數

若是這不是咱們想要的效果呢?我想要元素想一整個列表同樣,而不是居中;可是又想要一次滾動一個元素。ui

爲了實現這個效果,咱們須要深刻了解一下個咱們還沒用過的屬性:ScrollPhysics

Row vs PageView

PageView更多的是爲用戶滑動的一組頁面設計的,有點像播放幻燈片。咱們的狀況有些不一樣,由於咱們想要一個列表的效果,但同時又想一次滾動一個元素。放棄PageView,而使用ListView更加符合需求。

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: ListView.builder(
          scrollDirection: Axis.horizontal,
          itemCount: pages.length,
          itemBuilder: (context, index) => Container(
            height: double.infinity,
            width: 300,
            color: randomColor,
            margin: const EdgeInsets.all(20.0),
          ),
        ),
      ),
    );
  }
複製代碼

簡單。可是若是你向右滑動,就會發現不能一次滑動一個元素。 咱們如今是在處理List裏面的元素了,再也不是頁面。因此咱們須要本身創建頁面的概念,咱們可使用ListView的physics屬性作到這種效果。

ScrollPhysics

已經有不一樣的ScrollPhysics之類能夠用來控制滑動效果;其中有一個看起來很是有趣,PageScrollPhysics。PageView內部使用的就是PageScrollPhysics, 不興的是,在ListView中使用無效。咱們能夠本身設計一個出來,先看看PageScrollPhysics的實現。

class PageScrollPhysics extends ScrollPhysics {
  /// Creates physics for a [PageView].
  const PageScrollPhysics({ ScrollPhysics parent }) : super(parent: parent);

  @override
  PageScrollPhysics applyTo(ScrollPhysics ancestor) {
    return PageScrollPhysics(parent: buildParent(ancestor));
  }

  double _getPage(ScrollPosition position) {
    if (position is _PagePosition)
      return position.page;
    return position.pixels / position.viewportDimension;
  }

  double _getPixels(ScrollPosition position, double page) {
    if (position is _PagePosition)
      return position.getPixelsFromPage(page);
    return page * position.viewportDimension;
  }

  double _getTargetPixels(ScrollPosition position, Tolerance tolerance, double velocity) {
    double page = _getPage(position);
    if (velocity < -tolerance.velocity)
      page -= 0.5;
    else if (velocity > tolerance.velocity)
      page += 0.5;
    return _getPixels(position, page.roundToDouble());
  }

  @override
  Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
    // If we're out of range and not headed back in range, defer to the parent // ballistics, which should put us back in range at a page boundary. if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) || (velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) return super.createBallisticSimulation(position, velocity); final Tolerance tolerance = this.tolerance; final double target = _getTargetPixels(position, tolerance, velocity); if (target != position.pixels) return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance); return null; } @override bool get allowImplicitScrolling => false; } 複製代碼

源碼引用至GitHub

方法createBallisticSimulation是這個類的入口,將滾動條中的位置和速度做爲輸入參數。 首先這是在檢查用戶是向右滾動仍是向左滾動,接着計算滾動條中的新位置,也就是將當前加或減視圖的範圍,由於頁面視圖中的滾動是一個接一個的。

咱們要作的很是相似,可是咱們沒有使用視圖(viewport),而是使用自定義的大小,由於每一個視圖有多個元素。

這個自定義的大小咱們能夠本身計算,它是滾動條的總大小除以列表元素個數再減1.爲何要減1呢?列表中有1個元素不能滑動,2個元素就能滑1個元素...因此N個元素就能滑N-1

CustomScrollPhysics

class CustomScrollPhysics extends ScrollPhysics {
  final double itemDimension;

  CustomScrollPhysics({this.itemDimension, ScrollPhysics parent})
      : super(parent: parent);

  @override
  CustomScrollPhysics applyTo(ScrollPhysics ancestor) {
    return CustomScrollPhysics(
        itemDimension: itemDimension, parent: buildParent(ancestor));
  }

  double _getPage(ScrollPosition position) {
    return position.pixels / itemDimension;
  }

  double _getPixels(double page) {
    return page * itemDimension;
  }

  double _getTargetPixels(
      ScrollPosition position, Tolerance tolerance, double velocity) {
    double page = _getPage(position);
    if (velocity < -tolerance.velocity) {
      page -= 0.5;
    } else if (velocity > tolerance.velocity) {
      page += 0.5;
    }
    return _getPixels(page.roundToDouble());
  }
@override
  Simulation createBallisticSimulation(
      ScrollMetrics position, double velocity) {
    // If we're out of range and not headed back in range, defer to the parent // ballistics, which should put us back in range at a page boundary. if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) || (velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) return super.createBallisticSimulation(position, velocity); final Tolerance tolerance = this.tolerance; final double target = _getTargetPixels(position, tolerance, velocity); if (target != position.pixels) return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance); return null; } @override bool get allowImplicitScrolling => false; } 複製代碼

源碼引用至GitHub

咱們重寫getPixels()讓它返回基於頁碼的位置,重寫getPage()返回基於位置的頁碼,最後再把itemDimension傳入構造函數。

使用CustomScrollPhysics

幸運的是ScrollController能夠獲取滾動條的長度;可是呢,須要等到widget被建立出來以後才能夠拿到。咱們須要把咱們的Page改成StatefulWidget,去監聽dimensions的有效性通知,而後初始化CustomScrollPhysics

final _controller = ScrollController();

final List<int> pages = List.generate(4, (index) => index);

ScrollPhysics _physics;

@override
void initState() {
  super.initState();

  _controller.addListener(() {
    if (_controller.position.haveDimensions && _physics == null) {
      setState(() {
        var dimension =
            _controller.position.maxScrollExtent / (pages.length - 1);
        _physics = CustomScrollPhysics(itemDimension: dimension);
      });
    }
  });
}
複製代碼

到此,咱們可讓List一次滑動一個元素了。

總結

這是一個自定義ScrollPhysics來定製滑動效果的簡單例子;在示例中咱們讓ListView一次滑動一個元素。 完整的代碼以下:

import 'dart:math';

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _controller = ScrollController();

  final List<int> pages = List.generate(4, (index) => index);

  ScrollPhysics _physics;

  @override
  void initState() {
    super.initState();

    _controller.addListener(() {
      if (_controller.position.haveDimensions && _physics == null) {
        setState(() {
          var dimension =
              _controller.position.maxScrollExtent / (pages.length - 1);
          _physics = CustomScrollPhysics(itemDimension: dimension);
        });
      }
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: ListView.builder(
          scrollDirection: Axis.horizontal,
          controller: _controller,
          physics: _physics,
          itemCount: pages.length,
          itemBuilder: (context, index) => Container(
            height: double.infinity,
            width: 300,
            color: randomColor,
            margin: const EdgeInsets.all(20.0),
          ),
        ),
      ),
    );
  }

  Color get randomColor =>
      Color((Random().nextDouble() * 0xFFFFFF).toInt() << 0).withOpacity(1.0);
}

class CustomScrollPhysics extends ScrollPhysics {
  final double itemDimension;

  CustomScrollPhysics({this.itemDimension, ScrollPhysics parent})
      : super(parent: parent);

  @override
  CustomScrollPhysics applyTo(ScrollPhysics ancestor) {
    return CustomScrollPhysics(
        itemDimension: itemDimension, parent: buildParent(ancestor));
  }

  double _getPage(ScrollPosition position) {
    return position.pixels / itemDimension;
  }
  
  double _getPixels(double page) {
    return page * itemDimension;
  }

  double _getTargetPixels(
      ScrollPosition position, Tolerance tolerance, double velocity) {
    double page = _getPage(position);
    if (velocity < -tolerance.velocity) {
      page -= 0.5;
    } else if (velocity > tolerance.velocity) {
      page += 0.5;
    }
    return _getPixels(page.roundToDouble());
  }

  @override
  Simulation createBallisticSimulation(
      ScrollMetrics position, double velocity) {
    // If we're out of range and not headed back in range, defer to the parent // ballistics, which should put us back in range at a page boundary. if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) || (velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) return super.createBallisticSimulation(position, velocity); final Tolerance tolerance = this.tolerance; final double target = _getTargetPixels(position, tolerance, velocity); if (target != position.pixels) return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance); return null; } @override bool get allowImplicitScrolling => false; } 複製代碼

源碼引用至GitHub

就寫這麼多,歡迎提問互動。感謝您的閱讀!

相關文章
相關標籤/搜索