How to make opaque tutorial screen in flutter?
Asked Answered
P

6

36

I want to make tutorial screen that show to user at beginning. it's like below :

enter image description here

my specific question, how to make some certain elements will show normally and other are opaque ?

also the arrow and text, how to make them point perfectly based on mobile device screen size (mobile responsiveness) ?

Phosphocreatine answered 30/6, 2019 at 3:54 Comment(2)
I think easiest ways is, You have to create this type of image and display on the top of your screen by use of stack and wrap it into Opacity widget so you can hide it after click on tap. Thats it.Bruis
@Bruis but how the image can fit various mobile screen size ?Phosphocreatine
M
21

As RoyalGriffin mentioned, you can use highlighter_coachmark library, and I am also aware of the error you are getting, the error is there because you are using RangeSlider class which is imported from 2 different packages. Can you try this example in your app and check if it is working?

  1. Add highlighter_coachmark to your pubspec.yaml file

    dependencies:
      flutter:
        sdk: flutter
    
      highlighter_coachmark: ^0.0.3
    
  2. Run flutter packages get


Example:

import 'package:highlighter_coachmark/highlighter_coachmark.dart';

void main() => runApp(MaterialApp(home: HomePage()));

class HomePage extends StatefulWidget {
  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  GlobalKey _fabKey = GlobalObjectKey("fab"); // used by FAB
  GlobalKey _buttonKey = GlobalObjectKey("button"); // used by RaisedButton

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        key: _fabKey, // setting key
        onPressed: null,
        child: Icon(Icons.add),
      ),
      body: Center(
        child: RaisedButton(
          key: _buttonKey, // setting key
          onPressed: showFAB,
          child: Text("RaisedButton"),
        ),
      ),
    );
  }

  // we trigger this method on RaisedButton click
  void showFAB() {
    CoachMark coachMarkFAB = CoachMark();
    RenderBox target = _fabKey.currentContext.findRenderObject();

    // you can change the shape of the mark
    Rect markRect = target.localToGlobal(Offset.zero) & target.size;
    markRect = Rect.fromCircle(center: markRect.center, radius: markRect.longestSide * 0.6);

    coachMarkFAB.show(
      targetContext: _fabKey.currentContext,
      markRect: markRect,
      children: [
        Center(
          child: Text(
            "This is called\nFloatingActionButton",
            style: const TextStyle(
              fontSize: 24.0,
              fontStyle: FontStyle.italic,
              color: Colors.white,
            ),
          ),
        )
      ],
      duration: null, // we don't want to dismiss this mark automatically so we are passing null
      // when this mark is closed, after 1s we show mark on RaisedButton
      onClose: () => Timer(Duration(seconds: 1), () => showButton()),
    );
  }

  // this is triggered once first mark is dismissed
  void showButton() {
    CoachMark coachMarkTile = CoachMark();
    RenderBox target = _buttonKey.currentContext.findRenderObject();

    Rect markRect = target.localToGlobal(Offset.zero) & target.size;
    markRect = markRect.inflate(5.0);

    coachMarkTile.show(
      targetContext: _fabKey.currentContext,
      markRect: markRect,
      markShape: BoxShape.rectangle,
      children: [
        Positioned(
          top: markRect.bottom + 15.0,
          right: 5.0,
          child: Text(
            "And this is a RaisedButton",
            style: const TextStyle(
              fontSize: 24.0,
              fontStyle: FontStyle.italic,
              color: Colors.white,
            ),
          ),
        )
      ],
      duration: Duration(seconds: 5), // this effect will only last for 5s
    );
  }
}

Output:

enter image description here


Morly answered 15/8, 2019 at 6:43 Comment(6)
Hi @Morly I want to run Highlight Coachmark on the begininning of Home Screen, but get error : [ERROR:flutter/lib/ui/ui_dart_state.cc(148)] Unhandled Exception: NoSuchMethodError: The method 'findRenderObject' was called on null.Phosphocreatine
How to make sure all UI component has rendered before i call showMark()Phosphocreatine
@anunixercoder Sorry for the delay, I just got a chance to use my computer now, so I believe once all the widgets are rendered on the screen, it is safe to call showMark(), you can check if widgets are rendered using SchedulerBindingInstance.addPostFrameCallback method. You can put a bool _rendered = false in the beginning and in callback you can make it _rendered = true.Morly
The package highlighter_coachmark hasn't receieved an update since more than a year and has no null safety. I recommend to use the package tutorial_coachmark. @Morly maybe you can add this to your answer?Wiedmann
@Wiedmann Thank you, I'll update my answer in a day or so.Morly
@Wiedmann Sorry for being late, I just added another answer instead of editing this.Morly
A
12

You can use this library to help you achieve what you need. It allows you to mark views which you want to highlight and how you want to highlight them.

Autobiographical answered 4/7, 2019 at 13:46 Comment(1)
Hi any full example how to run this library ? because i try to run it but found error, please see detail error here: github.com/marica27/highlighter-coachmark/issues/4Phosphocreatine
M
11

Screenshot (Using null-safety):

enter image description here


Since highlighter_coachmark doesn't support null-safety as of this writing, use tutorial_coach_mark which supports null-safety.

