1 00:00:00,000 --> 00:00:00,260 2 00:00:00,260 --> 00:00:02,093 TOMMY MACWILLIAM: In this video, we're going 3 00:00:02,093 --> 00:00:05,970 to be writing an Android app that applies filters to images. 4 00:00:05,970 --> 00:00:08,640 Much like Instagram, we're going to write our own version, 5 00:00:08,640 --> 00:00:11,920 using a few libraries that we'll download from the Internet. 6 00:00:11,920 --> 00:00:13,680 So let's get started. 7 00:00:13,680 --> 00:00:18,210 Just like before, let's start a new Android Studio project. 8 00:00:18,210 --> 00:00:20,880 We're just going to select an empty activity, because we're 9 00:00:20,880 --> 00:00:23,762 going to write this ourselves. 10 00:00:23,762 --> 00:00:25,720 You can name the application whatever you like. 11 00:00:25,720 --> 00:00:28,840 I'm going to go with 50 gram. 12 00:00:28,840 --> 00:00:33,130 Again, our package name, just edu.harvard.cs50. 13 00:00:33,130 --> 00:00:35,320 That's fine wherever you'd like to save it. 14 00:00:35,320 --> 00:00:41,170 Make sure we've got Java, at least 5.0, and then using AndroidX Artifacts. 15 00:00:41,170 --> 00:00:43,210 So we'll click Finish. 16 00:00:43,210 --> 00:00:45,940 OK, and here is our empty project. 17 00:00:45,940 --> 00:00:49,820 Everything is automatically generated, just as it was before. 18 00:00:49,820 --> 00:00:53,780 Let's just give this a run to make sure it looks like what we expect. 19 00:00:53,780 --> 00:00:55,165 OK, we've installed successfully. 20 00:00:55,165 --> 00:00:59,920 And if we open up the emulator, we've got that blank activity again. 21 00:00:59,920 --> 00:01:04,910 So let's start by writing out the views for this application. 22 00:01:04,910 --> 00:01:06,770 So we'll have a pretty simple UI. 23 00:01:06,770 --> 00:01:10,390 Let's just display a slot for the image that you're looking at. 24 00:01:10,390 --> 00:01:12,910 And then we'll just have a few different buttons. 25 00:01:12,910 --> 00:01:15,880 One button is one to select an image. 26 00:01:15,880 --> 00:01:18,370 And for that, we're going to look into the images 27 00:01:18,370 --> 00:01:20,822 that the user has saved on their phone. 28 00:01:20,822 --> 00:01:22,780 And then we'll just have a couple other buttons 29 00:01:22,780 --> 00:01:27,600 that let you apply different image effects to those images. 30 00:01:27,600 --> 00:01:29,760 So to start writing our UI, remember we're 31 00:01:29,760 --> 00:01:33,210 going to go into this Resources folder, into Layout, 32 00:01:33,210 --> 00:01:36,820 and open our Layout XML File. 33 00:01:36,820 --> 00:01:40,590 So just like we did last time, let's get this text view out of the way. 34 00:01:40,590 --> 00:01:43,450 We don't need that. 35 00:01:43,450 --> 00:01:46,940 Now by default, this is using a Constraint Layout. 36 00:01:46,940 --> 00:01:50,090 So let's look at a different container that we might use, 37 00:01:50,090 --> 00:01:52,490 called a Scroll View. 38 00:01:52,490 --> 00:01:56,630 As it sounds, a Scroll View is going to allow you to scroll contents 39 00:01:56,630 --> 00:01:59,480 that might be too large for the screen. 40 00:01:59,480 --> 00:02:02,870 In fact, that Recycler View that we looked at before 41 00:02:02,870 --> 00:02:06,140 is very similar, because it's allowing you to scroll elements 42 00:02:06,140 --> 00:02:11,009 where the actual list is much larger than the size of the screen. 43 00:02:11,009 --> 00:02:13,850 So let's change this Constraint Layout, and let's instead 44 00:02:13,850 --> 00:02:16,040 make this a Scroll View. 45 00:02:16,040 --> 00:02:18,533 And notice, that we're auto completing automatically. 46 00:02:18,533 --> 00:02:20,450 And everything else is going to stay the same. 47 00:02:20,450 --> 00:02:24,870 Let's just Match Parent, as we did before. 48 00:02:24,870 --> 00:02:28,520 Now inside of the Scroll View, let's again use that Linear Layout. 49 00:02:28,520 --> 00:02:31,640 Since we're just going to be stacking things on top of each other, 50 00:02:31,640 --> 00:02:35,870 we'll have the Image View first and then a series of buttons below it. 51 00:02:35,870 --> 00:02:39,110 Let's add this Linear Layout. 52 00:02:39,110 --> 00:02:42,920 We want the width to be match parent, because that's 53 00:02:42,920 --> 00:02:46,100 going to fill the full width of the screen, 54 00:02:46,100 --> 00:02:48,860 and we want the height to be Wrap Content. 55 00:02:48,860 --> 00:02:51,860 We want the height of this Linear Layout to be 56 00:02:51,860 --> 00:02:56,820 equal to the sum of all the heights of everything inside of it. 57 00:02:56,820 --> 00:02:58,940 So let's create that layout. 58 00:02:58,940 --> 00:03:01,790 And now, let's add a couple things to it to start. 59 00:03:01,790 --> 00:03:06,820 The first thing we'll add is an Image View. 60 00:03:06,820 --> 00:03:11,720 As its name suggests, you can use an Image View to display an image. 61 00:03:11,720 --> 00:03:17,080 So for this, let's specify wrap content for both the width and the height. 62 00:03:17,080 --> 00:03:20,500 And what this will do is make sure that the Image View is just 63 00:03:20,500 --> 00:03:24,790 the size of the image that you loaded, that loads into it. 64 00:03:24,790 --> 00:03:27,430 And let's also give this an ID, because we know that we're 65 00:03:27,430 --> 00:03:29,200 going to want to access it later. 66 00:03:29,200 --> 00:03:32,827 And let's just call it Image View. 67 00:03:32,827 --> 00:03:34,160 That's everything we need there. 68 00:03:34,160 --> 00:03:37,260 So we can have slash and that tag. 69 00:03:37,260 --> 00:03:39,670 And now below it, let's add a button. 70 00:03:39,670 --> 00:03:43,130 In Android, that's just button. 71 00:03:43,130 --> 00:03:45,650 The width and height, let's just Wrap Content again, 72 00:03:45,650 --> 00:03:48,620 so the button is just big enough for the text. 73 00:03:48,620 --> 00:03:51,660 Then from there, let's set the text of our button. 74 00:03:51,660 --> 00:03:55,770 That's the same property we looked at before, just Android text. 75 00:03:55,770 --> 00:04:00,410 Let's say this says Choose Photo. 76 00:04:00,410 --> 00:04:02,680 OK, so let's try running this and see what we get. 77 00:04:02,680 --> 00:04:05,450 78 00:04:05,450 --> 00:04:06,680 We installed successfully. 79 00:04:06,680 --> 00:04:08,960 So let's pull up the emulator. 80 00:04:08,960 --> 00:04:14,000 And right now, there's no image loaded, so that makes sense. 81 00:04:14,000 --> 00:04:18,100 One last thing to do before we forget, is to set that orientation. 82 00:04:18,100 --> 00:04:19,570 So by default, it's horizontal. 83 00:04:19,570 --> 00:04:22,720 And so we just want to change that to vertical, so that all of the elements 84 00:04:22,720 --> 00:04:24,920 stack on top of each other. 85 00:04:24,920 --> 00:04:29,410 So now that our views are setup, let's tackle this problem in two halves. 86 00:04:29,410 --> 00:04:32,620 First, let's figure out how we can let the user 87 00:04:32,620 --> 00:04:37,870 select a photo from their phone and display that photo in the Image View. 88 00:04:37,870 --> 00:04:40,300 Then, after that's working, let's figure out 89 00:04:40,300 --> 00:04:43,540 how we can apply those filters to the image. 90 00:04:43,540 --> 00:04:45,740 So everything is all set in the View. 91 00:04:45,740 --> 00:04:48,670 So let's come back to our activity. 92 00:04:48,670 --> 00:04:52,050 Just like we did before, let's get a reference to that Image View, 93 00:04:52,050 --> 00:04:53,710 so we can save that. 94 00:04:53,710 --> 00:04:56,640 So we'll say Image View. 95 00:04:56,640 --> 00:05:03,900 And inside of On Create, we're going to say Image View is Find View by ID. 96 00:05:03,900 --> 00:05:08,260 And we gave that an ID of Image View. 97 00:05:08,260 --> 00:05:11,980 So next, let's write a method to enable the user 98 00:05:11,980 --> 00:05:14,590 to select a file from their phone. 99 00:05:14,590 --> 00:05:18,550 So let's just call this Public Void, not returning anything. 100 00:05:18,550 --> 00:05:22,120 101 00:05:22,120 --> 00:05:24,870 And we'll call it Choose Photo. 102 00:05:24,870 --> 00:05:29,580 So the way that you can pull up a file selector for the files on a user's 103 00:05:29,580 --> 00:05:32,580 phone is also with an intent. 104 00:05:32,580 --> 00:05:38,430 But this time, rather than specifying a class in our own application to open, 105 00:05:38,430 --> 00:05:41,580 we're going to specify a system wide intent, 106 00:05:41,580 --> 00:05:45,660 so we can actually open some other app on the user's phone. 107 00:05:45,660 --> 00:05:48,720 This is really nice, because it means we don't have to manually write 108 00:05:48,720 --> 00:05:49,980 all of this ourselves. 109 00:05:49,980 --> 00:05:53,820 It'd be a real pain if we had to write a whole image gallery from scratch right 110 00:05:53,820 --> 00:05:54,570 now. 111 00:05:54,570 --> 00:05:57,060 Instead, we can just use the image gallery 112 00:05:57,060 --> 00:05:59,370 that's already built into Android. 113 00:05:59,370 --> 00:06:01,410 So let's take a look. 114 00:06:01,410 --> 00:06:05,440 We're going to create an intent, just like we did before. 115 00:06:05,440 --> 00:06:07,920 But this time, in the Constructor, we're going 116 00:06:07,920 --> 00:06:13,770 to specify intent.action Open Document. 117 00:06:13,770 --> 00:06:16,860 And you can see here, scrolling through the autocomplete, all 118 00:06:16,860 --> 00:06:20,400 of the different things that you might want to specify. 119 00:06:20,400 --> 00:06:23,340 But Open Document, as the name suggests, is 120 00:06:23,340 --> 00:06:29,550 going to open up this gallery view, where you can select a file. 121 00:06:29,550 --> 00:06:35,170 Next, we want to specify to this intent what type of file we're looking for. 122 00:06:35,170 --> 00:06:40,830 So we're going to say intent.set type, And this takes a string. 123 00:06:40,830 --> 00:06:44,200 And we're looking for any type of image. 124 00:06:44,200 --> 00:06:48,480 So the way to specify that is by saying image/*. 125 00:06:48,480 --> 00:06:52,740 So this says, as long as a file type is an image, it could be a JPEG, or a PNG, 126 00:06:52,740 --> 00:06:53,880 or a GIF. 127 00:06:53,880 --> 00:06:58,790 No matter what it is, we want to open it so long as it's an image. 128 00:06:58,790 --> 00:07:02,580 So lastly, we just have to start up that activity. 129 00:07:02,580 --> 00:07:08,400 So just like we did before, we can say Start Activity for Result. 130 00:07:08,400 --> 00:07:11,980 We want to give it some intent. 131 00:07:11,980 --> 00:07:15,420 Then we're also going to specify as the second argument what's 132 00:07:15,420 --> 00:07:17,690 called a Request Code. 133 00:07:17,690 --> 00:07:19,770 And we'll just say 1. 134 00:07:19,770 --> 00:07:24,940 And this is basically a way to identify where the request came from. 135 00:07:24,940 --> 00:07:28,860 So what's going to happen, remember, is as soon as this line executes, 136 00:07:28,860 --> 00:07:32,580 we're going to get bounced out into some other activity. 137 00:07:32,580 --> 00:07:35,610 And eventually, that activity is going to finish. 138 00:07:35,610 --> 00:07:39,090 And we're going to come back to this activity. 139 00:07:39,090 --> 00:07:42,210 And it could be the case that this activity starts off 140 00:07:42,210 --> 00:07:47,220 a few different intents, maybe one to a file selector, another to a camera, 141 00:07:47,220 --> 00:07:49,410 maybe another to a web browser. 142 00:07:49,410 --> 00:07:52,330 And when we come back to our application, 143 00:07:52,330 --> 00:07:56,220 we want to know which intent we just came back from. 144 00:07:56,220 --> 00:07:58,320 So in this case, we'll only have one. 145 00:07:58,320 --> 00:08:02,160 But this Request Code is basically a unique ID that 146 00:08:02,160 --> 00:08:04,890 tells us where we're coming back from. 147 00:08:04,890 --> 00:08:07,360 So when we come back from this intent, we're going to say, 148 00:08:07,360 --> 00:08:10,710 let's make sure the Request Code is 1, because that's 149 00:08:10,710 --> 00:08:15,150 the code we specified for this getting a file intent. 150 00:08:15,150 --> 00:08:19,920 OK, so the last thing to do here is to hook this method up to something. 151 00:08:19,920 --> 00:08:22,980 You can see we're getting that same gray underline we got before, 152 00:08:22,980 --> 00:08:24,990 because it's not used. 153 00:08:24,990 --> 00:08:30,040 So to do that, let's jump back to our XML here. 154 00:08:30,040 --> 00:08:34,490 And inside of our button, let's say Android, on click, 155 00:08:34,490 --> 00:08:37,542 and then we just need to type the name of this method. 156 00:08:37,542 --> 00:08:39,500 You don't need to specify the class or anything 157 00:08:39,500 --> 00:08:43,159 like that, because Android already knows what activity 158 00:08:43,159 --> 00:08:47,330 is associated with this layout. 159 00:08:47,330 --> 00:08:50,490 So you can see here that after I've specified this on click, 160 00:08:50,490 --> 00:08:52,490 we're getting this helpful error message saying, 161 00:08:52,490 --> 00:08:56,940 the corresponding method handler, Choose Photo View, is not found. 162 00:08:56,940 --> 00:09:01,370 That's because in order to do this, we just have to specify a parameter here. 163 00:09:01,370 --> 00:09:03,980 So we can just say View The-- 164 00:09:03,980 --> 00:09:07,076 let's make sure we automatically import it. 165 00:09:07,076 --> 00:09:11,250 And so we're not actually going to use this parameter, but if we wanted to, 166 00:09:11,250 --> 00:09:14,100 this could tell us which button was pressed. 167 00:09:14,100 --> 00:09:17,250 So in theory, we could hook up the same event handler 168 00:09:17,250 --> 00:09:20,370 to a bunch of different buttons, but we don't need to do that right now. 169 00:09:20,370 --> 00:09:22,110 We can just specify. 170 00:09:22,110 --> 00:09:25,260 We can just add this parameter, and then not use it. 171 00:09:25,260 --> 00:09:27,530 One last thing is, that button looked a little small, 172 00:09:27,530 --> 00:09:29,870 because I set the width to Wrap Content. 173 00:09:29,870 --> 00:09:32,330 So let's actually set this with to Match Parent, 174 00:09:32,330 --> 00:09:36,030 and now the button is going to be the full width of the screen. 175 00:09:36,030 --> 00:09:39,120 So let's try running this app and see what happens. 176 00:09:39,120 --> 00:09:40,400 We've installed successfully. 177 00:09:40,400 --> 00:09:45,020 So let's open up the emulator and tap our now larger Choose Photo button. 178 00:09:45,020 --> 00:09:48,120 179 00:09:48,120 --> 00:09:51,603 Nice, so looks like we've opened up this new activity. 180 00:09:51,603 --> 00:09:53,520 And it's blue and has all these nice controls. 181 00:09:53,520 --> 00:09:54,720 And we didn't write this. 182 00:09:54,720 --> 00:09:57,240 This is just something that's already built into Android, 183 00:09:57,240 --> 00:10:00,220 and we've just popped out to their activity. 184 00:10:00,220 --> 00:10:04,370 So if I select a photo, nothing happens, which makes sense. 185 00:10:04,370 --> 00:10:07,660 We haven't specified what's going to happen when a user selects a photo. 186 00:10:07,660 --> 00:10:10,060 But really, with just those three lines of code, 187 00:10:10,060 --> 00:10:14,080 we can open up this brand new activity with all this functionality already 188 00:10:14,080 --> 00:10:16,190 built in for us. 189 00:10:16,190 --> 00:10:20,230 So now, let's take a look at how we'll handle that data that's 190 00:10:20,230 --> 00:10:22,660 coming from the activity. 191 00:10:22,660 --> 00:10:26,610 To do that, let's open up our main activity again. 192 00:10:26,610 --> 00:10:32,530 And we're going to override a method called On Activity Result. Again, 193 00:10:32,530 --> 00:10:35,290 we're just letting autocomplete do all of that for us. 194 00:10:35,290 --> 00:10:39,880 But this is a method that's defined in that App Compat Activity, that's 195 00:10:39,880 --> 00:10:45,160 going to be called by the system when I exit out of that activity I opened. 196 00:10:45,160 --> 00:10:49,880 So once I select a photo, I'm going to jump into this method here. 197 00:10:49,880 --> 00:10:54,680 Now, you'll notice the first parameter to this method is that Request Code. 198 00:10:54,680 --> 00:10:58,690 So when I come back from that photo picker, the value of this variable 199 00:10:58,690 --> 00:11:01,580 is going to be 1. 200 00:11:01,580 --> 00:11:04,010 Next, we have this Result Code. 201 00:11:04,010 --> 00:11:07,220 And this basically is a way for the other activity 202 00:11:07,220 --> 00:11:11,530 to specify whether or not there was some kind of error. 203 00:11:11,530 --> 00:11:15,340 Lastly, we're getting back an intent, which as you'd guess, 204 00:11:15,340 --> 00:11:21,410 is a way for that activity to pass back any data it wants back to me. 205 00:11:21,410 --> 00:11:23,740 So the first thing you want to do is call Super. 206 00:11:23,740 --> 00:11:27,640 Could be the case that this on activity results in App Compat Activity, 207 00:11:27,640 --> 00:11:29,060 doing something important. 208 00:11:29,060 --> 00:11:32,070 So let's just call Super to make sure. 209 00:11:32,070 --> 00:11:34,350 Now, we want to check a couple of things. 210 00:11:34,350 --> 00:11:45,850 First, we want to make sure that Result Code is OK. 211 00:11:45,850 --> 00:11:50,430 So this is a special constant that's defined in the activity class. 212 00:11:50,430 --> 00:11:52,180 It just indicates that nothing went wrong. 213 00:11:52,180 --> 00:11:54,590 We just want to check for that. 214 00:11:54,590 --> 00:11:57,610 And we also want to make sure that we got some data back. 215 00:11:57,610 --> 00:12:00,600 So we'll say and data is not equal to null. 216 00:12:00,600 --> 00:12:03,440 If we didn't get a data back or something went wrong, 217 00:12:03,440 --> 00:12:07,200 then we don't want to try to load the image. 218 00:12:07,200 --> 00:12:12,450 So now, we want to extract the image from this intent. 219 00:12:12,450 --> 00:12:14,638 And doing so is a little bit verbose, there's 220 00:12:14,638 --> 00:12:16,680 a few different things we're going to have to do, 221 00:12:16,680 --> 00:12:19,390 but let's just walk through them. 222 00:12:19,390 --> 00:12:25,140 The first thing you're going to get from this intent is a URI object. 223 00:12:25,140 --> 00:12:30,750 Now URI is just like a URO, just sort of specifies the location of something. 224 00:12:30,750 --> 00:12:33,350 And to get that, I'm going to say data.getdata. 225 00:12:33,350 --> 00:12:37,450 226 00:12:37,450 --> 00:12:41,430 And so in this intent, there's some data that's being passed along. 227 00:12:41,430 --> 00:12:46,210 And this is going to give me a path to that data. 228 00:12:46,210 --> 00:12:50,280 So now that I have a URI, I want to open up that data, 229 00:12:50,280 --> 00:12:55,350 since that URI is pointing to some image on disk. 230 00:12:55,350 --> 00:13:01,440 To do that, I'm going to call another method that's defined on Activity, 231 00:13:01,440 --> 00:13:04,500 called get content resolver. 232 00:13:04,500 --> 00:13:06,600 This is just a special class that's going to allow 233 00:13:06,600 --> 00:13:08,880 you to do different file operations. 234 00:13:08,880 --> 00:13:13,870 So there's a method on that class called Open File Descriptor. 235 00:13:13,870 --> 00:13:17,360 And it looks like from my autocomplete, this takes a couple arguments. 236 00:13:17,360 --> 00:13:21,160 The first one is sum URI to open. 237 00:13:21,160 --> 00:13:24,140 And the second one is the mode to open it in. 238 00:13:24,140 --> 00:13:28,180 You might remember this from earlier in the course, but if we specify R, 239 00:13:28,180 --> 00:13:32,530 that's saying we just want to read this. 240 00:13:32,530 --> 00:13:34,630 So my autocomplete tells me that this is going 241 00:13:34,630 --> 00:13:40,360 to return some object, called a Parcel File Descriptor. 242 00:13:40,360 --> 00:13:42,190 So let's just save that here. 243 00:13:42,190 --> 00:13:45,050 244 00:13:45,050 --> 00:13:48,340 So from that Parcel File Descriptor, this is basically 245 00:13:48,340 --> 00:13:51,217 a wrapper around that content. 246 00:13:51,217 --> 00:13:53,300 And looks like I've got a red under headline here. 247 00:13:53,300 --> 00:13:54,830 So let's see what it says-- 248 00:13:54,830 --> 00:13:56,020 unhandled exception. 249 00:13:56,020 --> 00:14:01,300 So that means that somewhere in here this exception, file not found, 250 00:14:01,300 --> 00:14:02,500 could be thrown. 251 00:14:02,500 --> 00:14:05,920 That makes sense, because if I try to open up an invalid URI, 252 00:14:05,920 --> 00:14:07,880 something could go wrong there. 253 00:14:07,880 --> 00:14:14,270 So let's surround this block of code, as we did last time, in a try catch. 254 00:14:14,270 --> 00:14:18,670 So let's say try, bump the indentation here. 255 00:14:18,670 --> 00:14:24,610 And then we're going to catch this file not found exception. 256 00:14:24,610 --> 00:14:28,570 If we do have that, let's just use that log class again. 257 00:14:28,570 --> 00:14:31,660 We'll log, tag of CS50. 258 00:14:31,660 --> 00:14:34,120 We'll say image not found. 259 00:14:34,120 --> 00:14:36,160 Then again, we'll pass along that exception 260 00:14:36,160 --> 00:14:38,650 object so that we can print it out. 261 00:14:38,650 --> 00:14:43,630 Finally, we want to change this Parcel File Descriptor into a different type 262 00:14:43,630 --> 00:14:46,480 of object, called a File Descriptor. 263 00:14:46,480 --> 00:14:48,020 Luckily, that's pretty easy. 264 00:14:48,020 --> 00:14:50,440 We can just say File Descriptor. 265 00:14:50,440 --> 00:14:55,270 File Descriptor is Parcel File Descriptor, get File Descriptor. 266 00:14:55,270 --> 00:14:58,180 So this is kind of some of the verbosity I was talking about. 267 00:14:58,180 --> 00:15:02,140 But all you're really doing is just going through a few different objects 268 00:15:02,140 --> 00:15:05,300 to eventually get the data that you want to get. 269 00:15:05,300 --> 00:15:08,740 So now that I have a reference to this File Descriptor, 270 00:15:08,740 --> 00:15:11,830 I want to load it into an image object. 271 00:15:11,830 --> 00:15:16,390 To do that, I'm going to use the class bitmap. 272 00:15:16,390 --> 00:15:19,990 This is a class that's also defined in Android. 273 00:15:19,990 --> 00:15:23,150 And it's basically used to load images. 274 00:15:23,150 --> 00:15:27,640 So for now, let's just call this Bitmap Image. 275 00:15:27,640 --> 00:15:35,290 So now, to go from a File Descriptor to an image, I can say image equals, 276 00:15:35,290 --> 00:15:38,860 I'll use this class called Bitmap Factory. 277 00:15:38,860 --> 00:15:42,170 And this is just a class that has a bunch of static methods. 278 00:15:42,170 --> 00:15:44,590 And when you see the word factory, that basically 279 00:15:44,590 --> 00:15:49,930 means it's a class whose job it is to instantiate objects. 280 00:15:49,930 --> 00:15:53,050 So inside a Bitmap Factory are some methods 281 00:15:53,050 --> 00:15:56,650 that are used to create bitmap objects. 282 00:15:56,650 --> 00:16:00,370 One such method is one from a File Descriptor. 283 00:16:00,370 --> 00:16:03,380 So I have Decode File Descriptor here. 284 00:16:03,380 --> 00:16:07,760 And that's where I can pass in my File Descriptor. 285 00:16:07,760 --> 00:16:11,930 So another convention would be to say New Bitmap and pass something in. 286 00:16:11,930 --> 00:16:14,520 Another way to do that is just to have some factory. 287 00:16:14,520 --> 00:16:18,170 And the factory is producing instances of bitmap. 288 00:16:18,170 --> 00:16:24,043 So this Decode File Descriptor method, it returns a bitmap object. 289 00:16:24,043 --> 00:16:25,960 All right, so just a couple more things to do. 290 00:16:25,960 --> 00:16:30,980 Let's close out this File Descriptor, since we don't need this file anymore. 291 00:16:30,980 --> 00:16:32,050 We've already opened it. 292 00:16:32,050 --> 00:16:32,860 We've decoded it. 293 00:16:32,860 --> 00:16:34,540 We've loaded it into memory. 294 00:16:34,540 --> 00:16:38,260 No reason to leave the file open anymore. 295 00:16:38,260 --> 00:16:42,380 And now it looks like we've got another exception. 296 00:16:42,380 --> 00:16:44,850 So this is Java IO exception. 297 00:16:44,850 --> 00:16:49,550 So let's just change this file not found to an IO exception. 298 00:16:49,550 --> 00:16:53,170 And it just so happens that that's going to cover all of the exceptions that 299 00:16:53,170 --> 00:16:56,060 might be thrown here. 300 00:16:56,060 --> 00:17:00,680 And so now, the last thing we need to do is just load up our image. 301 00:17:00,680 --> 00:17:05,109 So from the Image View, we have this method, set image bitmap. 302 00:17:05,109 --> 00:17:07,599 That's going to take a bitmap object, which is great, 303 00:17:07,599 --> 00:17:10,150 because that's exactly what we have. 304 00:17:10,150 --> 00:17:12,640 And now we can save this. 305 00:17:12,640 --> 00:17:14,730 So just to recap what we just did there. 306 00:17:14,730 --> 00:17:17,940 So we got back an intent from this other activity. 307 00:17:17,940 --> 00:17:22,500 And this intent sent along some data in the form of a URI. 308 00:17:22,500 --> 00:17:27,819 We then took that URI, which is just a path to something on disk. 309 00:17:27,819 --> 00:17:29,100 We opened it up. 310 00:17:29,100 --> 00:17:31,400 We loaded it into an image. 311 00:17:31,400 --> 00:17:35,340 And we took that image and loaded it into the Image View. 312 00:17:35,340 --> 00:17:37,320 So the Image View is ultimately what's going 313 00:17:37,320 --> 00:17:40,630 to display that data on the screen. 314 00:17:40,630 --> 00:17:42,780 So let's try giving this a run. 315 00:17:42,780 --> 00:17:44,920 Everything looks like it installed successfully. 316 00:17:44,920 --> 00:17:47,400 So let's jump to the emulator. 317 00:17:47,400 --> 00:17:49,170 Let's tap on Choose Photo. 318 00:17:49,170 --> 00:17:53,020 Select this photo that I already have downloaded onto my simulator. 319 00:17:53,020 --> 00:17:54,900 And there we go, we've displayed it. 320 00:17:54,900 --> 00:17:58,260 And you'll notice that there's no sort of weird stretching or cropping 321 00:17:58,260 --> 00:18:01,560 happening with the photo, and that's because we've specified that Wrap 322 00:18:01,560 --> 00:18:05,460 Content, rather than giving it an explicit height or width. 323 00:18:05,460 --> 00:18:11,940 So now that we have this photo loaded, let's apply some image filters to it. 324 00:18:11,940 --> 00:18:14,820 So Android doesn't have a lot of this built in. 325 00:18:14,820 --> 00:18:17,790 So we're going to turn to some third party libraries 326 00:18:17,790 --> 00:18:20,230 to use some image filters. 327 00:18:20,230 --> 00:18:23,310 So let's take a look at the libraries that we'll use. 328 00:18:23,310 --> 00:18:25,510 This first library is an image library. 329 00:18:25,510 --> 00:18:27,210 And I'm on the GitHub page here. 330 00:18:27,210 --> 00:18:30,810 And it says Android transformation library providing a variety of image 331 00:18:30,810 --> 00:18:32,070 transforms. 332 00:18:32,070 --> 00:18:33,660 Great, so let's scroll down a bit. 333 00:18:33,660 --> 00:18:35,760 And they have this handy dandy GIF that shows 334 00:18:35,760 --> 00:18:39,413 you all of the different transforms that they can apply to an image. 335 00:18:39,413 --> 00:18:40,830 So we've got some cool stuff here. 336 00:18:40,830 --> 00:18:44,230 Looks like they've got a sepia thing, the sketch one looks pretty cool. 337 00:18:44,230 --> 00:18:46,230 So this library looks pretty good. 338 00:18:46,230 --> 00:18:48,170 I want to use this one. 339 00:18:48,170 --> 00:18:51,090 So then we have this section, how do I use it? 340 00:18:51,090 --> 00:18:54,360 And this is a snippet from a Gradle file. 341 00:18:54,360 --> 00:18:56,940 So it looks here like we have another implementation line. 342 00:18:56,940 --> 00:18:58,570 We've seen this before. 343 00:18:58,570 --> 00:19:01,065 But at the end of it, it says 4.x.x. 344 00:19:01,065 --> 00:19:05,750 We're actually meant to replace that with the actual version of the library 345 00:19:05,750 --> 00:19:07,120 we want to use. 346 00:19:07,120 --> 00:19:08,490 So let's do two things. 347 00:19:08,490 --> 00:19:11,290 First, let's just copy this. 348 00:19:11,290 --> 00:19:12,940 And we'll add this to our Gradle file. 349 00:19:12,940 --> 00:19:16,650 350 00:19:16,650 --> 00:19:18,930 Just like we did before, paste it in. 351 00:19:18,930 --> 00:19:22,650 But we don't want to leave those two X's, so let's go back 352 00:19:22,650 --> 00:19:24,450 to this GitHub page. 353 00:19:24,450 --> 00:19:28,650 And at the top you can see the latest version is 4.1.0, 354 00:19:28,650 --> 00:19:31,330 so we're just going to type that in-- 355 00:19:31,330 --> 00:19:34,300 4.1.0. 356 00:19:34,300 --> 00:19:35,740 So let's come back to GitHub. 357 00:19:35,740 --> 00:19:40,500 And it also says if you want to, use the GPU filters. 358 00:19:40,500 --> 00:19:44,850 I don't know what that is, so let's just keep scrolling-- 359 00:19:44,850 --> 00:19:46,960 transformations. 360 00:19:46,960 --> 00:19:49,450 OK, so here's this GPU filter thing. 361 00:19:49,450 --> 00:19:51,670 OK, there's that sepia, there's that sketch. 362 00:19:51,670 --> 00:19:54,530 So it turns out I do want to use this other thing. 363 00:19:54,530 --> 00:19:58,270 So they link to this other repo here. 364 00:19:58,270 --> 00:20:01,990 And that's this library that this other library depends on. 365 00:20:01,990 --> 00:20:04,450 So in order to use those transforms, we just 366 00:20:04,450 --> 00:20:07,030 have to load in this other library. 367 00:20:07,030 --> 00:20:10,540 Same thing, this gives its own little implementation line. 368 00:20:10,540 --> 00:20:16,963 Let's just copy that, paste that into our Gradle file. 369 00:20:16,963 --> 00:20:18,880 And then, just like before, let's just replace 370 00:20:18,880 --> 00:20:20,860 those X's with the current version. 371 00:20:20,860 --> 00:20:24,760 Looks like it's 2.0.4. 372 00:20:24,760 --> 00:20:28,420 So 204. 373 00:20:28,420 --> 00:20:31,720 And now we've added those dependencies to our project. 374 00:20:31,720 --> 00:20:35,320 Let's click Sync to actually download them. 375 00:20:35,320 --> 00:20:36,050 Looks good. 376 00:20:36,050 --> 00:20:37,700 The sync was successful. 377 00:20:37,700 --> 00:20:40,960 So now we can start using these libraries. 378 00:20:40,960 --> 00:20:45,860 So the first thing you want to do is add a few more buttons to our view. 379 00:20:45,860 --> 00:20:47,390 So let's go ahead and do that. 380 00:20:47,390 --> 00:20:49,510 Let's just start with one. 381 00:20:49,510 --> 00:20:52,030 Let's copy this button. 382 00:20:52,030 --> 00:20:54,200 And we'll change the text to-- 383 00:20:54,200 --> 00:20:55,710 let's start with that sepia filter. 384 00:20:55,710 --> 00:20:57,580 I really like that one. 385 00:20:57,580 --> 00:21:02,772 And then on click, let's call a method called Applied Sepia. 386 00:21:02,772 --> 00:21:04,730 We're going to get a red underline immediately, 387 00:21:04,730 --> 00:21:06,650 because we haven't defined that method yet. 388 00:21:06,650 --> 00:21:08,630 So let's go ahead and do that-- 389 00:21:08,630 --> 00:21:11,123 Public Void apply sepia. 390 00:21:11,123 --> 00:21:13,040 Again, let's just give it that view parameter. 391 00:21:13,040 --> 00:21:16,520 We're probably not going to use it, but it needs to be there. 392 00:21:16,520 --> 00:21:21,020 And let's just give our project a make to make sure there's no errors. 393 00:21:21,020 --> 00:21:21,890 And there's not. 394 00:21:21,890 --> 00:21:24,560 So looks like we've compiled successfully. 395 00:21:24,560 --> 00:21:28,400 OK, so now we're ready to start writing our filters. 396 00:21:28,400 --> 00:21:29,810 So I just found this library. 397 00:21:29,810 --> 00:21:31,580 So I'm not really sure how to use it yet. 398 00:21:31,580 --> 00:21:35,130 So let's check out its documentation. 399 00:21:35,130 --> 00:21:37,320 So here we are on the GitHub page again. 400 00:21:37,320 --> 00:21:40,010 We can scroll past the demo. 401 00:21:40,010 --> 00:21:42,830 And here's a couple examples of what to do. 402 00:21:42,830 --> 00:21:46,690 So looks like this is using this other library, called Glide. 403 00:21:46,690 --> 00:21:48,510 And I haven't loaded that in yet. 404 00:21:48,510 --> 00:21:51,870 So let's just make sure if I need it or not. 405 00:21:51,870 --> 00:21:56,840 So if I click on this link here, yep, so this 406 00:21:56,840 --> 00:22:00,410 looks like it's just another library, just a sort of generic image library, 407 00:22:00,410 --> 00:22:03,230 that this other library also depends on. 408 00:22:03,230 --> 00:22:15,930 So simple, let's just copy, paste this into our Gradle file, sync, 409 00:22:15,930 --> 00:22:17,590 and we're good to go. 410 00:22:17,590 --> 00:22:21,460 So now I have that last dependency that I need. 411 00:22:21,460 --> 00:22:23,970 So now I can start following this example. 412 00:22:23,970 --> 00:22:25,440 So looks like I can say Glide. 413 00:22:25,440 --> 00:22:30,190 Here's some static class that's provided in that Glide library. 414 00:22:30,190 --> 00:22:34,710 It's going to say with this, so this is probably a reference to some activity. 415 00:22:34,710 --> 00:22:39,330 Then it looks like there's a load method, where I can load in some image. 416 00:22:39,330 --> 00:22:41,640 And then I have this method called Apply. 417 00:22:41,640 --> 00:22:45,540 So OK, that looks neat, looks like you can apply some transformations here. 418 00:22:45,540 --> 00:22:47,580 That's great, that's what I want to do. 419 00:22:47,580 --> 00:22:51,310 Then finally, there's this method, into, that looks like it takes an Image View. 420 00:22:51,310 --> 00:22:56,580 And so what's that's going to do is draw out the result of those transformations 421 00:22:56,580 --> 00:22:58,313 into an Image View. 422 00:22:58,313 --> 00:22:58,980 So that's great. 423 00:22:58,980 --> 00:23:00,355 This looks like it's really easy. 424 00:23:00,355 --> 00:23:04,380 It's just one line of code to a plot, to load an image, apply filters to it, 425 00:23:04,380 --> 00:23:05,750 and load into my Image View. 426 00:23:05,750 --> 00:23:06,570 So that's great. 427 00:23:06,570 --> 00:23:08,440 That's exactly what I want to do. 428 00:23:08,440 --> 00:23:13,020 So let's give this a shot with that sepia filter. 429 00:23:13,020 --> 00:23:16,680 OK, so let's open up this main activity and follow those instructions. 430 00:23:16,680 --> 00:23:18,280 Let's say glide.withthis. 431 00:23:18,280 --> 00:23:21,240 432 00:23:21,240 --> 00:23:23,610 That's my activity. 433 00:23:23,610 --> 00:23:24,480 Then what was next? 434 00:23:24,480 --> 00:23:25,800 Next was load. 435 00:23:25,800 --> 00:23:29,130 Looks like there's a load method here that takes a bitmap. 436 00:23:29,130 --> 00:23:31,920 I called my bitmap image. 437 00:23:31,920 --> 00:23:37,590 All right, after I loaded it, I wanted to apply some filter. 438 00:23:37,590 --> 00:23:39,450 Now let's just jump back to documentation 439 00:23:39,450 --> 00:23:41,070 to see what the argument is. 440 00:23:41,070 --> 00:23:46,080 So, OK, looks like we want to say Request Options. 441 00:23:46,080 --> 00:23:49,380 Request Options, then there's that bitmap transform. 442 00:23:49,380 --> 00:23:50,190 I've got a bitmap. 443 00:23:50,190 --> 00:23:51,410 So that looks right. 444 00:23:51,410 --> 00:23:54,880 And then I want to specify a transformation. 445 00:23:54,880 --> 00:23:57,620 So let's go back and see if I can remember-- 446 00:23:57,620 --> 00:24:00,960 OK, so it's this sepia filter transformation. 447 00:24:00,960 --> 00:24:02,820 Let's create a new one of those. 448 00:24:02,820 --> 00:24:06,120 449 00:24:06,120 --> 00:24:08,510 And let's see-- OK, I haven't imported this yet. 450 00:24:08,510 --> 00:24:14,860 So I can just have Android Studio automatically import that for me. 451 00:24:14,860 --> 00:24:19,590 Now, let's just format this a little nicely, so I'll have one per line. 452 00:24:19,590 --> 00:24:21,760 So that's going to apply the transformation. 453 00:24:21,760 --> 00:24:27,020 And last, they're going to specify an Image View to load it into. 454 00:24:27,020 --> 00:24:27,910 And there we go. 455 00:24:27,910 --> 00:24:30,740 That's that same one line that we just looked at. 456 00:24:30,740 --> 00:24:34,037 But this time, we're applying the filter that we want. 457 00:24:34,037 --> 00:24:36,620 And you notice here, Android Studio is being a little helpful. 458 00:24:36,620 --> 00:24:41,690 Again, it's telling you at each stage of this chain what each step is returning. 459 00:24:41,690 --> 00:24:47,190 So this first step looks like it returns a request manager, and so on. 460 00:24:47,190 --> 00:24:49,340 So now, let's try giving this a shot. 461 00:24:49,340 --> 00:24:52,130 Let's run our app. 462 00:24:52,130 --> 00:24:54,160 Looks like we're installed. 463 00:24:54,160 --> 00:24:55,410 There's our new sepia button. 464 00:24:55,410 --> 00:24:56,960 Let's click Choose Photo. 465 00:24:56,960 --> 00:25:00,866 Select that photo and then add sepia. 466 00:25:00,866 --> 00:25:01,830 And there we go. 467 00:25:01,830 --> 00:25:05,400 We've applied an effect to that image. 468 00:25:05,400 --> 00:25:10,770 So now let's add a few other filters to see what else this library can do. 469 00:25:10,770 --> 00:25:15,220 A couple that caught my eye on this list was one was Tune and one was Sketch. 470 00:25:15,220 --> 00:25:18,270 So let's just add both of those. 471 00:25:18,270 --> 00:25:22,830 Just like I did before, let's add a couple buttons first. 472 00:25:22,830 --> 00:25:25,680 Let's go one here and one here. 473 00:25:25,680 --> 00:25:28,620 So let's say this one is Tune. 474 00:25:28,620 --> 00:25:30,150 We'll call this Apply Tune. 475 00:25:30,150 --> 00:25:33,330 And this one is Sketch. 476 00:25:33,330 --> 00:25:36,220 We'll call this Apply Sketch. 477 00:25:36,220 --> 00:25:39,430 OK, my layout is good to go. 478 00:25:39,430 --> 00:25:41,250 Let's jump back to the activity. 479 00:25:41,250 --> 00:25:43,560 Let's define those two methods. 480 00:25:43,560 --> 00:25:47,105 Public Void, Apply Tune. 481 00:25:47,105 --> 00:25:49,630 482 00:25:49,630 --> 00:25:51,900 And Public Void Apply Sketch. 483 00:25:51,900 --> 00:25:55,300 484 00:25:55,300 --> 00:25:57,500 OK, looks good. 485 00:25:57,500 --> 00:26:04,070 And so now, naively, what I could do is just kind of copy, paste this code. 486 00:26:04,070 --> 00:26:05,990 Say I was going to copy it here. 487 00:26:05,990 --> 00:26:10,730 Let's just switch this to tune filter transformation. 488 00:26:10,730 --> 00:26:13,040 But there's kind of a lot of repeated code there. 489 00:26:13,040 --> 00:26:18,480 And so let's see if we can factor some of this out into a separate method. 490 00:26:18,480 --> 00:26:21,470 So let's look at these two method implementations. 491 00:26:21,470 --> 00:26:22,640 And it's mostly the same. 492 00:26:22,640 --> 00:26:24,950 So let's see what's different. 493 00:26:24,950 --> 00:26:28,910 It looks like the only difference is this transformation object. 494 00:26:28,910 --> 00:26:33,970 So maybe let's create a new method that's just called Apply. 495 00:26:33,970 --> 00:26:37,540 And let's pass in that transformation object. 496 00:26:37,540 --> 00:26:41,170 The problem is, is that we need to specify a type. 497 00:26:41,170 --> 00:26:43,810 And it doesn't look like, from just reading 498 00:26:43,810 --> 00:26:45,790 this code, that they're the same type. 499 00:26:45,790 --> 00:26:48,250 They're these two different classes. 500 00:26:48,250 --> 00:26:53,980 But maybe there's some common base class or interface that they both share, 501 00:26:53,980 --> 00:26:57,670 and then we can use that as our type instead. 502 00:26:57,670 --> 00:27:00,880 So as a shortcut to figure out what that might be, 503 00:27:00,880 --> 00:27:02,290 I'm going to use my autocomplete. 504 00:27:02,290 --> 00:27:04,120 So I'm going to go back to Request Options 505 00:27:04,120 --> 00:27:08,398 and just take a look at this method again. 506 00:27:08,398 --> 00:27:10,440 And you'll notice that as soon as I do that, it's 507 00:27:10,440 --> 00:27:14,250 going to tell me the type that's passed into this method. 508 00:27:14,250 --> 00:27:19,050 And it looks like that type is a transformation of type bitmap. 509 00:27:19,050 --> 00:27:22,470 So that's the type that I can use in my new method. 510 00:27:22,470 --> 00:27:27,225 Both of those transformations implement this interface or extend this class. 511 00:27:27,225 --> 00:27:28,570 It doesn't matter. 512 00:27:28,570 --> 00:27:31,750 All that matters is that it's a type that I can use. 513 00:27:31,750 --> 00:27:38,220 So that means that we want our type to be a transformation of a bitmap. 514 00:27:38,220 --> 00:27:41,100 And we'll just call it Filter-- 515 00:27:41,100 --> 00:27:42,420 awesome. 516 00:27:42,420 --> 00:27:48,370 So now, let's move this code into Apply. 517 00:27:48,370 --> 00:27:52,090 Now rather than using this sepia filter transformation, 518 00:27:52,090 --> 00:27:57,060 we're going to use the filter that we passed in. 519 00:27:57,060 --> 00:28:00,240 OK, so now let's use this method. 520 00:28:00,240 --> 00:28:03,060 Let's say Apply. 521 00:28:03,060 --> 00:28:09,180 And then we're going to pass in a new sepia filter transformation. 522 00:28:09,180 --> 00:28:18,760 And similarly, here, we can remove this, and Apply a Tune Filter transformation. 523 00:28:18,760 --> 00:28:22,560 So now let's run a make on this project. 524 00:28:22,560 --> 00:28:25,710 So it looks like we have some compile errors here. 525 00:28:25,710 --> 00:28:27,700 Let's take a look at what it is. 526 00:28:27,700 --> 00:28:33,580 So this says type android.view.animation does not take parameters. 527 00:28:33,580 --> 00:28:36,330 And so that looks a little suspicious, because that's not actually 528 00:28:36,330 --> 00:28:39,000 the transformation that we wanted to import. 529 00:28:39,000 --> 00:28:43,630 So Android Studio actually auto imported the wrong thing. 530 00:28:43,630 --> 00:28:46,290 So let's just jump back up to our imports, 531 00:28:46,290 --> 00:28:50,040 expand this, let's look for that transformation. 532 00:28:50,040 --> 00:28:51,915 And we can just get rid of that. 533 00:28:51,915 --> 00:28:54,000 Notice we also have another unused import. 534 00:28:54,000 --> 00:28:55,560 So let's get rid of that. 535 00:28:55,560 --> 00:28:59,500 What we actually want to import is that other transformation. 536 00:28:59,500 --> 00:29:05,660 So we're looking to import, when we look at using autocomplete again, 537 00:29:05,660 --> 00:29:08,100 this was the name of that library. 538 00:29:08,100 --> 00:29:10,710 So let's use that instead. 539 00:29:10,710 --> 00:29:13,640 So now, we can just get rid of this again. 540 00:29:13,640 --> 00:29:16,160 Now, if we scroll back up to that import list, 541 00:29:16,160 --> 00:29:20,130 you can see now we're importing the right transformation. 542 00:29:20,130 --> 00:29:22,580 And so this is where packages are really important, 543 00:29:22,580 --> 00:29:26,660 because you might have some class name, like transformation, that's 544 00:29:26,660 --> 00:29:29,060 used in a few different packages. 545 00:29:29,060 --> 00:29:31,550 We found out that, apparently, there's some Android 546 00:29:31,550 --> 00:29:33,350 class called transformation. 547 00:29:33,350 --> 00:29:35,122 But it's in a different package. 548 00:29:35,122 --> 00:29:36,830 So we want to make sure that we're always 549 00:29:36,830 --> 00:29:41,060 importing from the right package in order to use the right class. 550 00:29:41,060 --> 00:29:43,860 So let's try building again. 551 00:29:43,860 --> 00:29:46,640 There we go, no compile errors this time. 552 00:29:46,640 --> 00:29:48,830 So let's try it out. 553 00:29:48,830 --> 00:29:51,360 All right, we've installed successfully. 554 00:29:51,360 --> 00:29:54,440 Let's pick our photo. 555 00:29:54,440 --> 00:29:56,500 Make sure sepia still works. 556 00:29:56,500 --> 00:29:57,748 It does. 557 00:29:57,748 --> 00:30:00,040 Now, here's where the Scroll View is going to be handy. 558 00:30:00,040 --> 00:30:02,415 You'll notice that not all the buttons fit on the screen. 559 00:30:02,415 --> 00:30:05,140 So we already added that Scroll View so we can scroll around. 560 00:30:05,140 --> 00:30:07,860 Now let's try out our new filter. 561 00:30:07,860 --> 00:30:09,405 All right, that's pretty interesting. 562 00:30:09,405 --> 00:30:13,210 And so now, let's add that one last filter, 563 00:30:13,210 --> 00:30:16,000 but we can do so really, really quickly, because we already 564 00:30:16,000 --> 00:30:17,260 have this helper method. 565 00:30:17,260 --> 00:30:19,900 So all we need to do is say Apply. 566 00:30:19,900 --> 00:30:22,210 Then let's just pass in that sketch filter. 567 00:30:22,210 --> 00:30:25,090 I don't remember what it was called, but the autocomplete saved me. 568 00:30:25,090 --> 00:30:29,800 So now, let's run the app one last time and check out our new filter. 569 00:30:29,800 --> 00:30:31,730 Installed successfully. 570 00:30:31,730 --> 00:30:32,650 Let's pick our photo. 571 00:30:32,650 --> 00:30:35,660 572 00:30:35,660 --> 00:30:37,760 Hit the new Sketch button. 573 00:30:37,760 --> 00:30:39,410 And there's our filter. 574 00:30:39,410 --> 00:30:42,980 So that's it, we've built our simple image filter app. 575 00:30:42,980 --> 00:30:44,520 And notice how simple that was. 576 00:30:44,520 --> 00:30:47,990 We're really able to leverage those third party libraries 577 00:30:47,990 --> 00:30:51,840 and use code that other people wrote, bring that into our app, 578 00:30:51,840 --> 00:30:56,200 and then build something really cool with only a few lines of code. 579 00:30:56,200 --> 00:30:57,363