main image

Flutter 3D Controller

User Image

Janis Akmentins

2024-08-30

In this tutorial we will create a simple product page, but we will use a 3D object. To do it we will utilize flutter_3d_controller package.

This is a versatile 3D rendering package, that also allows to control animations. The goal is to create an interactive page, where the object slowly spins arround, but the user can hold it and rotate it as they please.

Here's a preview of how the complete page will look.

Example Image

 

Pretty cool right? So lets begin by adding the package. In my case, I'm using version 1.3.1.

You can paste it in pubspec.yaml file manually, or using a command:

 

flutter pub add flutter_3d_controller

 

To create this functionality successfully, there are a few things to note. First of all we will need some 3D assets. For a business project, these would likely be created in accordance to your product offering. For testing, you can download some free assets from sites like sketchfab.com. I recommend working with glb or gltf formats, but flutter_3d_controller will support other formats as well. Next we will make an auto-rotate function that will periodically update the angle variable. We then can use this angle value in setCameraOrbit function to rotate the 3D object. This now will rotate the object, but we also want to be able to interact with it via touch. While it might seem intuitive to use the GestureDetector to disable auto-rotation when we touch the object, this will not work as expected, as the Flutter3DViewer widget handles touch events internally. One way to work arround that is to use PointerEvent. This will allow us to detect touch interactions and manage the auto-rotation behavior.

 

And here is the full code for creating such a page. Of course, feel free to design it however you like. While this example is for a product page, you can easily adapt the code for different purposes depending on your project's needs.

 

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_3d_controller/flutter_3d_controller.dart';
 
class ProductPage extends StatefulWidget {
  const ProductPage({super.key});
 
  @override
  State<ProductPage> createState() => _ProductPageState();
}
 
class _ProductPageState extends State<ProductPage> {
  Flutter3DController myController = Flutter3DController();
  Timer? _rotationTimer;
  double angle = 0.0;
  bool isRotating = true;
  DateTime? lastUpdate;
  bool userInteracting = false;
 
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.pointerRouter.addGlobalRoute(_handlePointerEvent);
    _autoRotate();
  }
 
  @override
  void dispose() {
    WidgetsBinding.instance.pointerRouter
        .removeGlobalRoute(_handlePointerEvent);
    _rotationTimer?.cancel();
    super.dispose();
  }
 
  void _handlePointerEvent(PointerEvent event) {
    if (event is PointerDownEvent) {
      _stopRotation();
      userInteracting = true;
    } else if (event is PointerUpEvent) {
      userInteracting = false;
      _resumeRotation();
    }
  }
 
  void _autoRotate() {
    _rotationTimer = Timer.periodic(const Duration(milliseconds: 30), (timer) {
      if (isRotating && !userInteracting) {
        setState(() {
          angle += 1.0;
        });
 
        // updating the camera every 200 miliseconds
        if (lastUpdate == null ||
            DateTime.now().difference(lastUpdate!) >
                const Duration(milliseconds: 200)) {
          lastUpdate = DateTime.now();
          _updateCameraOrbit();
        }
      }
    });
  }
 
  void _updateCameraOrbit() {
    if (isRotating) {
      myController.setCameraOrbit(angle, 70, 100);
    }
  }
 
  void _stopRotation() {
    setState(() {
      isRotating = false;
    });
  }
 
  void _resumeRotation() {
    setState(() {
      isRotating = true;
    });
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("RedBull (Original)"),
        backgroundColor: Colors.indigo.shade900,
        foregroundColor: Colors.white,
        leading: const Icon(Icons.arrow_back_ios),
        actions: [
          IconButton(
            icon: const Icon(Icons.shopping_cart),
            onPressed: () {
              // navigate to cart logic
            },
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            SizedBox(
              height: 300,
              child: Flutter3DViewer(
                controller: myController,
                progressBarColor: Colors.transparent,
                src: 'assets/3d/redbull_can.glb', // your 3d asset
              ),
            ),
            const Divider(
              color: Colors.grey,
              thickness: 1.0,
            ),
            const Text(
              "RedBull (Original)",
              style: TextStyle(fontSize: 26.0, fontWeight: FontWeight.w500),
            ),
            const Text(
              "250ml",
              style: TextStyle(fontSize: 22.0, fontWeight: FontWeight.w300),
            ),
            const SizedBox(
              height: 4,
            ),
            const Text('Nutritional Information (per 250 ml can):'),
            Text(
              'Calories: 110 kcal\n'
              'Total Fat: 0 g\n'
              'Cholesterol: 0 mg\n'
              'Sodium: 105 mg (4% DV)\n'
              'Total Carbohydrate: 28 g (9% DV)\n'
              'Niacin (Vitamin B3): 22 mg (110% DV)\n'
              'Vitamin B6: 5 mg (250% DV)\n'
              'Vitamin B12: 5.1 µg (213% DV)\n'
              'Caffeine: 80 mg\n'
              'Taurine: 1000 mg\n'
              'Glucuronolactone: 600 mg\n'
              'Inositol: 50 mg\n',
              style: TextStyle(
                  color: Colors.indigo.shade900,
                  fontSize: 12.0,
                  fontWeight: FontWeight.w300),
            ),
            const SizedBox(
              height: 20,
            ),
            // add to cart button
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [
                Row(
                  children: [
                    IconButton(
                      icon: const Icon(Icons.remove),
                      onPressed: () {
                        // decrease count
                      },
                    ),
                    const Text(
                      '1', // here should be the count
                      style: TextStyle(fontSize: 18.0),
                    ),
                    IconButton(
                      icon: const Icon(Icons.add),
                      onPressed: () {
                        // increase count
                      },
                    ),
                  ],
                ),
                ElevatedButton(
                  onPressed: () {
                    // add to cart logic goes here
                  },
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.indigo.shade900,
                    foregroundColor: Colors.white,
                    padding: const EdgeInsets.symmetric(
                        horizontal: 20.0, vertical: 12.0),
                  ),
                  child: const Text(
                    'Add to Cart',
                    style: TextStyle(fontSize: 18.0),
                  ),
                ),
              ],
            )
          ],
        ),
      ),
    );
  }
}

0

1

Comments

Please log in to post comments

  • No comments yet.