How to build next-gen UIs in Flutter

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments

So we're supposed to compose our UIs from static images and shaders now? ;-)

The effects are cool, for sure, but the fact that all classes he's talking about are private classes to the demo screen, says that there's no much to reuse elsewhere. Also, IMHO, writing the code is the easy part. But how to come up with the initial idea, the graphics and animations would be the part I'd struggle with.

👍︎︎ 1 👤︎︎ u/eibaan 📅︎︎ May 11 2023 🗫︎ replies
Captions
[MUSIC PLAYING] CRAIG LABENZ: Hello, and welcome to Building Next Gen UIs in Flutter. My name is Craig Labenz and I'm a developer relations engineer on the Flutter team. Today we have something special in store for you. Over the past few months, the Flutter team worked with our good friends at gskinner, creators of the Wonderous app, to build a ground breaking demo. If you follow along with this codelab, you're going to see some really neat strategies to build a dazzling UI in Flutter, something you'd see in a futuristic sci-fi video game's main menu. Let's take a look at the finished product. It's called Outpost 57, Into the Unknown, and it features this spacesuit clad character here in the middle staring up defiantly at a mysterious orb pulsing in the night sky. Notice how there's energy fields emanating off the orb as well? The orb grows and shrinks, by the way, as our cursor moves in and out from the center of the screen. And also, very neatly, the walls on the side reflect that light in a realistic way. When the orb shrinks and its light dims, we see that reflected on the walls. Lastly, the title in the top left seems to be being bombarded by some sort of cosmic radiation. So if you have no idea how you'd even approach building something like this, then you're right where I was before we started this project a few months ago. Join me as we work through this codelab and see what the good folks at gskinner cooked up. If this is your first thing in Flutter, you will need to set up your development environment. But luckily, just for the sake of this codelab, you can skip installing the iOS or Android emulators because this codelab targets web and desktop, and neither of those require special emulators. But a note for all you Flutter experts out there that are working through this. You will need to use the latest version of Flutter 3.10, so be sure to pause this video and run Flutter Upgrade if you haven't yet already. OK. We're now ready to get started. The first thing you need to do is download all the code and load it up into your IDE. Now I've already done that, but you'll need to drill into the next-gen-ui folder and load step one. Your editor will probably offer to install all the packages by running flutter pub get behind the scenes. Of course, you want it to do that. Now the next thing the codelab tells us to do is run the starter app, so I am going to do that as well. First I'll close the finished product that we were just looking at, and I'll switch to my code, jump back to the starting branch, and build. Now a note on how I'm going to do this codelab. I'll spend most of my time in the codelab UI that you will also see at home on the website, and I'll only jump over to my editor when it's time to change to a branch and rerender the next milestone that we see in the codelab. OK, here is the starting point. It's not much to write home about, but that's OK because that just means you get to take all the credit for when it looks great at the end. Now there are a few interesting details in the code that you start with, and I want to spend a second to talk about those. The first is the assets folder, and it's got three subdirectories-- fonts, images, and shaders. Especially images and shaders are of interest here. The shaders folder contains those raw fragment shaders that we're going to use later. One is going to apply that cosmic radiation effect to the title and the second will, of course, be the intimidating sky orb in the center. But the images folder is also surprisingly interesting. If we open this up, we'll see a handful of different categories of images in here, starting with these background images. Then if we scroll down, we see some midground images, and above that, foreground. So we'll open these in a second, but later on those are going to come together to compose our scene. Now within these categories, background, midground, and foreground, you'll also see emit and receive images, light emit and light receive, and these are going to create the different effects of the scene and the room that we're in reflecting the light from the orbit and natural way. So I'll focus on the midground and just pull up some of these images. So we can kind of see what we're talking about. Midground base is dark here, but it captures the frame of the room that we're looking at. Now contrast that with midground light receive. This has light surfaces everywhere, and we'll later use a neat color filter trick and a nice blend mode to recolor this image and apply a new shade to the whole scene. And then lastly there's the emit flavor of image, and these draw lines that cut through the orb and walls of this scene that we're in, and these emit images are going to create the effect of some other material in the room. Maybe it's getting charged up by the orb in the sky, but it's going to emit its own light as if it's being animated by the effect. So one last note on them. They're all the same resolution. They're all 2,080 pixels by 1,170 pixels. So when we lay them over top of each other and they all take up the same amount of space, they won't be able to help themselves but to recompose our scene. The last thing I want to point out is that we have a couple special images that will help us draw buttons and borders. So we've got-- these aren't crosshairs, but they always make me think of crosshairs. We've got this left side effect and right side. And then also our Start button had a really unique border as well, and that's actually just an image. This would be very complicated to draw with a plain widget, and I think us Flutter developers often overlook the value in using a widget like this. We see something like this and think, well, how would I do that with a custom clip and a raw painter and whatnot? But you can just use an image. OK. So we've explored most of the code base. The last thing I want to look at before we dive into it is the pub spec file. At the top we simply see all the dependencies that we'll ever need throughout this, so you don't need to run flutter pub get ever again until we get to the end, and at the bottom of the pub spec file our assets are pulled in from the folders we were just looking at. Our fonts, shaders, and images are all ready to go. So now back to the codelab, and we see we've run the app. We saw that starter UI, and we have explored the different milestones in the code base. So we're ready to get started and paint the scene. Now, the first thing you'll need to do here is create a new directory in your lib directory called title_screen, and populate it with a new file called title_screen.dart. And then I recommend you simply grab all of this code in the first snippet here and paste it right into your editor, and let's talk about what this does. It's a new full screen widget, which we can tell because it uses a scaffold. And then inside it quickly delegates to a stack, which ultimately arranges the images that we were just looking at. And remember, these images all have the same size, so our background images appearing first means they will be the most distant. They'll be covered up by the mid and foreground. And then midground appears next, and lastly we have the foreground images at the bottom. So we've created our first image but we can't see it yet. We, of course, have to use it first. So if I scroll down, we see that the codelab directs us back to main.dart for our second change. You've got a few imports that you'll want to update at the top of this file, but then after that, down in the Next Gen UI app, you can rip out everything that was under this home parameter in your material app and replace it simply with the title screen widget. All right. This, the codelab indicates, should get us ready to view the first milestone. So I'm going to switch back to my code and jump to the Git branch that matches this point in the codelab, which for me is step three, paint the scene, milestone one. I'll now rebuild, and when I load my app we see all of the images composing this scene. Now they are grayscale. Of course, what we saw earlier was not grayscale. So the next step we're going to do is going to apply the color to these images and begin to really bring some life to this scene. OK. If we scroll down, we see the next part is called add an image coloring utility. So we're still in title_screen.dart, and at the bottom of this file you'll see a new widget called lit image. And this lit widget takes a couple of interesting parameters. First it takes a color, then it takes an actual image source-- and this needs to be made available by our pubspec.yaml file-- and then it takes this parameter called light amount, and this is the intensity of the color. So remember, our orb is going to grow and shrink, and as it grows, it casts more light on the scene. We'll reflect that through this light amount parameter. So how does this widget work? Well, in its build method it uses a color filtered widget and a blend mode of modulate. And if you're not familiar with blend modes, that's OK. If you are, this modulate blend mode is similar to multiply from Photoshop, but essentially this whole concept involves taking an image as your base and then applying some kind of modification to the whole thing pixel by pixel, and the blend mode is the algorithm by which you compare each corresponding pixel in your base image and your modification layer and ultimately collapse those down into your final pixel color-- your value for every pixel in your resulting image. So the modulate or multiply blend mode, that's ultimately going to darken the image that we're looking at. And this is really interesting because if we go back to those images quickly and I open up, for example, the receive midground image, we see a bunch of white areas here. So those white areas are going to cleanly be able to be color filtered by the modulate blend mode. And then the gray areas around the bottom, they'll create the effect of something that was actually gray in real life and is being pseudo illuminated by green light. It's a really neat effect. All right, back to the codelab. Once you add the lit image at the bottom of the file, like always, first you add the widget and then you have to use it. So back up at the top of title_screen there's a few more imports that we need to juggle, and then we're going to make a few modifications to the title screen widget itself. First, we've got a couple parameters here. These are going to control the strength of the light that is applied in our receive and emit images respectively. They're hardcoded for now, but later they're going to incorporate what the orb is doing, how big it is, how powerful of light it is casting. Then down here we see two new variables in the actual build method or the color and emit color. Now eventually, the color of this whole scene is going to be dictated by the difficulty that the user has chosen. In the bottom left there were three difficulty buttons, casual, normal, and hardcore. Well, we don't have those buttons yet so we can't drive the color according to our business logic yet. So for now, we're just grabbing the first one off the list-- that's green corresponding with the casual color-- and assigning that to the whole scene. So if you copy this code and paste it over your old definition of title_screen, you will be ready to view the next milestone. So if I scroll down, we see lit image here appears again. There's no changes. You just need to modify title_screen. We're ready to see what we've got. So I'll again switch back to my code, jump to the correct branch. Step three, milestone two. Rebuild, and we see the UI now being basked in this strong green light from an orb that doesn't even exist yet-- but it will before too long. OK, back to the codelab. We've reached the end of step three, paint the scene, and we're ready to move on. Step four, add a UI. Now for the purpose of this codelab, the word UI just means the title and all the buttons. Obviously you could think of all of this as the UI, but that's what it means for our purposes. OK. To begin, we need a new file, so you need to create title_screen_ui.dart in that same title screen folder right next to the previous file we made. And to begin, you can copy in all of this code and fill it completely with that. So this adds a new title screen UI widget and then another widget below that. First, what does the title screen UI widget do? Well, it quickly delegates, again, to a stack. This is a really common pattern in this codelab. Sometimes Flutter developers think, maybe is it-- am I using too many stacks? Is this too expensive? I've got stacks in stacks in stacks. I've got stacks of stacks. Is that OK? It absolutely is. It's a really nice way to lay out everything just right, and you don't have to worry about it. So we're using another stack, and ultimately the main first child here is this title text widget. It's decorated by two other widgets. Top left is simply a readability utility to place this in the top left of our stack, and UI scaler is going to grow and shrink the child widget proportionally to the size of the actual window. This will keep, on large desktop displays or high resolutions, our UIs from looking kind of distant and stretched out from each other with really small chunks of UI. They'll all grow and shrink accordingly. OK, but TitleText. That's the character here. So if we scroll down, we see TitleText, and this build method isn't too complicated. It's basically a column with two children. This row draws Outpost 57, and the second child is simply a text widget that draws Into the Unknown. So once we have added this new file and populated it with title_screen_ui, we'll be ready to actually use it. So continuing on, we'll return to title_screen.dart. And if we scroll down in here, you can add the import to the top, and ultimately, in the build method at the very bottom, you'll fill the entire stack with the title screen UI widget. And of course, this makes sense because putting it last means it will be in front of everything else in the stack, and we wouldn't want any of our background things to sit-in front of our menu buttons or title. OK, so once we've done that, yes, I believe we will be ready to view another milestone. So I'll switch back to my code and jump to the next branch, which is step four, milestone one. And when I rebuild here, we'll see the title appear. And just as a quick note on that UI scaler widget, when I shrink everything like this, we see UI scaler is what's responsible for keeping our proportions right. OK, back to the codelab. Scrolling down, we're now ready to add the difficulty buttons. So we're staying in title_screen_ui, and the first thing we want to do is import this focusable control builder package. That's already been included from the pubspec.yaml file so you don't have to do anything there, but focusable control builder is a really nice utility package. It's written and distributed by the same folks that wrote all of this, gskinner. And what it does is it allows you to create fully featured, custom form controls from a blank, unopinionated starting point. So what do I mean by fully featured? Well, it's a button, for example, that will respond to focus events, so a desktop user could press Tab and move the focus down to your button. It'll also register itself with the semantic system of Flutter, so a screen reader can find and correctly read your button. But it doesn't have any opinions about how your button should look, which is great for you if you want a radically custom looking button like we have in this UI. So we're adding the focusable control builder package and scrolling down to look at some changes we're going to make to title_screen_ui. First, we have a few new parameters that we're going to add to the constructor. Remember, we're adding buttons here so this implies storing some state. Which button was last clicked? What did it do? What's the handler for the next click event? So the title screen UI widget is now going to need to accept some parameters to deal with all of this stuff. Then down in the build method there's a few keywords that you'll need to juggle around and reposition, but ultimately we're adding this new difficulty buttons widget in the bottom left of our screen, again wrapped with the UI scaler and bottom left positioning widget. OK. Difficulty buttons doesn't exist yet, so let's keep going. The next step is the codelab stays in title_screen_ui and we're adding those difficulty buttons widgets. This block adds both the difficulty buttons, plural, and below, difficulty button. So you can just grab it all in one go and paste it into the bottom of your title_screen_ui.dart file. So what does this do? Well, in the build method it ultimately delegates to a column and draws those three buttons. And if you've ever made your own custom buttons in Flutter before, you've probably seen this kind of API where, in a different widget of your creation, you hide all of your opinionated decisions and you expose just a minimal API for yourself to recreate those buttons later. So we see that that is what's going on here. All we're passing in is the label, what text should the button show, is this button selected, and then what happens when the user interacts with the button. Very minimal. And below, in the difficulty button widget itself, we'll see how it all comes together. So in difficulty button, first of all, we notice that it begins with the focusable control builder widget. And this uses a builder pattern, as the name suggests, and it passes in a state variable, which has information like, is this hovered? Is this focused? And then it's up to you to decide, what does that mean for your widget? In the build method or in the method that the builder runs, we see, again, it quickly delegates to a stack. Now in here, I want to point out how much this button does make use of that state object. Look, here we check to see, is it hovered or focused? We also check, is the button selected? So this button is very iteratively built up and very dependently built up based on the nature of its state. So those are the difficulty buttons, and if you made it to the end of this snippet you shouldn't see any linter errors in your title_screen_ui.dart file, but you will see some errors in your title_screen.dart file. So switch back to that and we'll get that in order. Now in title_screen.dart, remember, we just were adding these handlers in a difficulty variable to that nested widget title screen UI. So some thing's got to store, for example, which difficulty level is, in fact, currently selected. Well, the place to do this is to convert title screen to a stateful widget, just like the codelab says. So we want to convert it from stateless to stateful, and your IDE can help you with that if you click on where it says stateless widget. Once you've converted it, you'll be ready to make the rest of the changes. To start in title screen state, you'll want to add all of this new code, and let's talk about what it does. First of all, there's new variables for emit color and orb color. Remember, earlier I said that we were just going to hard code those to grab the color associated with the casual difficulty button or the casual difficulty, but now we're adding the buttons so we can actually have the color of our entire scene accurately be painted by what difficulty the user is choosing? Next we've got some variables to store the difficulty, and then some handlers to wire up the behaviors that change when the user clicks on buttons or hovers over buttons. Scrolling down, it also can be good to paste the entire build method again because the orb color variable now has an underscore, whereas before it didn't because the orb and emit color variables have been moved up here and they're now private computed variables. So if you copy that in as well, you get down to the bottom. If you copy the whole build method you'll get this. If you make the edits by hand then be sure at the bottom, for title screen UI you will need to add in the difficulty, the three difficulty parameters-- the value itself, the pressed, and the focused handler. So there's no changes in lit image again, and this means we're ready to re-evaluate our app. So again, I'm switching back to my code and I'm jumping to the next branch for me, which is right here. Step four, milestone two. And when I rebuild, we see our difficulty buttons appear in the bottom left, and they're operable. So as I hover over different difficulty buttons, we'll see the color of the entire scene redraw. Now the change here is instantaneous. It's really not that pleasant, but in the next step we're going to add a lot of nice animations that'll breathe some life and polish into this. OK. Back to the codelab. Moving on we get to the next step, add the Start button. So we're staying in title_screen_ui, and if we scroll down into the bottom of its build method we see the first change that we need to make, which is to add another child to the stack in this build method. And the main character here is this Start button widget. Later it's going to have a valuable onPressed value that will actually do something, but for now it's just an empty lambda. But you can copy the whole section into the bottom of your stack. Then it's time to add the Start button itself. So when you scroll down, we'll see the definition for the Start button. Grab the whole thing, just paste it right in the bottom of title_screen_ui.dart. So what does the Start button do? Well, it's reusing focusable control builder. We saw that earlier. And then, just like a lot of the things in this codelab, it delegates to a stack to get everything positioned just right. It does include the images for that angled border that we saw, and then it ultimately results in a-- makes its way to a text widget that draws the Start Mission text. So let's now return to our app and see that running. I'll jump to step forward milestone three, rebuild, and there it is. And it's got a hover effect that is subtle, and it's going to get a lot cooler in the next step when we add animations. Back to the codelab. Oh, we've actually completed step four, add a UI, and it's time for those animations right now. All right, add animations. Now in this step, like I said, a lot of polish is going to come into our UI. But I'm equally excited about how we're going to add the animations as I am about the animations themselves. And the how we're going to add these animations is by using a package called a Flutter Animate. If you watched the announcement video for this at Flutter Forward back in January, then you probably already know some of why I'm so excited. But if not, I'm really looking forward to sharing this with you. The first thing that we want to do is add a little developer helper utility from this package in our main.dart file. So open up main.dart and import Flutter Animate. Then scroll down, and that will allow us to add this one line right here, Animate.restartOnHotReload equals true. Now if you've written animations in Flutter before, you may have noticed that they don't tend to rerun when your app hot reloads. That's kind of frustrating, but it does make sense, because a hot reload is different from a hot restart in that the hot reload does not re-execute your initialization code. If a hot reload did rerun your initialization code, then it would clobber all of your state and it would basically be a hot restart. So, this is relevant for animations because we tend to kick off our animations in our initialization code, often in an init state method of a stateful widget. So what this line does is it tells Flutter Animate, which is already keeping track of all the animations that you declare using the package, to watch for hot reload events and restart all of those animations every time it sees one. It's a really nice developer productivity win. All right. Once you've got that wired up, you do have to restart your app because just like I was saying, initialization code doesn't rerun on hot reload. So to get that hot reload trick working, we have to hot restart our app. OK. But now we're ready to really get into it. Back down in title screen UI, of course, begin by importing Flutter Animate, and then we can get going. The first widget that we're going to animate is the title text widget, and I want you to look in its build method and find this row, and then notice all the new code at the end of this line, .animate and .fadeIn. So you can copy these in, and then do the same for the text widget below it, .animate and .fadeIn. Now let's talk about what this is doing. You may never have seen this .animate method before, especially because it's new and it's provided by the Flutter Animate package. Well, .animate is an extension method on all widgets, and it simply wraps that widget that you originally called it on. So in this case, it's going to wrap this row with a new widget called animate, and that animate widget sets up a lot of the infrastructure to get us going and ready to call the other very helpful methods that this package offers. So once we call .animate, we're ready to call something like fadeIn, and we simply tell it, wait 800 milliseconds before you start this animation and complete the animation another 700 milliseconds later. Of course, summing to one and a half seconds. And then we do the same thing just below it. Into the Unknown, you fade in one second after you would have otherwise rendered and complete another 700 milliseconds later. And merely by incrementing the delay between these two widgets, we can begin to orchestrate chained animations, and that is really, really neat. And think about how tricky all of this would be to write if we were using our own custom animation controllers. It would really be a challenge to wire this up. But with just that tiny declaration, we are ready to see it in action. So as always, I'll switch back to my editor, pop to the correct branch, and rerun. And when I return to my app, we see Outpost 57, Into the Unknown, animate in very elegantly off an absolutely minimal declaration from us. It's a really exciting new way to write animations in Flutter. OK. Scrolling down, we're going to keep this train going. We're going to add this kind of effect to all the other buttons in the screen. So next up is the difficulty buttons, and we're going to see some similar code here. First find the casual button, and then add these three lines at the bottom. Animate it in, wait 1.3 seconds. We're continuing the progression. Before we had 0.8, 1.0, and now 1.3. And then a new player has entered the party as well. We have a new .slide method. Not only will this casual button fade into place, but it will fade and slide into place, which I think is an effect that you're really going to like in a second. Now add the same thing to the normal button. A couple lines down here and then scroll down to the hardcore button, and we'll find a few more methods. OK. With that, we're ready to see some new effects. Have you ever written animations this quickly before? It has never been so easy. All right, and I will refresh. And when I returned to my app, we now see this whole left side come in this staggered marching band-like order one after another. Really, really cool. OK, next we're going to do the same thing with the Start button. So back to the codelab. I'll scroll down, and we see fade in the Start button is next. So you can find its definition, again, at the bottom of your title_screen_ui.dart file, and what we're really changing here is just these lines at the bottom. We're going to add two animation lines right where your stack closes, and then three more right where the sized box closes. And of course, those are just coming from the top of the build method, stack and size box there respectively. But there's another very interesting new method here, .shimmer, and I'm excited to show you how cool this one looks. So I'm hopping back over to my editor, moving to step five, milestone three, rebuilding. And first of all, the whole UI now fades in, and you can see there's Start button sliding up just like the difficulty buttons. But watch the shimmer effect. As we mouse over, it's just a really, really cool hover effect, in my opinion. Very, very nice. And the next thing we're going to do is add other hover effects to the difficulty buttons. So back to the codelab. We scroll down and we're going to animate the difficulty hover effect. We're staying in title_screen_ui. That's where all of our buttons' code is. And in the difficulty button class, I want you to scroll down and find, in the stack, the container that is the first child. Before you've made the change that we're talking about here, that container is going to be the first thing you see. And I want you to copy the entire animated opacity widget and paste it right over the top of the container. It'll recontain the container, but this is how you get this update. Once you've done that, we're actually ready to watch this again. Moving to step five, milestone four, rebuilding, and let's see how this works. First of all, our buttons look totally different. Our difficulty buttons used to all have a border. Now only the selected button has a border. But the reason for that is that they were all showing their hover border all the time, so now this hover border fades in and out as we hover over a specific button and it reveals the active state that was there all along of, again, the not crosshairs that always make me think of crosshairs, that kind of bracket, whichever difficulty button is, in fact, colored. OK. So we've added the hover effect to our difficulty buttons and we're ready to do the last thing in the animation step, which is animating the color change as we actually change our difficulty. So we've made a lot of changes in title_screen_ui. Now we're going to pop back to title_screen.dart. And just like we were doing elsewhere, first we want to import the Flutter Animate package. Now scrolling down, there's a new widget here, AnimatedColors. You can grab its whole definition and paste it right in the bottom of title_screen.dart. So animated colors. What does it do? Well, it takes a couple of parameters. First a builder, and then also two colors-- orb color and emit color. These are the parameters that we've seen floating around this code base. And it's kind of an implicitly animated widget, so it's going to keep track of the old values of orb color and emit color, and whenever it gets new values it's going to linearly interpolate and call that builder-- that's 60-ish frames a second depending on your screen-- until it arrives at the updated values. So down here in the builder we see it uses-- oh, sorry, in the build method we see it uses two tween animation builders. It decides that it's going to take half a second to do this whole thing, and it's down here at the very bottom where the magic really happens. This is the final method that the widget calls where it eventually gets around to running the builder that we pass in. And here, all of the intermediate values for orb color and emit color are visited until the animation is complete. All right. So we've added the animated colors widget. Step two always, use the new widget. So in title_screen is where we make the next few changes. To begin, you want to copy everything in the animated colors block here the way down to where it ends. You can, of course, copy just the whole definition for the class if you'd like. But what's actually changing is you're going to copy the animated colors widget and paste it on top of the stack. Before you make any change here, you'll see the scaffold give way to a center widget, which then renders a stack, and you're going to wrap that stack in the animated colors widget. Scrolling down, this is the last milestone in step five. We'll be ready to see our scene animate from one color scheme to the next. So I've switched branches, rebuilt. First of all, our scene just looks the same, of course, but now when I hover over a button, it gradually shifts to the next color scheme, which is a much more natural looking effect. Remember, the orb is going to be in the middle and it's going to visually be the source of all this light, so it wouldn't make sense for it to instantaneously switch colors. All right. We've wrapped up the animation step and we're ready to move on. So we've done a lot so far. Step six is where we finally get to add those juicy fragment shaders. To begin, we're going to return to main art and make a couple modifications. First, import the provider package. Again, that's been lurking about in the pubspec.yaml this whole time and you don't need to reimport anything. Then scroll down into the main method itself and replace the entire old definition of runApp with this new way to call runApp. And we're wrapping that Next Gen app widget with a provider, which is going to load these shaders. Also, don't forget to import assets.dart as well. That's where the load shaders method comes from. Now, our whole widget tree will be able to reach up and grab those shaders whenever they need. OK. The codelab also reminds us we've made an edit to the main method in main.dart, so no hot reload will be sufficient here. That initialization code is not re-executed. So after you hot restart, your app will be ready to continue. Now we're back in title_screen_ui for the first shader. We're going to begin with that cosmic radiation glitching effect on the text in the upper left. So there's a handful of imports to add. Just copy them all, paste them right in, and then we're ready to make some changes in title_screen_ui. So the TitleText widget is going to be the first part that receives some shader action. And if we scroll down to its build method, what you'll see before you make any edits is that you're simply returning this column widget. But I don't want you to do that. I want you to instead capture that column widget in another variable and call it Content. Once you do that, you'll be ready to scroll down and add this entire long return statement. So what does this long block of code do? Well, it begins with a consumer widget that reaches up the tree and finds the provider that we just added to main.dart. Then once all the shaders are actually in place, it gets things going by using a ticking builder. Ticking builder is an interesting widget you may not have ever seen before, but its idea is to tell the Flutter framework, I want to draw a new frame every chance we get. Every time the operating system says you can draw a frame if you want, let's do it. Now normally this is a terrible idea for most of your apps. It would just waste battery pointlessly. But once your app has ambient motion, then there's no way around the fact that you need to draw new frames at some interval. So the ticking builder is going to do that and it exposes to its children how much time has passed. Then we get to this animated sampler widget, and I'm not going to lie. The animated sampler widget is a bit of a beast. Its code is all included in this code base, but you really don't need to read or worry about it too much. The framework one day is probably going to offer something like this so this is just a first draft of how this API might look. But let's take a peek at what this does. First, it takes another widget as its child, and what this animated sampler widget is going to do is apply a shader to what some widget would have otherwise rendered. Pretty cool. Keep that in mind. So it takes two parameters. The first is that child widget that we just talked about, and the other is a function that itself receives three parameters, and I'm going to talk about these in reverse order. First is the canvas. Now the canvas is simply where you draw all the stuff. You might have seen this before if you've used custom painters or things like that. The last line here we see we actually use this canvas to draw something to the screen. Now above that-- well also, there's the size parameter. This is just how much space we have. And then lastly, we get this image parameter first. It's not like a JPEG. It's not that kind of an image. It's a rendered version of the widget that we passed in to the child parameter. So for us, that's the actual text. The shape of the words, Outpost 57, Into the Unknown. That's coming in in this handle called Image. So this function sets up some parameters. It passes in the relevant data to the actual UI shader. This UI shader is the one for the title. And notice the last parameter here. We're telling it, hey, this is your starting point. This is what you're going to run your shader logic on, however this other widget would have rendered itself but for you. So then we finally do-- we do this other setup and finally call a canvas.drawRect, and that is what will apply the shader. OK. That was pretty cool and we're ready to see it in action. So I'll return to my code, switch to step six, milestone one, and rebuild. And when I do that, once the app comes back into place, we see the cosmic radiation is once again bombarding our title, and it's almost melting under the influence. A really, really cool effect. Any time you see on Twitter where some otherwise normal Flutter UI is being really intensely manipulated, it's this trick that they're doing. Like the counter app that entirely ripples like the surface of water after a stone was dropped in or something is using something like the animated sampler and an appropriate fragment shader. OK. So continuing on, we finally arrive at the main character of this whole saga, the space orb. So we're still in title_screen_ui for now, and there's just one change we're going to make. Add this onStartPressed handler to the title_screen_ui's constructor, add an attribute to store that, and then scrolling down, update what we actually pass into the Start button. The Start button, in a moment, is going to be able to slightly influence the state of the orb. Once that's in place, you can return to title_screen where we're going to make some dramatic changes. First add all of these imports at the top, and then we're ready to get into the good stuff. Now the codelab says here we're modifying title_screen state, and it reads almost every part of the class is modified in some way, and that is true. So I recommend copying this entire class definition, completely pasting over top of what you've got currently, and then we'll talk through what it does. So the interesting parts in here are-- well, first of all, finalReceiveLightAmt and finalEmitLightAmt have become the computed properties I promised you they would one day be, and they take into account the orb's energy. It's going to be growing and shrinking under multiple different influences momentarily, and we want the scene, the walls and the rocks around the orb, to be reflecting that light in a realistic way. So orb energy is now involved in the calculations for how those images should reflect the light. Scrolling down, we see an animation controller, pulseEffect. This is going to create a little heartbeat effect in the orb on its own, so it'll just kind of beat a little bit and seem alive even if the user has walked away from the screen and is not touching anything. That heartbeat, by the way, has a slightly random interval to it as driven by this getRndPulseDuration method. Here we see some code that whenever the animation completes it gets a new random duration and goes backwards again, and then when it finishes going backwards it gets a new random duration and goes forwards again. So a pretty cool little effect here. The next interesting method is bumpMinEnergy. This is going to be called when the user taps any of the buttons on the screen, the difficulty or the Start button. And this is going to breathe, again, a little bit of life into the orb in the center. All right. Here we see handleMouseMove. This updates a mouse position variable. Remember, the orb is going to be growing and shrinking based on the location of the cursor. So handleMouseMove is passed to a new MouseRegion widget which allows that to actually work. Now, not a ton has changed in the build method itself, but of course, there is this new entry, the actual orb itself, and it's realized by this OrbShaderWidget, which wraps the widget-- or wraps the shader itself from the provider that we added to main.dart, and we pass in the variables about how we think it should look. And then there's an onUpdate method where it tells us every time it's changed something on its own. So those are the changes to title_screen state. But if you copy that in, you'll see a bunch of errors where your editor is telling you that LitImage, something is off there. Well, it's received this new parameter, pulseEffect. So if we scroll down, the next step is to update the definition of LitImage, and pulseEffect is that kind of heartbeat animation controller earlier that we were talking about. LitImage is now going to watch that and rerender itself whenever that animation controller ticks. OK. We are ready to see the orb once again. It's been off stage for a minute, but no more. I'll jump to step six, milestone two, and rebuild. And returning to our UI is the space anomaly itself, and as our cursor moves out and in, the orb grows and shrinks. And as we do that, watch how those side walls get brighter and dimmer. That's the light amount variables that we've been working with this whole time. And again, the whole scene changes as we change the difficulty level. And then we wired through that little handler to Start button, so watch how as we tap the Start button we get this pulsing effect on the space orb. Pretty neat. OK. That's the end of step six, add fragment shaders. We're on to the final active step where we add a particle field. So in here we need one new file, particle_overlay.dart. And I recommend simply grabbing all of this code and pasting it in to that file, and we'll talk about how it works. So particle_overlay, the widget that we're creating, uses particle_field, another library written and distributed by-- you guessed it-- gskinner. So particle_overlay, the widget, immediately delegates to a particle_field widget from that package. And we supply particle_field with an image that it should use and a blend mode for how to recolor that image, and then an all important onTick method. An onTick, as the comment says, runs every tick of the app, and the contract for this method is that it passes us a particle controller. That particle controller has a .particles attribute, and it's our job in this method to update that list. That's all we have to do. So to begin, we see that this method, every tick it adds one new particle to the particle field, so one more wave will be wafting off the orb. After it adds that particle field-- oh, by the way, there's a lot of intimidating looking trigonometry code in here. This positions all of the individual particles in a circular shape around the orb itself. After they're positioned we see that it loops over the particles and checks for any that are too old and it kicks them out of the screen if it finds them. Any that aren't too old get updated by moving a little further away, growing slightly, and having their lifespan decremented so they will one day time out. Now we've created this widget. Step two is always to use it. So back in title_screen.dart, import particle overlay and scroll down into the build method of title_screen state. And you'll find basically in the middle of the build method, just above titleFgBase, a new entry appears, and it is allowed to take up as much space as it wants, as indicated by position.filled. But in the middle, we see our new particle_overlay widget. All right. That's the last change in this step, so if I return to my code and jump to step seven, milestone one and only, rebuild, we will see the final product once again. As we move into the center, we see the energy field, the particles that we've just added, grow in size and intensity, and they dim as well. They reduce their intensity as the orb dims, so they're tied into the strength that the orb is emitting. So folks, if you made it this far, if you followed along, congratulations. We've covered a lot of stuff in this codelab. We brought together a lot of different effects, ranging from these highly detailed custom shaders to a particle field around that custom shader to animations moving things ever so slightly around our app, and in the end, I think we built a screen that pushes the boundaries of what most people think of when they think of Flutter UIs. I also hope that you saw that when we say with Flutter you can control every pixel on your screen, we really mean it. Thanks for watching everyone. Enjoy the rest of Google I/O. [MUSIC PLAYING]
Info
Channel: Flutter
Views: 36,957
Rating: undefined out of 5
Keywords: Google I/O, Google IO, IO, I/O, IO 23, I/O 23, Google I/O 23, Google IO 23, Google I/O 2023, Google IO 2023, IO 2023, Google New, Google Announcement, Google Developers, Developer, Development
Id: HQT8ABlgsq0
Channel Id: undefined
Length: 46min 59sec (2819 seconds)
Published: Wed May 10 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.