TOMMY MACWILLIAM: In this video, we're going
to be writing an Android app that applies filters to images.
Much like Instagram, we're going to write our own version,
using a few libraries that we'll download from the Internet.
So let's get started.
Just like before, let's start a new Android Studio project.
We're just going to select an empty activity, because we're
going to write this ourselves.
You can name the application whatever you like.
I'm going to go with 50 gram.
Again, our package name, just edu.harvard.cs50.
That's fine wherever you'd like to save it.
Make sure we've got Java, at least 5.0, and then using AndroidX Artifacts.
So we'll click Finish.
OK, and here is our empty project.
Everything is automatically generated, just as it was before.
Let's just give this a run to make sure it looks like what we expect.
OK, we've installed successfully.
And if we open up the emulator, we've got that blank activity again.
So let's start by writing out the views for this application.
So we'll have a pretty simple UI.
Let's just display a slot for the image that you're looking at.
And then we'll just have a few different buttons.
One button is one to select an image.
And for that, we're going to look into the images
that the user has saved on their phone.
And then we'll just have a couple other buttons
that let you apply different image effects to those images.
So to start writing our UI, remember we're
going to go into this Resources folder, into Layout,
and open our Layout XML File.
So just like we did last time, let's get this text view out of the way.
We don't need that.
Now by default, this is using a Constraint Layout.
So let's look at a different container that we might use,
called a Scroll View.
As it sounds, a Scroll View is going to allow you to scroll contents
that might be too large for the screen.
In fact, that Recycler View that we looked at before
is very similar, because it's allowing you to scroll elements
where the actual list is much larger than the size of the screen.
So let's change this Constraint Layout, and let's instead
make this a Scroll View.
And notice, that we're auto completing automatically.
And everything else is going to stay the same.
Let's just Match Parent, as we did before.
Now inside of the Scroll View, let's again use that Linear Layout.
Since we're just going to be stacking things on top of each other,
we'll have the Image View first and then a series of buttons below it.
Let's add this Linear Layout.
We want the width to be match parent, because that's
going to fill the full width of the screen,
and we want the height to be Wrap Content.
We want the height of this Linear Layout to be
equal to the sum of all the heights of everything inside of it.
So let's create that layout.
And now, let's add a couple things to it to start.
The first thing we'll add is an Image View.
As its name suggests, you can use an Image View to display an image.
So for this, let's specify wrap content for both the width and the height.
And what this will do is make sure that the Image View is just
the size of the image that you loaded, that loads into it.
And let's also give this an ID, because we know that we're
going to want to access it later.
And let's just call it Image View.
That's everything we need there.
So we can have slash and that tag.
And now below it, let's add a button.
In Android, that's just button.
The width and height, let's just Wrap Content again,
so the button is just big enough for the text.
Then from there, let's set the text of our button.
That's the same property we looked at before, just Android text.
Let's say this says Choose Photo.
OK, so let's try running this and see what we get.
We installed successfully.
So let's pull up the emulator.
And right now, there's no image loaded, so that makes sense.
One last thing to do before we forget, is to set that orientation.
So by default, it's horizontal.
And so we just want to change that to vertical, so that all of the elements
stack on top of each other.
So now that our views are setup, let's tackle this problem in two halves.
First, let's figure out how we can let the user
select a photo from their phone and display that photo in the Image View.
Then, after that's working, let's figure out
how we can apply those filters to the image.
So everything is all set in the View.
So let's come back to our activity.
Just like we did before, let's get a reference to that Image View,
so we can save that.
So we'll say Image View.
And inside of On Create, we're going to say Image View is Find View by ID.
And we gave that an ID of Image View.
So next, let's write a method to enable the user
to select a file from their phone.
So let's just call this Public Void, not returning anything.
And we'll call it Choose Photo.
So the way that you can pull up a file selector for the files on a user's
phone is also with an intent.
But this time, rather than specifying a class in our own application to open,
we're going to specify a system wide intent,
so we can actually open some other app on the user's phone.
This is really nice, because it means we don't have to manually write
all of this ourselves.
It'd be a real pain if we had to write a whole image gallery from scratch right
now.
Instead, we can just use the image gallery
that's already built into Android.
So let's take a look.
We're going to create an intent, just like we did before.
But this time, in the Constructor, we're going
to specify intent.action Open Document.
And you can see here, scrolling through the autocomplete, all
of the different things that you might want to specify.
But Open Document, as the name suggests, is
going to open up this gallery view, where you can select a file.
Next, we want to specify to this intent what type of file we're looking for.
So we're going to say intent.set type, And this takes a string.
And we're looking for any type of image.
So the way to specify that is by saying image/*.
So this says, as long as a file type is an image, it could be a JPEG, or a PNG,
or a GIF.
No matter what it is, we want to open it so long as it's an image.
So lastly, we just have to start up that activity.
So just like we did before, we can say Start Activity for Result.
We want to give it some intent.
Then we're also going to specify as the second argument what's
called a Request Code.
And we'll just say 1.
And this is basically a way to identify where the request came from.
So what's going to happen, remember, is as soon as this line executes,
we're going to get bounced out into some other activity.
And eventually, that activity is going to finish.
And we're going to come back to this activity.
And it could be the case that this activity starts off
a few different intents, maybe one to a file selector, another to a camera,
maybe another to a web browser.
And when we come back to our application,
we want to know which intent we just came back from.
So in this case, we'll only have one.
But this Request Code is basically a unique ID that
tells us where we're coming back from.
So when we come back from this intent, we're going to say,
let's make sure the Request Code is 1, because that's
the code we specified for this getting a file intent.
OK, so the last thing to do here is to hook this method up to something.
You can see we're getting that same gray underline we got before,
because it's not used.
So to do that, let's jump back to our XML here.
And inside of our button, let's say Android, on click,
and then we just need to type the name of this method.
You don't need to specify the class or anything
like that, because Android already knows what activity
is associated with this layout.
So you can see here that after I've specified this on click,
we're getting this helpful error message saying,
the corresponding method handler, Choose Photo View, is not found.
That's because in order to do this, we just have to specify a parameter here.
So we can just say View The--
let's make sure we automatically import it.
And so we're not actually going to use this parameter, but if we wanted to,
this could tell us which button was pressed.
So in theory, we could hook up the same event handler
to a bunch of different buttons, but we don't need to do that right now.
We can just specify.
We can just add this parameter, and then not use it.
One last thing is, that button looked a little small,
because I set the width to Wrap Content.
So let's actually set this with to Match Parent,
and now the button is going to be the full width of the screen.
So let's try running this app and see what happens.
We've installed successfully.
So let's open up the emulator and tap our now larger Choose Photo button.
Nice, so looks like we've opened up this new activity.
And it's blue and has all these nice controls.
And we didn't write this.
This is just something that's already built into Android,
and we've just popped out to their activity.
So if I select a photo, nothing happens, which makes sense.
We haven't specified what's going to happen when a user selects a photo.
But really, with just those three lines of code,
we can open up this brand new activity with all this functionality already
built in for us.
So now, let's take a look at how we'll handle that data that's
coming from the activity.
To do that, let's open up our main activity again.
And we're going to override a method called On Activity Result. Again,
we're just letting autocomplete do all of that for us.
But this is a method that's defined in that App Compat Activity, that's
going to be called by the system when I exit out of that activity I opened.
So once I select a photo, I'm going to jump into this method here.
Now, you'll notice the first parameter to this method is that Request Code.
So when I come back from that photo picker, the value of this variable
is going to be 1.
Next, we have this Result Code.
And this basically is a way for the other activity
to specify whether or not there was some kind of error.
Lastly, we're getting back an intent, which as you'd guess,
is a way for that activity to pass back any data it wants back to me.
So the first thing you want to do is call Super.
Could be the case that this on activity results in App Compat Activity,
doing something important.
So let's just call Super to make sure.
Now, we want to check a couple of things.
First, we want to make sure that Result Code is OK.
So this is a special constant that's defined in the activity class.
It just indicates that nothing went wrong.
We just want to check for that.
And we also want to make sure that we got some data back.
So we'll say and data is not equal to null.
If we didn't get a data back or something went wrong,
then we don't want to try to load the image.
So now, we want to extract the image from this intent.
And doing so is a little bit verbose, there's
a few different things we're going to have to do,
but let's just walk through them.
The first thing you're going to get from this intent is a URI object.
Now URI is just like a URO, just sort of specifies the location of something.
And to get that, I'm going to say data.getdata.
And so in this intent, there's some data that's being passed along.
And this is going to give me a path to that data.
So now that I have a URI, I want to open up that data,
since that URI is pointing to some image on disk.
To do that, I'm going to call another method that's defined on Activity,
called get content resolver.
This is just a special class that's going to allow
you to do different file operations.
So there's a method on that class called Open File Descriptor.
And it looks like from my autocomplete, this takes a couple arguments.
The first one is sum URI to open.
And the second one is the mode to open it in.
You might remember this from earlier in the course, but if we specify R,
that's saying we just want to read this.
So my autocomplete tells me that this is going
to return some object, called a Parcel File Descriptor.
So let's just save that here.
So from that Parcel File Descriptor, this is basically
a wrapper around that content.
And looks like I've got a red under headline here.
So let's see what it says--
unhandled exception.
So that means that somewhere in here this exception, file not found,
could be thrown.
That makes sense, because if I try to open up an invalid URI,
something could go wrong there.
So let's surround this block of code, as we did last time, in a try catch.
So let's say try, bump the indentation here.
And then we're going to catch this file not found exception.
If we do have that, let's just use that log class again.
We'll log, tag of CS50.
We'll say image not found.
Then again, we'll pass along that exception
object so that we can print it out.
Finally, we want to change this Parcel File Descriptor into a different type
of object, called a File Descriptor.
Luckily, that's pretty easy.
We can just say File Descriptor.
File Descriptor is Parcel File Descriptor, get File Descriptor.
So this is kind of some of the verbosity I was talking about.
But all you're really doing is just going through a few different objects
to eventually get the data that you want to get.
So now that I have a reference to this File Descriptor,
I want to load it into an image object.
To do that, I'm going to use the class bitmap.
This is a class that's also defined in Android.
And it's basically used to load images.
So for now, let's just call this Bitmap Image.
So now, to go from a File Descriptor to an image, I can say image equals,
I'll use this class called Bitmap Factory.
And this is just a class that has a bunch of static methods.
And when you see the word factory, that basically
means it's a class whose job it is to instantiate objects.
So inside a Bitmap Factory are some methods
that are used to create bitmap objects.
One such method is one from a File Descriptor.
So I have Decode File Descriptor here.
And that's where I can pass in my File Descriptor.
So another convention would be to say New Bitmap and pass something in.
Another way to do that is just to have some factory.
And the factory is producing instances of bitmap.
So this Decode File Descriptor method, it returns a bitmap object.
All right, so just a couple more things to do.
Let's close out this File Descriptor, since we don't need this file anymore.
We've already opened it.
We've decoded it.
We've loaded it into memory.
No reason to leave the file open anymore.
And now it looks like we've got another exception.
So this is Java IO exception.
So let's just change this file not found to an IO exception.
And it just so happens that that's going to cover all of the exceptions that
might be thrown here.
And so now, the last thing we need to do is just load up our image.
So from the Image View, we have this method, set image bitmap.
That's going to take a bitmap object, which is great,
because that's exactly what we have.
And now we can save this.
So just to recap what we just did there.
So we got back an intent from this other activity.
And this intent sent along some data in the form of a URI.
We then took that URI, which is just a path to something on disk.
We opened it up.
We loaded it into an image.
And we took that image and loaded it into the Image View.
So the Image View is ultimately what's going
to display that data on the screen.
So let's try giving this a run.
Everything looks like it installed successfully.
So let's jump to the emulator.
Let's tap on Choose Photo.
Select this photo that I already have downloaded onto my simulator.
And there we go, we've displayed it.
And you'll notice that there's no sort of weird stretching or cropping
happening with the photo, and that's because we've specified that Wrap
Content, rather than giving it an explicit height or width.
So now that we have this photo loaded, let's apply some image filters to it.
So Android doesn't have a lot of this built in.
So we're going to turn to some third party libraries
to use some image filters.
So let's take a look at the libraries that we'll use.
This first library is an image library.
And I'm on the GitHub page here.
And it says Android transformation library providing a variety of image
transforms.
Great, so let's scroll down a bit.
And they have this handy dandy GIF that shows
you all of the different transforms that they can apply to an image.
So we've got some cool stuff here.
Looks like they've got a sepia thing, the sketch one looks pretty cool.
So this library looks pretty good.
I want to use this one.
So then we have this section, how do I use it?
And this is a snippet from a Gradle file.
So it looks here like we have another implementation line.
We've seen this before.
But at the end of it, it says 4.x.x.
We're actually meant to replace that with the actual version of the library
we want to use.
So let's do two things.
First, let's just copy this.
And we'll add this to our Gradle file.
Just like we did before, paste it in.
But we don't want to leave those two X's, so let's go back
to this GitHub page.
And at the top you can see the latest version is 4.1.0,
so we're just going to type that in--
4.1.0.
So let's come back to GitHub.
And it also says if you want to, use the GPU filters.
I don't know what that is, so let's just keep scrolling--
transformations.
OK, so here's this GPU filter thing.
OK, there's that sepia, there's that sketch.
So it turns out I do want to use this other thing.
So they link to this other repo here.
And that's this library that this other library depends on.
So in order to use those transforms, we just
have to load in this other library.
Same thing, this gives its own little implementation line.
Let's just copy that, paste that into our Gradle file.
And then, just like before, let's just replace
those X's with the current version.
Looks like it's 2.0.4.
So 204.
And now we've added those dependencies to our project.
Let's click Sync to actually download them.
Looks good.
The sync was successful.
So now we can start using these libraries.
So the first thing you want to do is add a few more buttons to our view.
So let's go ahead and do that.
Let's just start with one.
Let's copy this button.
And we'll change the text to--
let's start with that sepia filter.
I really like that one.
And then on click, let's call a method called Applied Sepia.
We're going to get a red underline immediately,
because we haven't defined that method yet.
So let's go ahead and do that--
Public Void apply sepia.
Again, let's just give it that view parameter.
We're probably not going to use it, but it needs to be there.
And let's just give our project a make to make sure there's no errors.
And there's not.
So looks like we've compiled successfully.
OK, so now we're ready to start writing our filters.
So I just found this library.
So I'm not really sure how to use it yet.
So let's check out its documentation.
So here we are on the GitHub page again.
We can scroll past the demo.
And here's a couple examples of what to do.
So looks like this is using this other library, called Glide.
And I haven't loaded that in yet.
So let's just make sure if I need it or not.
So if I click on this link here, yep, so this
looks like it's just another library, just a sort of generic image library,
that this other library also depends on.
So simple, let's just copy, paste this into our Gradle file, sync,
and we're good to go.
So now I have that last dependency that I need.
So now I can start following this example.
So looks like I can say Glide.
Here's some static class that's provided in that Glide library.
It's going to say with this, so this is probably a reference to some activity.
Then it looks like there's a load method, where I can load in some image.
And then I have this method called Apply.
So OK, that looks neat, looks like you can apply some transformations here.
That's great, that's what I want to do.
Then finally, there's this method, into, that looks like it takes an Image View.
And so what's that's going to do is draw out the result of those transformations
into an Image View.
So that's great.
This looks like it's really easy.
It's just one line of code to a plot, to load an image, apply filters to it,
and load into my Image View.
So that's great.
That's exactly what I want to do.
So let's give this a shot with that sepia filter.
OK, so let's open up this main activity and follow those instructions.
Let's say glide.withthis.
That's my activity.
Then what was next?
Next was load.
Looks like there's a load method here that takes a bitmap.
I called my bitmap image.
All right, after I loaded it, I wanted to apply some filter.
Now let's just jump back to documentation
to see what the argument is.
So, OK, looks like we want to say Request Options.
Request Options, then there's that bitmap transform.
I've got a bitmap.
So that looks right.
And then I want to specify a transformation.
So let's go back and see if I can remember--
OK, so it's this sepia filter transformation.
Let's create a new one of those.
And let's see-- OK, I haven't imported this yet.
So I can just have Android Studio automatically import that for me.
Now, let's just format this a little nicely, so I'll have one per line.
So that's going to apply the transformation.
And last, they're going to specify an Image View to load it into.
And there we go.
That's that same one line that we just looked at.
But this time, we're applying the filter that we want.
And you notice here, Android Studio is being a little helpful.
Again, it's telling you at each stage of this chain what each step is returning.
So this first step looks like it returns a request manager, and so on.
So now, let's try giving this a shot.
Let's run our app.
Looks like we're installed.
There's our new sepia button.
Let's click Choose Photo.
Select that photo and then add sepia.
And there we go.
We've applied an effect to that image.
So now let's add a few other filters to see what else this library can do.
A couple that caught my eye on this list was one was Tune and one was Sketch.
So let's just add both of those.
Just like I did before, let's add a couple buttons first.
Let's go one here and one here.
So let's say this one is Tune.
We'll call this Apply Tune.
And this one is Sketch.
We'll call this Apply Sketch.
OK, my layout is good to go.
Let's jump back to the activity.
Let's define those two methods.
Public Void, Apply Tune.
And Public Void Apply Sketch.
OK, looks good.
And so now, naively, what I could do is just kind of copy, paste this code.
Say I was going to copy it here.
Let's just switch this to tune filter transformation.
But there's kind of a lot of repeated code there.
And so let's see if we can factor some of this out into a separate method.
So let's look at these two method implementations.
And it's mostly the same.
So let's see what's different.
It looks like the only difference is this transformation object.
So maybe let's create a new method that's just called Apply.
And let's pass in that transformation object.
The problem is, is that we need to specify a type.
And it doesn't look like, from just reading
this code, that they're the same type.
They're these two different classes.
But maybe there's some common base class or interface that they both share,
and then we can use that as our type instead.
So as a shortcut to figure out what that might be,
I'm going to use my autocomplete.
So I'm going to go back to Request Options
and just take a look at this method again.
And you'll notice that as soon as I do that, it's
going to tell me the type that's passed into this method.
And it looks like that type is a transformation of type bitmap.
So that's the type that I can use in my new method.
Both of those transformations implement this interface or extend this class.
It doesn't matter.
All that matters is that it's a type that I can use.
So that means that we want our type to be a transformation of a bitmap.
And we'll just call it Filter--
awesome.
So now, let's move this code into Apply.
Now rather than using this sepia filter transformation,
we're going to use the filter that we passed in.
OK, so now let's use this method.
Let's say Apply.
And then we're going to pass in a new sepia filter transformation.
And similarly, here, we can remove this, and Apply a Tune Filter transformation.
So now let's run a make on this project.
So it looks like we have some compile errors here.
Let's take a look at what it is.
So this says type android.view.animation does not take parameters.
And so that looks a little suspicious, because that's not actually
the transformation that we wanted to import.
So Android Studio actually auto imported the wrong thing.
So let's just jump back up to our imports,
expand this, let's look for that transformation.
And we can just get rid of that.
Notice we also have another unused import.
So let's get rid of that.
What we actually want to import is that other transformation.
So we're looking to import, when we look at using autocomplete again,
this was the name of that library.
So let's use that instead.
So now, we can just get rid of this again.
Now, if we scroll back up to that import list,
you can see now we're importing the right transformation.
And so this is where packages are really important,
because you might have some class name, like transformation, that's
used in a few different packages.
We found out that, apparently, there's some Android
class called transformation.
But it's in a different package.
So we want to make sure that we're always
importing from the right package in order to use the right class.
So let's try building again.
There we go, no compile errors this time.
So let's try it out.
All right, we've installed successfully.
Let's pick our photo.
Make sure sepia still works.
It does.
Now, here's where the Scroll View is going to be handy.
You'll notice that not all the buttons fit on the screen.
So we already added that Scroll View so we can scroll around.
Now let's try out our new filter.
All right, that's pretty interesting.
And so now, let's add that one last filter,
but we can do so really, really quickly, because we already
have this helper method.
So all we need to do is say Apply.
Then let's just pass in that sketch filter.
I don't remember what it was called, but the autocomplete saved me.
So now, let's run the app one last time and check out our new filter.
Installed successfully.
Let's pick our photo.
Hit the new Sketch button.
And there's our filter.
So that's it, we've built our simple image filter app.
And notice how simple that was.
We're really able to leverage those third party libraries
and use code that other people wrote, bring that into our app,
and then build something really cool with only a few lines of code.