import 'dart:async';

import 'package:basketball/constants.dart';
import 'package:flutter/material.dart';

class BouncingBallApp extends StatefulWidget {
  const BouncingBallApp({Key? key}) : super(key: key);

  @override
  BouncingBallAppState createState() => BouncingBallAppState();
}

class BouncingBallAppState extends State<BouncingBallApp> {
  double floor = 0.0;
  double ceiling = 0;
  double ballPositionX = 0;
  double ballPositionY = 0;
  double ballVelocityX = 0;
  double ballVelocityY = 0;
  double gravity = 9.8;
  double restitution = 0.8;
  double friction = 0.98;
  bool isDragging = false;
  double rotationAngle = 0.0;
  double previousVelocityMagnitude = 0;

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

    Timer.periodic(const Duration(milliseconds: 16), (timer) {
      _updateBall(16);
    });
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    ballPositionY = MediaQuery.of(context).size.height * 0.9;
    ballPositionX = MediaQuery.of(context).size.width / 2;
  }

  void _onPanStart(DragStartDetails details) {
    setState(() {
      isDragging = true;
    });
  }

  void _onPanUpdate(DragUpdateDetails details) {
    setState(() {
      ballVelocityX += details.delta.dx;
      ballVelocityY += -details.delta.dy;
    });
  }

  void _onPanEnd(DragEndDetails details) {
    setState(() {
      isDragging = false;
      ballVelocityX *= friction;
      ballVelocityY *= friction;
    });
  }

  void _updateBall(double delta) {
    if (!isDragging) {
      double velocityMagnitude =
          (ballVelocityX.abs().ceil() + ballVelocityY.abs().ceil()).toDouble();
      double deltaVelocity = velocityMagnitude - previousVelocityMagnitude;
      previousVelocityMagnitude = velocityMagnitude;
      setState(() {
        ballPositionX += ballVelocityX * delta / 100;
        ballPositionY += ballVelocityY * delta / 100;

        if (ballPositionY + AppConstants.ballSize / 2 >= floor) {
          ballPositionY = floor - AppConstants.ballSize / 2;
          ballVelocityX *= friction;
          if ((ballVelocityY * ballVelocityY) < 4) {
            ballVelocityY = 0;
          } else {
            ballVelocityY *= -restitution;
          }
        } else if (ballPositionY - AppConstants.ballSize / 2 <= ceiling) {
          ballPositionY = ceiling + AppConstants.ballSize / 2;
          ballVelocityY *= -restitution;
        } else if ((ballVelocityY * ballVelocityY) + (ballVelocityX * ballVelocityX) < 5) {
          ballVelocityY = 0;
          ballVelocityX = 0;
        } else {
          ballVelocityY += gravity * delta / 100;
        }

        if (ballPositionX < AppConstants.ballSize / 2) {
          ballPositionX = AppConstants.ballSize / 2;
          ballVelocityX *= -restitution;
          ballVelocityY *= restitution;
        } else if (ballPositionX > MediaQuery.of(context).size.width - AppConstants.ballSize / 2) {
          ballPositionX = MediaQuery.of(context).size.width - AppConstants.ballSize / 2;
          ballVelocityX *= -restitution;
          ballVelocityY *= restitution;
        }

        rotationAngle += deltaVelocity;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    floor = MediaQuery.of(context).size.height;
    return GestureDetector(
      onPanStart: _onPanStart,
      onPanUpdate: _onPanUpdate,
      onPanEnd: _onPanEnd,
      child: Scaffold(
        body: Center(
          child: Stack(
            children: <Widget>[
              Positioned.fill(
                child: Image.asset(
                  'assets/background.png',
                  fit: BoxFit.fitWidth,
                ),
              ),
              Positioned(
                left: MediaQuery.of(context).size.width / 2 - AppConstants.shieldWidth / 2,
                top: MediaQuery.of(context).size.height * 0.2,
                child: Image.asset(
                  'assets/shield.png',
                  width: AppConstants.shieldWidth,
                  height: AppConstants.shieldHeight,
                  fit: BoxFit.fitWidth,
                ),
              ),
              Positioned(
                left: ballPositionX - AppConstants.ballSize / 2,
                top: ballPositionY - AppConstants.ballSize / 2,
                child: Transform.rotate(
                  angle: (rotationAngle + previousVelocityMagnitude) / 100,
                  child: const Image(
                    width: AppConstants.ballSize,
                    height: AppConstants.ballSize,
                    image: AssetImage('assets/ball.png'),
                  ),
                ),
              ),
              Positioned(
                left: MediaQuery.of(context).size.width / 2 - AppConstants.ringWidth / 2,
                top: MediaQuery.of(context).size.height * 0.2 + 112,
                child: const Image(
                  width: AppConstants.ringWidth,
                  height: AppConstants.ringHeight,
                  image: AssetImage('assets/ring.png'),
                ),
              ),
              Positioned(
                left: MediaQuery.of(context).size.width / 2 - AppConstants.ringWidth / 2 + 4,
                top: MediaQuery.of(context).size.height * 0.2 + 117,
                child: Container(
                  height: 8,
                  width: 4,
                  color: Colors.black,
                ),
              ),
              Positioned(
                left: MediaQuery.of(context).size.width / 2 + AppConstants.ringWidth / 2 - 6,
                top: MediaQuery.of(context).size.height * 0.2 + 117,
                child: Container(
                  height: 8,
                  width: 4,
                  color: Colors.black,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

void main() => runApp(MaterialApp(home: BouncingBallApp(key: UniqueKey())));

I have Flutter app that simulates the ball behavior. Also i have a 2D projection of basketball ring. Imagine, you are looking at the basketball ring staying in front of it. You see just a projection of ring that looks like flat line. That what i have in my app. For better illustration I added two black containers, emulating edges of the ring. I want the ball to collide with the left and right edges of the ring. And if the ball moving from the above of the ring passes between this two container I want to print a message in console (to consider this as a score).