You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现正方形图片在半高容器内平滑滚动展示

问题描述

需要开发一个Flutter应用,实现以下效果:

  • 在高度为正方形图片一半的SizedBox中展示图片
  • 初始状态显示图片的顶部区域
  • 图片平滑向下滚动,逐步展示出图片的下半部分

尝试过ClipRect和AnimatedBuilder但未实现预期效果,以下是之前尝试的代码:

import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:ui' as ui;

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

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

class ImageScrollAnimation extends StatefulWidget {
  @override
  _ImageScrollAnimationState createState() => _ImageScrollAnimationState();
}

class _ImageScrollAnimationState extends State<ImageScrollAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _animation;
  Size? _imageSize;

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

    _animationController = AnimationController(
      vsync: this,
      duration: Duration(seconds: 3),
    );

    _animation = Tween<double>(begin: 0, end: 0.5).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Curves.easeInOut,
      ),
    );

    _animationController.forward();
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _initData() async {
    final image = Image.asset('assets/main.webp');
    _imageSize = await getImageSize(image);
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    double screenWidth = MediaQuery.of(context).size.width;

    if (_imageSize == null) {
      return Center(child: CircularProgressIndicator());
    }

    double adjustedHeight = getAdjustedHeight(_imageSize!, screenWidth);

    return SingleChildScrollView(
      child: Column(
        children: [
          AnimatedBuilder(
            animation: _animation,
            builder: (context, child) {
              return Transform.translate(
                offset: Offset(0, -_animation.value * adjustedHeight),
                child: child,
              );
            },
            child: Image.asset(
              'assets/main.webp',
              fit: BoxFit.fitWidth,
              width: screenWidth,
            ),
          ),
          Container(
            height: 800, // 滚动后显示的内容
            color: Colors.red,
          ),
          Container(
            height: 800,
            color: Colors.blue,
          ),
        ],
      ),
    );
  }
}

Future<Size> getImageSize(Image image) async {
  final Completer<ui.Image> completer = Completer<ui.Image>();

  image.image.resolve(ImageConfiguration()).addListener(
    ImageStreamListener((ImageInfo info, bool _) {
      completer.complete(info.image);
    }),
  );

  final ui.Image img = await completer.future;
  return Size(img.width.toDouble(), img.height.toDouble());
}

double getAdjustedHeight(Size imageSize, double screenWidth) {
  double result = imageSize.height * (screenWidth / imageSize.width);

  print('Adjusted Height: $result');

  return result;
}
解决方案

核心问题是之前的代码未对图片进行裁剪,导致图片超出容器范围。通过ClipRect配合Transform.translate,再用SizedBox固定显示区域高度,即可实现需求。

修改后的代码如下:

import 'package:flutter/material.dart';

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

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

class ImageScrollAnimation extends StatefulWidget {
  @override
  _ImageScrollAnimationState createState() => _ImageScrollAnimationState();
}

class _ImageScrollAnimationState extends State<ImageScrollAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _scrollOffset;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: Duration(seconds: 3),
    );
    // 动画值从0到1,对应图片向下滚动自身一半高度的偏移量
    _scrollOffset = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
    );
    _animationController.forward();
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final screenWidth = MediaQuery.of(context).size.width;
    // 正方形图片,显示区域高度为屏幕宽度的一半
    final displayHeight = screenWidth / 2;

    return SingleChildScrollView(
      child: Column(
        children: [
          // 固定显示区域高度
          SizedBox(
            height: displayHeight,
            width: screenWidth,
            // 裁剪超出区域的内容
            child: ClipRect(
              child: AnimatedBuilder(
                animation: _scrollOffset,
                builder: (context, child) {
                  // 向下偏移图片,偏移量为显示区域高度乘以动画值
                  return Transform.translate(
                    offset: Offset(0, displayHeight * _scrollOffset.value),
                    child: child,
                  );
                },
                child: Image.asset(
                  'assets/main.webp',
                  fit: BoxFit.fitWidth,
                  width: screenWidth,
                ),
              ),
            ),
          ),
          Container(height: 800, color: Colors.red),
          Container(height: 800, color: Colors.blue),
        ],
      ),
    );
  }
}

关键修改点:

  • 使用SizedBox固定显示区域高度为screenWidth / 2(正方形图片适配屏幕宽度后,高度等于屏幕宽度,一半即需求的展示高度)
  • 通过ClipRect裁剪超出SizedBox范围的图片部分
  • 调整Transform.translate偏移逻辑:动画值从0到1,偏移量为displayHeight * _scrollOffset.value,实现图片从顶部向下滚动、逐步展示下半部分的效果
  • 移除不必要的图片尺寸获取逻辑,利用正方形图片的特性简化计算

内容的提问来源于stack exchange,提问作者한경완

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 00:02:32