Full Code:

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  late final List<TargetFocus> targets;

  final GlobalKey _key1 = GlobalKey();
  final GlobalKey _key2 = GlobalKey();
  final GlobalKey _key3 = GlobalKey();

  @override
  void initState() {
    super.initState();
    targets = [
      TargetFocus(
        identify: 'Target 1',
        keyTarget: _key1,
        contents: [
          TargetContent(
            align: ContentAlign.bottom,
            child: _buildColumn(title: 'First Button', subtitle: 'Hey!!! I am the first button.'),
          ),
        ],
      ),
      TargetFocus(
        identify: 'Target 2',
        keyTarget: _key2,
        contents: [
          TargetContent(
            align: ContentAlign.top,
            child: _buildColumn(title: 'Second Button', subtitle: 'I am the second.'),
          ),
        ],
      ),
      TargetFocus(
        identify: 'Target 3',
        keyTarget: _key3,
        contents: [
          TargetContent(
            align: ContentAlign.left,
            child: _buildColumn(title: 'Third Button', subtitle: '... and I am third.'),
          )
        ],
      ),
    ];
  }

  Column _buildColumn({required String title, required String subtitle}) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Text(
          title,
          style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
        ),
        Padding(
          padding: const EdgeInsets.only(top: 10.0),
          child: Text(subtitle),
        )
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Stack(
          children: [
            Align(
              alignment: Alignment.topLeft,
              child: ElevatedButton(
                key: _key1,
                onPressed: () {},
                child: Text('Button 1'),
              ),
            ),
            Align(
              alignment: Alignment.center,
              child: ElevatedButton(
                key: _key2,
                onPressed: () {
                  TutorialCoachMark(
                    context,
                    targets: targets,
                    colorShadow: Colors.cyanAccent,
                  ).show();
                },
                child: Text('Button 2'),
              ),
            ),
            Align(
              alignment: Alignment.bottomRight,
              child: ElevatedButton(
                key: _key3,
                onPressed: () {},
                child: Text('Button 3'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Thanks to @josxha for the suggestion.

Morly answered 11/5, 2021 at 18:11 Comment(0)
I
10

Wrap your current top widget with a Stack widget, having the first child of the Stack your current widget. Below this widget add a Container with black color, wrapped with Opacity like so:

return Stack(
  children: <Widget>[
    Scaffold( //first child of the stack - the current widget you have
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: <Widget>[
              Text("Foo"),
              Text("Bar"),
            ],
          ),
        )),
    Opacity( //seconds child - Opaque layer
      opacity: 0.7,
      child: Container(
        decoration: BoxDecoration(color: Colors.black),
      ),
    )
  ],
);

you then need to create image assets of the descriptions and arrows, in 1x, 2x, 3x resolutions, and place them in your assets folder in the appropriate structure as described here: https://flutter.dev/docs/development/ui/assets-and-images#declaring-resolution-aware-image-assets

you can then use Image.asset(...) widget to load your images (they will be loaded in the correct resolution), and place these widgets on a different container that will also be a child of the stack, and will be placed below the black container in the children list (the Opacity widget on the example above).

Icecold answered 4/7, 2019 at 20:31 Comment(2)
Hi, any full example of using your method ? because i dont understand how to put images on opaque layer and How to make specific element to highlight... thank youPhosphocreatine
The transparent overlay is not the issue, I guess. But how does he cut out the space where underlying UI-elements are?Pansophy
V
8

It should be mentioned that instead of an opaque approach the Material-oriented feature_discovery package uses animation and integrates into the app object hierarchy itself and therefore requires less custom highlight programming. The turnkey solution also supports multi-step highlights.

Vegetarian answered 5/12, 2019 at 16:27 Comment(0)
P
5

If you don't want to rely on external libraries, you can just do it yourself. It's actually not that hard. Using a stack widget you can put the semi-transparent overlay on top of everything. Now, how do you "cut holes" into that overlay that emphasize underlying UI elements?

Here is an article that covers the exact topic: https://www.flutterclutter.dev/flutter/tutorials/how-to-cut-a-hole-in-an-overlay/2020/510/

I will summarize the possibilities you have:

Use a ClipPath

By using a CustomClipper, given a widget, you can define what's being drawn and what's not. You can then just draw a rectangle or an oval around the relevant underlying UI element:

class InvertedClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    return Path.combine(
      PathOperation.difference,
      Path()..addRect(
          Rect.fromLTWH(0, 0, size.width, size.height)
      ),
      Path()
        ..addOval(Rect.fromCircle(center: Offset(size.width -44, size.height - 44), radius: 40))
        ..close(),
    );
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) => true;
}

Insert it like this in your app:

ClipPath(
  clipper: InvertedClipper(),
    child: Container(
      color: Colors.black54,
    ),
);

Use a CustomPainter

Instead of cutting a hole in an overlay, you can directly draw a shape that is as big as the screen and has the hole already cut out:

class HolePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.black54;

    canvas.drawPath(
      Path.combine(
        PathOperation.difference,
        Path()..addRect(
          Rect.fromLTWH(0, 0, size.width, size.height)
        ),
        Path()
          ..addOval(Rect.fromCircle(center: Offset(size.width -44, size.height - 44), radius: 40))
          ..close(),
      ),
      paint
    );
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}

Insert it like this:

CustomPaint(
  size: MediaQuery.of(context).size,
  painter: HolePainter()
);

Use ColorFiltered

This solution works without paint. It cuts holes where children in the widget trees are inserted by using a specific blendMode:

ColorFiltered(
  colorFilter: ColorFilter.mode(
    Colors.black54,
    BlendMode.srcOut
  ),
  child: Stack(
    children: [
      Container(
        decoration: BoxDecoration(
          color: Colors.transparent,
        ),
        child: Align(
          alignment: Alignment.bottomRight,
          child: Container(
            margin: const EdgeInsets.only(right: 4, bottom: 4),
            height: 80,
            width: 80,
            decoration: BoxDecoration(
              // Color does not matter but must not be transparent
              color: Colors.black,
              borderRadius: BorderRadius.circular(40),
            ),
          ),
        ),
      ),
    ],
  ),
);
Pansophy answered 10/7, 2020 at 9:56 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.