Flutter - 個(gè)人主頁(yè)下拉放大背景圖

下拉放大背景在個(gè)人主頁(yè)的設(shè)計(jì)中比較常見(jiàn),但是我沒(méi)在Flutter的文章中找到實(shí)現(xiàn)方式。
結(jié)合了幾個(gè)文章的方案后,我嘗試修改實(shí)現(xiàn)方式,最終結(jié)合CustomScrollView 和Listener的方法實(shí)現(xiàn)了以下的方案

CustomScrollView

可以說(shuō)列表視圖想要自定義滾動(dòng)效果,繞不過(guò)CustomScrollView
詳細(xì)的定義可以參見(jiàn)其他博客https://juejin.im/post/5bceb534e51d457aa4596f9a
這里不詳細(xì)贅述

SliverPersistentHeader 可高度自定制的頭部伸縮視圖

很多文章里推薦大家用SliverAppBar來(lái)實(shí)現(xiàn)頭部,這種方案并不是很靈活
SliverPersistentHeader 可以說(shuō)對(duì)下拉放大背景圖的支持更好一些

SliverPersistentHeader(
              delegate: _SVPersonalAppBarDelegate(
                  minHeight: defaultHeight,
                  maxHeight: maxHeight,
                  child: _createImageWidget()),
            )

  _createImageWidget() {
    return Container(
      child: Image.network(
        'https://pic4.zhimg.com/80/v2-b02e601349241df0e3f25fd1ec622155_1440w.jpg',
        fit: BoxFit.cover,
      ),
    );
  }

SliverPersistentHeader 需要我們傳入一個(gè)SliverPersistentHeaderDelegate對(duì)象,SliverPersistentHeaderDelegate是一個(gè)抽象類,所以需要我們自己去繼承實(shí)現(xiàn)一個(gè)子類

class _SVPersonalAppBarDelegate extends SliverPersistentHeaderDelegate {
  _SVPersonalAppBarDelegate({
    @required this.minHeight,
    @required this.maxHeight,
    @required this.child,
  });

  final double minHeight;
  final double maxHeight;
  final Widget child;

  @override
  double get minExtent => minHeight;

  @override
  double get maxExtent => max(maxHeight, minHeight);

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return new SizedBox.expand(child: child);
  }

  @override
  bool shouldRebuild(_SVPersonalAppBarDelegate oldDelegate) {
    return maxHeight != oldDelegate.maxHeight ||
        minHeight != oldDelegate.minHeight ||
        child != oldDelegate.child;
  }
}

完成頭部視圖的定制后,我們需要關(guān)心的幾個(gè)問(wèn)題

  1. 放大后,解除觸摸,如何讓他自動(dòng)重置到正常狀態(tài)?
  2. 從頂部快速滑下后,如何制造彈性放大回置的動(dòng)畫?

我通過(guò)引入Listener 和狀態(tài)監(jiān)聽(tīng)來(lái)完成了這兩部份的實(shí)現(xiàn)
完整代碼如下

import 'dart:math';

import 'package:flutter/material.dart';

enum SVDragState {
    SVDragStateIdle,
    SVDragStateBegin,
    SVDrageStateEnd,
}

class SVPersonalInfoPage extends StatefulWidget {
  @override
  _SVPersonalInfoPageState createState() => _SVPersonalInfoPageState();
}

class _SVPersonalInfoPageState extends State<SVPersonalInfoPage> {
  double startOffsetY;
  static double defaultHeight = 250;
  static double maxHeight = 350.0;
  static double offsetY = 0;
  static double distance = maxHeight - defaultHeight;
  SVDragState dragState = SVDragState.SVDragStateIdle;
  final ScrollController controller =
      ScrollController(initialScrollOffset: distance);

  @override
  void initState() {
    controller.addListener(() {
      offsetY = controller.offset;
      if (offsetY <= 0) {
        controller.jumpTo(0);
        _resetWithAnimation(true);
      }
    });

    super.initState();
  }

  _resetWithAnimation(bool delay) {
    if (!delay) {
       if (controller.offset < distance && dragState == SVDragState.SVDrageStateEnd) {
        dragState = SVDragState.SVDragStateIdle;
        controller.animateTo(distance,
            duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
            return;
      }
    }
    Future.delayed(Duration(milliseconds: 200)).then((value) {
      if (controller.offset < distance && dragState == SVDragState.SVDrageStateEnd) {
        dragState = SVDragState.SVDragStateIdle;
        controller.animateTo(distance,
            duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
            return;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
        onPointerDown: (e) { dragState = SVDragState.SVDragStateBegin; },
        onPointerUp: (e) { 
          dragState = SVDragState.SVDrageStateEnd;
          _resetWithAnimation(false);  
        },
        child: CustomScrollView(
          controller: controller,
          slivers: <Widget>[
            SliverPersistentHeader(
              delegate: _SVPersonalAppBarDelegate(
                  minHeight: defaultHeight,
                  maxHeight: maxHeight,
                  child: _createImageWidget()),
            ),
            SliverList(delegate: SliverChildBuilderDelegate(
              (context, index) {
                return Container(
                  child: Text(
                    "This is item $index",
                    style: TextStyle(fontSize: 20),
                  ),
                  color: Colors.redAccent,
                );
              },
            ))
          ],
        ));
  }

  _createImageWidget() {
    return Container(
      child: Image.network(
        'https://pic4.zhimg.com/80/v2-b02e601349241df0e3f25fd1ec622155_1440w.jpg',
        fit: BoxFit.cover,
      ),
    );
  }
}

class _SVPersonalAppBarDelegate extends SliverPersistentHeaderDelegate {
  _SVPersonalAppBarDelegate({
    @required this.minHeight,
    @required this.maxHeight,
    @required this.child,
  });

  final double minHeight;
  final double maxHeight;
  final Widget child;

  @override
  double get minExtent => minHeight;

  @override
  double get maxExtent => max(maxHeight, minHeight);

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return new SizedBox.expand(child: child);
  }

  @override
  bool shouldRebuild(_SVPersonalAppBarDelegate oldDelegate) {
    return maxHeight != oldDelegate.maxHeight ||
        minHeight != oldDelegate.minHeight ||
        child != oldDelegate.child;
  }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

友情鏈接更多精彩內(nèi)容