// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';

void main() {
  runApp(const SliverAutoScrollExampleApp());
}

class SliverAutoScrollExampleApp extends StatelessWidget {
  const SliverAutoScrollExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: SliverAutoScrollExample());
  }
}

class SliverAutoScrollExample extends StatefulWidget {
  const SliverAutoScrollExample({super.key});

  @override
  State<SliverAutoScrollExample> createState() =>
      _SliverAutoScrollExampleState();
}

class _SliverAutoScrollExampleState extends State<SliverAutoScrollExample> {
  final GlobalKey alignedItemKey = GlobalKey();
  late final ScrollController scrollController;
  late double lastScrollOffset;

  @override
  void initState() {
    super.initState();
    scrollController = ScrollController();
  }

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

  void autoScrollTo(double offset) {
    scrollController.position.animateTo(
      offset,
      duration: const Duration(milliseconds: 300),
      curve: Curves.easeInOut,
    );
  }

  // After an interactive scroll ends, if the alignedItem is partially visible
  // at the top or bottom of the viewport, then auto-scroll so that it's
  // completely visible. To accommodate mouse-wheel scrolls and other small
  // adjustments, scrolls that change the scroll offset by less than
  // the alignedItem's extent don't trigger an auto-scroll.
  void maybeAutoScrollAlignedItem(RenderSliver alignedItem) {
    final SliverConstraints constraints = alignedItem.constraints;
    final SliverGeometry geometry = alignedItem.geometry!;
    final double sliverOffset = constraints.scrollOffset;

    if ((scrollController.offset - lastScrollOffset).abs() <=
        geometry.maxPaintExtent) {
      // Ignore scrolls that are smaller than the aligned item's extent.
      return;
    }
    final double overflow = geometry.maxPaintExtent - geometry.paintExtent;
    if (overflow > 0 && overflow < geometry.scrollExtent) {
      // indicates partial visibility
      if (sliverOffset > 0) {
        autoScrollTo(constraints.precedingScrollExtent); // top
      } else if (sliverOffset == 0) {
        autoScrollTo(scrollController.offset + overflow); // bottom
      }
    }
  }

  // Calls maybeAutoScrollAlignedItem in a post-frame callback so that
  // auto-scrolls are triggered _after_ the current scroll activity
  // has completed. Otherwise auto-scrolling would be a no-op.
  bool handleScrollNotification(ScrollNotification notification) {
    if (notification is ScrollStartNotification) {
      lastScrollOffset = scrollController.offset;
    }
    if (notification is ScrollEndNotification) {
      SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
        final RenderSliver? sliver = alignedItemKey.currentContext
            ?.findAncestorRenderObjectOfType<RenderSliver>();
        if (sliver != null && sliver.geometry != null) {
          maybeAutoScrollAlignedItem(sliver);
        }
      });
    }
    return false;
  }

  @override
  Widget build(BuildContext context) {
    const EdgeInsets horizontalPadding = EdgeInsets.symmetric(horizontal: 8);

    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 4),
          child: NotificationListener<ScrollNotification>(
            onNotification: handleScrollNotification,
            child: Scrollbar(
              controller: scrollController,
              thumbVisibility: true,
              child: CustomScrollView(
                controller: scrollController,
                slivers: <Widget>[
                  const SliverPadding(
                    padding: horizontalPadding,
                    sliver: ItemList(itemCount: 15),
                  ),
                  SliverPadding(
                    padding: horizontalPadding,
                    sliver: BigOrangeSliver(sliverChildKey: alignedItemKey),
                  ),
                  const SliverPadding(
                    padding: horizontalPadding,
                    sliver: ItemList(itemCount: 25),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

// A big list item that's easy to spot. The provided key is assigned to
// the aligned sliver's child so that we can find the this item's RenderSliver
// later with BuildContext.findAncestorRenderObjectOfType.
class BigOrangeSliver extends StatelessWidget {
  const BigOrangeSliver({super.key, required this.sliverChildKey});

  final Key sliverChildKey;

  @override
  Widget build(BuildContext context) {
    return SliverToBoxAdapter(
      child: Card(
        key: sliverChildKey,
        color: Colors.orange,
        child: const SizedBox(
          width: 300,
          child: ListTile(
            textColor: Colors.white,
            title: Padding(
              padding: EdgeInsets.symmetric(vertical: 32),
              child: Text('Aligned Item'),
            ),
          ),
        ),
      ),
    );
  }
}

// A placeholder SliverList of 50 items.
class ItemList extends StatelessWidget {
  const ItemList({super.key, this.itemCount = 50});

  final int itemCount;

  @override
  Widget build(BuildContext context) {
    final ColorScheme colorScheme = Theme.of(context).colorScheme;
    return SliverList.builder(
      itemCount: itemCount,
      itemBuilder: (BuildContext context, int index) {
        return Card(
          color: colorScheme.onSecondary,
          child: SizedBox(
            width: 100,
            child: ListTile(
              textColor: colorScheme.secondary,
              title: Text('Item $index.$itemCount'),
            ),
          ),
        );
      },
    );
  }
}
