vendredi 26 février 2016

Let’s build a simple Android app, part 2

DSCN0192

In the last exciting installment of "Let's Build a Simple Android App"… We went through the process of creating a basic app that asked a question and let you give a response. It was cooler than it sounds – it had a nice color palette and everything.

In part 2, we'll be building on that starting point and adding some more advanced functionality. There will be multiple questions, sounds, animations and more. You can either play along and build something similar for your own ends, or you can take each lesson as it comes and apply it to another project.

Either way, I do recommend that you read part one first. You can find that here.

Also, fair warning: this is not all going to be easy. By the end, we'll be working with strings, arrays, nested if statements… you name it. I'm sure a lot of you won't have the patience to build this whole thing but in that case you can tell from the headings what each section is about and just learn the things you're interested in.

If you are playing along, then grab a cup of coffee, put on some Daft Punk and let's get to work! Oh and you can find all the resources and code on GitHub here.

Adding pretty colored buttons

Straight out the gate let's add something easy that looks good. That way, we'll have an early win in our pockets.

Just add this line to the button widgets in activity_questions.xml:

style="@style/Widget.AppCompat.Button.Colored"

Note: You need to add this line twice, once for each button.

If you recall, we previously edited the file 'colors.xml' and defined values for 'colorPrimaryDark' and 'colorAccent' using the palette we created at Paletton. This means that when you make your buttons colored, they should automatically match the color scheme you've been using and it looks pretty great. It's certainly much more professional looking than the default 'plain' buttons we had before.

Screenshot_2016-02-24-14-31-14-16x9-1080p

This was nice and easy but don't be deceived. It's going to get a LOT more difficult… But fun too. Definitely fun…

Decorating our questions page

Next up, it's time to add in a fancy animation. The toast message is nice and all, but it's not a terribly attractive way to congratulate our users for getting the right answer. We want to make something with a little polish!

To accomplish this, first we need to create a new 'ImageView'. This is simply a type of view that shows an image. It is aptly named…

If you remember, activity_questions.xml used both a vertical and horizontal linear layout. This is going to go after the first linear layout closes, but before the second one closes:

'Weirdtick' is another image I made. It's a weird tick that is supposed to be in-keeping with the rest of this app's design. This will go in our 'drawables' folder with the logo from part 1.

weirdtick

If you've done this right, then the screen should now have a little tick just below the buttons in the center. The 'id' for this image view is 'tickcross'. That will make sense in a moment…

Below that, we're going to add some text congratulating our winner:

Screen 1

And finally, let's put a button just below that so they can progress to the next question:

So now you might be wondering: 'wait… what?' Currently we're saying 'correct' before the user has actually written anything. That is obviously not what we want…

Screenshot_2016-02-24-16-36-02-16x9-1080p

So now you're going to change that by going back to the Java for this page (questions.java) and inserting these three lines of code:

findViewById(R.id.tickcross).setVisibility(View.INVISIBLE);  findViewById(R.id.correctornot).setVisibility(View.INVISIBLE);  findViewById(R.id.nextbutton).setVisibility(View.INVISIBLE);

screen 3

This will go right underneath 'onCreate' within the curly brackets. This means that as soon as the activity appears, those views are going to disappear so that we can't see them. This will happen so fast that no-one will possibly see them.

Notice that we are now changing attributes of our layout programmatically. This will come in handy a lot, so it pays to remember that your xml files are really only setting the starting conditions for your UI.

And can you guess what happens when the user gets the right answer? They appear again! To test this, you can simply find the 'Right!' toast message in questions.java and replace it with these three lines:

findViewById(R.id.tickcross).setVisibility(View.VISIBLE);  findViewById(R.id.correctornot).setVisibility(View.VISIBLE);  findViewById(R.id.nextbutton).setVisibility(View.VISIBLE);

So now, when the user gets the answer right, these congratulatory views will spring up. But that's not very pretty now, is it?

Fancy animations

What we need is a fancy animation to make this a little nicer. We can do this pretty easily in our questions.java by adding this code after we set 'tickcross' to visible:

TranslateAnimation animation = new TranslateAnimation(0,0,2000,0);  animation.setDuration(1000);  findViewById(R.id.tickcross).startAnimation(animation);

All you really need to know is that this creates an animation that's affecting our tick. To talk you through it a little, we create the new animation and define how it's going to work in the top line. 'Translate' means that the animation is moving (as opposed to spinning or fading), while the four numbers in the brackets are coordinates that relate to its current position. The first two refer to the 'x' coordinate and refer to where it is moving to and where it is moving from respectively (with 0 being the current position). The latter two numbers are the same thing but for the 'y' coordinate. Here we are moving along the Y axis from 2000 (far down the screen) to the starting position.

Note: You will need to import TranslateAnimation by clicking on it and then pressing alt + return when instructed to.

This is how the animation will look when we're done...

This is how the animation will look when we're done…

The next line tells us how quick the animation is. In this case, it lasts one second. Lastly, the third line tells the view 'tickcross' to use our animation and sets it into motion.

As you can see, everything appears at once, except the tick which moves upward from the bottom of the screen. But wouldn't it look better if the text and the 'next' button appeared only once the tick reached its final resting place? (Weirdly ominous phrasing there, sorry…)

We can do this by adding an 'animationListener'. What this means is that your app is now observing the animation and will know when it starts, ends and repeats (we haven't told it to repeat, so we don't need to worry about this).

To use one, you want to add this line underneath 'setDuration' and before you start the animation:

animation.setAnimationListener(new Animation.AnimationListener()

When you do this, you should find that Android Studio automatically ads in some extra code for you with a curly bracket. If it doesn't, then the code should look look like this:

animation.setAnimationListener(new Animation.AnimationListener() {      @Override      public void onAnimationStart(Animation animation) {        }        @Override      public void onAnimationEnd(Animation animation) {              }        @Override      public void onAnimationRepeat(Animation animation) {        }  });

What we're interested in is the 'onAnimationEnd' part, which fires once the animation has finished (one second after you hit 'Okay').

Move the code around so that the text and button are set to visible in this event and that way, they'll pop up once the tick is nicely in position. It all just looks a whole lot nicer. After this, you're then starting the animation on the view.

screen 5

So the whole thing looks as follows:

if (answer.equals(correctanswer)) {      findViewById(R.id.tickcross).setVisibility(View.VISIBLE);      TranslateAnimation animation = new TranslateAnimation(0,0,2000,0);      animation.setDuration(1000);      animation.setAnimationListener(new Animation.AnimationListener() {          @Override          public void onAnimationStart(Animation animation) {            }            @Override          public void onAnimationEnd(Animation animation) {              findViewById(R.id.correctornot).setVisibility(View.VISIBLE);              findViewById(R.id.nextbutton).setVisibility(View.VISIBLE);            }            @Override          public void onAnimationRepeat(Animation animation) {            }      });      findViewById(R.id.tickcross).startAnimation(animation);    } else {      Toast toasty = Toast.makeText(getApplicationContext(), "Nope!", Toast.LENGTH_SHORT);      toasty.show();  }

Run the app and see for yourself what a difference that makes! Remember, it's the little details that make your app look and feel more professional.

Creating a method

So that's what happens when our users get the answer right. How about when they get it wrong? In this case, you want to do the exact same thing, except you're showing a cross and you're not telling them they're correct. In fact, it would be great if we could show the right answer so they learn for next time.

First, let's get the 'wrong' button to do the same thing as the right button; then we can tweak the specifics. Before you set about copying and pasting though, know that this isn't good coding practice as it's unnecessarily lengthy. It's okay, you weren't to know.

Ideally, when programming you want to avoid doing anything more than once if at all possible. Programming is one aspect of life where laziness is encouraged. As such, the best way for us to go about this is to take everything we just wrote and drop it into a separate method (also called a function). This is a separate 'event' we can trigger from anywhere else in our code whenever we need a certain sequence to happen.

To do this, you will create a new public void just like the onClick listeners and place it anywhere within questions.java – as long as it's not inside another method (so it will be inside the 'public class' curly brackets but not inside any 'public void' curly brackets).

This will look like so:

public void answersubmitted() {  }

Don't worry about the brackets for now, just know that you always need them when you create a new method. You can now put any code you like inside those brackets and then run that code from within other functions. So paste all of the code that made the views become visible and that handled our animation into here. In other words, all the code from within the if statement that checked if the answer given equals the correct answer:

screen 5

And now, where that code used to be (in the onClick method), you can just write 'answersubmitted();' to make the same thing happen.

That means we can also put this line where we used to have the toast message for incorrect answers, rather than writing everything out twice.

if (answer.equals(correctanswer)) {      answersubmitted();    } else {      answersubmitted();  }

But, by calling answersubmitted when the answer is wrong then the same thing happens whether the user gets the answer right or wrong. We can change that by manipulating our views from within the code again.

This time, we're finding the views the 'proper' way, by creating new 'TextView' and 'ImageView' references so that we can mess around with their specific properties. Then we're just going to change the text and the image before running the animation. This looks like this:

if (answer.equals(correctanswer)) {      TextView t = (TextView) findViewById(R.id.correctornot);      t.setText("CORRECT!");      ImageView i = (ImageView) findViewById(R.id.tickcross);      i.setImageDrawable(getDrawable(R.drawable.weirdtick));      answersubmitted();    } else {      TextView t = (TextView) findViewById(R.id.correctornot);      t.setText("CORRECT ANSWER: " + correctanswer);      ImageView i = (ImageView) findViewById(R.id.tickcross);      i.setImageDrawable(getDrawable(R.drawable.weirdcross));      answersubmitted();  }

Note: You may need to import TextView by clicking on it and then pressing alt + return when instructed to.

screen 6

You'll also notice that the way we change the answer for the wrong response is a little different. This allows us to show the correct answer using the 'correctanswer' string we made earlier, as well as some text. By doing it this way, we'll be able to have the correct answer change as the question changes and we won't have to rewrite any code.

Likewise, we're setting the drawable either to the 'weirdtick' or to a 'weirdcross', the latter of which is another image I've created for the drawable folder. It's a cross. And it's weird.

weirdcross

I also think that we should make everything consistently capitals. Remember in part 1 we set the answer to lower case? Now we're going to change that by setting the answer and the question to upper case (this also means we don't need to worry about using the correct case when we add to strings.xml). Swap that lower case code with these two lines:

correctanswer = correctanswer.toUpperCase();  answer = answer.toUpperCase();

So now when you get an answer wrong, the same thing happens except the image and text are different to indicate you didn't get it right. We're still a little way off though, as there's currently only one question and you can keep putting in different answers to get different responses. So in the next section, we'll be introducing variables!

Screenshot_2016-02-25-10-05-37-16x9-1080p

Introducing booleans

A variable is something you can use to carry data. In math, you might remember using variables like 'x' and 'y' for equations, where those letters would have represented numbers.

x + y = 13
x – y = 7
Find x and y

Sound familiar?
We've already used one type of variable when we used strings. Strings are variables that can 'stand in' for characters rather than numbers. Now we're going to use another variable type called a 'boolean'.

Essentially, a boolean is a variable that can be either a '1' or a '0', which in computer speak means 'true' or 'false'. In this case, we're going to use a boolean to record and test whether or not the question has been answered. So just above the 'onCreate' method, add this line:

private boolean done;

This boolean will be 'false' by default (all variables equal zero when you create them) but after the user clicks 'Okay', we're going to set it to 'true'. The 'Okay' button will only work the first time, when it is 0, as everything inside the 'onClick' will also be inside an if statement. It should look like this:

  public void onAnswerClick(View view) {          if (done == false) {              String answer = ((EditText) findViewById(R.id.answer)).getText().toString();              String correctanswer = getString(R.string.A1);              //gets the answer and correct answer from the edit text and strings.xml respectively              answer = answer.toLowerCase();              //makes sure that the strings are lower case                  if (answer.equals(correctanswer)) {                  TextView t = (TextView) findViewById(R.id.correctornot);                  t.setText("CORRECT!");                  ImageView i = (ImageView) findViewById(R.id.tickcross);                  i.setImageDrawable(getDrawable(R.drawable.weirdtick));                  answersubmitted();                } else {                  TextView t = (TextView) findViewById(R.id.correctornot);                  t.setText("CORRECT ANSWER: " + correctanswer);                  ImageView i = (ImageView) findViewById(R.id.tickcross);                  i.setImageDrawable(getDrawable(R.drawable.weirdcross));                  answersubmitted();              }              done = true;          }      }    }  

screen 7
Run the app and you'll find that after your first answer, the app now won't accept any more input. Now we want to make the 'Next' button clean things up a bit. The next bit you should be familiar with. We're adding an 'onClick' to our 'Next' button and by now you've done this a few times for different widgets. First, hop back into 'activity_questions.xml' and insert this line anywhere in the next button widget:

android:onClick="onNextClick"

Now return to the questions.java and add your onClick method. You know the drill, it's:

public void onNextClick(View view) {    }

And you can put this anywhere, as long as it's not inside another method. This will run whenever we click that button and the first thing we're going to do is to clear the answer and images away and refresh all the text.

Again, you should know how most of this code is working at this point:

if (done) {  findViewById(R.id.tickcross).setVisibility(View.INVISIBLE);  findViewById(R.id.correctornot).setVisibility(View.INVISIBLE);  findViewById(R.id.nextbutton).setVisibility(View.INVISIBLE);  EditText et = (EditText) findViewById(R.id.answer);  et.setText("");    done = false;  }

Notice that we're also setting 'done' to false – which lets people click the 'Okay' button again with their new answer. The whole thing is also inside an 'if (done)' statement, which means the user can't accidentally click 'Next' while it's invisible before they've answered the question.

The eagle-eyed among you will also have noticed that I didn't right 'if (done == true)'. That's because booleans let you skip that bit. If 'done' is true, then that if statement statement is true. Choose the names for your booleans wisely and this means it can read like plain English, making it easier to look through your code later. For instance 'If (userhasclickedexit) { finish() }'.

This is a pretty short experience for our users at the moment, so now we need to start adding extra questions. This is where things get a little more complicated. You ready? Sure?

And now: arrays!

At this point, hitting next after submitting your answer simply returns you to the position you were in to begin with and lets you do the first question again. Obviously that's not we want and this is where we're going to need two more types of variables: an 'integer' (just called 'int') and an 'array'. We'll be looking at the array first.

An array is essentially a variable that contains multiple other variables and assigns each one an index. We're making an array of strings and this is going to allow us to retrieve the string we want by using its corresponding number.

Probably best if I just show you…

So open up strings.xml. You should remember at this is where we stored our questions, hints and answers as strings. Now though, we're adding in some arrays. This will look like so:

      What is the letter A in the phonetic alphabet?      What is the letter B in the phonetic alphabet?      What is the letter C in the phonetic alphabet?          alpha      bravo      charlie          A tough, domineering bloke      Well done!      Snoopy\'s mate  

That's three different arrays – 'questions', 'answers' and 'hints' – and each one has three different strings inside it. Notice the '\' in the third hint; you need to insert a backslash first whenever you use an apostrophe to differentiate it from opening or closing your quotes.

screen 8

Now to grab these strings, we need to create a string array in our java and then say which string from that array we want to retrieve. A string is written as 'String[]' and when retrieving strings, you put the index inside those square brackets.

But because this wasn't complicated enough already, there's an extra caveat you need to keep in mind, arrays are indexed from zero. This means that the second string has an index of one. So if you have 7 strings, the index of the last string is '6'.

Right, so if we add this line to our 'Next' button's 'onClick' method in questions.java, we can see this in action:

String[] questions = getResources().getStringArray(R.array.Questions);  TextView t = (TextView) findViewById(R.id.question);  t.setText(questions[1]);

You will probably see an error for R.id.question, that is because during part 1 we didn't give the TextView which shows the questions and ID. So jump over to activity_questionts.xml and add the following line to the TextView which is used to display strings/Q1:

android:id="@+id/question"

Now, when you click 'Next' everything will clear and the question will change to question two (stored in the first position). Study that code for a moment and make sure you can see how it's all working.

Fun with integers

There's a problem with this though, which is that we're having to manually tell our app which string to grab and at the moment it sticks at '2'. Instead, we want it to move from question 1 to question 2 and beyond all on its own.

This is where our 'integer' comes in. This is a variable that simply stores a single whole number (i.e. no decimal points). We're going to create our integer and stick it up the top of the questions.java underneath our 'done' boolean. I'm calling mine 'QuestionNo'.

screen 11

As QuestionNo represents a number, that means you can replace:

t.setText(questions[1]);

With:

t.setText(questions[QuestionNo]);

screen 9
At the moment, QuestionNo equals '0', so this will just send the user back to the start again. To fix that, we want to increase the value of our question number each time the user clicks 'Next'. We can do this by inserting the following line in the onNextClick method, right before we use the variable:

QuestionNo = QuestionNo + 1;

Now the value of the question number goes up by one each time, meaning that the next question will be shown from the array on each refresh. You can also write this as 'QuestionNo++;' which is shorthand for when you want to incrementally increase an integer.

There's one more problem though, which is that our app will crash once the user gets past question three. We need another 'if' statement then, this time showing the following:

if (QuestionNo < (questions.length - 1)) {

Here, 'questions.length' will return an integer that corresponds to the number of questions in your array. We can treat it just like any other integer, just as some lines of code previously stood-in for strings. We're now comparing the length of our array with 'QuestionNo' and want to stop once the value of QuestionNo is one less. Remember: the last filled position is '2', not '3'.

Now the whole thing should look like this:

public void onNextClick(View view) {      if (done) {          String[] questions = getResources().getStringArray(R.array.Questions);          if (QuestionNo < (questions.length - 1)) {              QuestionNo = QuestionNo + 1;              TextView t = (TextView) findViewById(R.id.question);              t.setText(questions[QuestionNo]);                findViewById(R.id.tickcross).setVisibility(View.INVISIBLE);              findViewById(R.id.correctornot).setVisibility(View.INVISIBLE);              findViewById(R.id.nextbutton).setVisibility(View.INVISIBLE);              EditText et = (EditText) findViewById(R.id.answer);              et.setText("");                done = false;          }      }  }

Hey, I told you it wasn't easy! Just to recap though, this code fires when the user clicks 'Next'. It then clears up all our UI elements and increases the QuestionNo to the next question (up until the last question).

At the moment though, the correct answer is always going to be 'alpha', which we don't want! To fix this little problem, we need to refer to our other arrays to get the hints and the answers elsewhere in the code. 'onAnswerClick' now looks like this:

public void onAnswerClick(View view) {      if (done == false) {          String answer = ((EditText) findViewById(R.id.answer)).getText().toString();          String[] answers = getResources().getStringArray(R.array.Answers);          String correctanswer = answers[QuestionNo];          //gets the answer and correct answer from the edit text and strings.xml respectively          correctanswer = correctanswer.toUpperCase();          answer = answer.toUpperCase();            if (answer.equals(correctanswer)) {              TextView t = (TextView) findViewById(R.id.correctornot);              t.setText("CORRECT!");              ImageView i = (ImageView) findViewById(R.id.tickcross);              i.setImageDrawable(getDrawable(R.drawable.weirdtick));              answersubmitted();            } else {              TextView t = (TextView) findViewById(R.id.correctornot);              t.setText("CORRECT ANSWER: " + correctanswer);              ImageView i = (ImageView) findViewById(R.id.tickcross);              i.setImageDrawable(getDrawable(R.drawable.weirdcross));              answersubmitted();          }          done = true;      }  }

And 'onHintClick' look like this:

public void onHintClick(View view) {      String[] hints = getResources().getStringArray(R.array.Hints);      Toast toasty = Toast.makeText(getApplicationContext(), hints[QuestionNo], Toast.LENGTH_SHORT);      toasty.show();  }

I've also opted to create the question programmatically in my 'onCreate' method. In other words, I don't want to manually define the first question in 'activity_questions.xml' any more, but rather by using this again:

String[] questions = getResources().getStringArray(R.array.Questions);  TextView t = (TextView) findViewById(R.id.question);  t.setText(questions[QuestionNo]);

This means that you should be able to delete all references to 'Q1', 'A1' and 'H1' throughout your code and in your strings.xml. It's just a bit tidier and it means if you want to change the questions later on, you only have to change them in that one place.

screen 13

Finishing touches – sound and orientation

The cool thing about the way we've structured this app is that you can add as many questions to the array as you like it be able to adapt with no changes to the code. Just make absolutely sure that you have the same number of hints and answers to go along with those questions.

One thing you might notice that still isn't quite right though, is that rotating the app makes us lose our place and go back to the first question. This is because apps essentially refresh every time you rotate the screen and to fix this, you'll need to either freeze the orientation of the activity or learn about app life-cycles and saveInstanceState.

I've given you the links so you can start doing your own research but the most logical way for us to go about this is to lock the orientation. We do this by opening 'AndroidManifest.xml' and adding this line to the two activities:

android:screenOrientation="portrait"    

screen 10

I've also taken the liberty of adding some sound effects to the app as well. To do this, I created a new folder called 'raw', in the 'res' directory (just using Windows Explorer) and I put two '.wav' files in there (created with Bfxr). One of these is called 'right.wav' and one is called 'wrong.wav'.

Have a listen and see what you think. If you think they're horrid, you can make your own. If you don't think they're horrid… then you're wrong.

I then added these two lines to the 'onAnswerClick' method where the 'correct' sequence of events are:

MediaPlayer mp = MediaPlayer.create(getApplicationContext(), R.raw.right);  mp.start();

We can also do the same but with 'R.raw.wrong' for the 'incorrect' sequence:

if (answer.equals(correctanswer)) {      TextView t = (TextView) findViewById(R.id.correctornot);      t.setText("CORRECT!");      MediaPlayer mp = MediaPlayer.create(getApplicationContext(), R.raw.right);      mp.start();      ImageView i = (ImageView) findViewById(R.id.tickcross);      i.setImageDrawable(getDrawable(R.drawable.weirdtick));      answersubmitted();    } else {      TextView t = (TextView) findViewById(R.id.correctornot);      t.setText("CORRECT ANSWER: " + correctanswer);      MediaPlayer mp = MediaPlayer.create(getApplicationContext(), R.raw.wrong);      mp.start();      ImageView i = (ImageView) findViewById(R.id.tickcross);      i.setImageDrawable(getDrawable(R.drawable.weirdcross));      answersubmitted();  }

Remember to import Media Player as well, as prompted by Android Studio.

Where to next?

Okay, so as you can see, programming can be complex, but it isn't impossible. Hopefully you're still with me and hopefully you managed to take something helpful from this tutorial. Don't worry if it doesn't work at first, just carefully read through the code and double check everything – normally the answer is staring you in the face. And remember, you can just copy and paste from my code here and reverse engineer it.

There are lots more things I'd like to add to this but I think we've covered more than enough for one post. It would be good to add in some kind of message congratulating the user when the get to the end for example. Giving them the opportunity to start again would also make sense and to do this you could create a new activity or use dialogs. It would also be cool to have more than one set of questions and maybe to let the user create their own questions too (using OutputStreamWriter perhaps). You could also add some animations to the text when the next question loads. And how about keeping tabs on a score?

This is where the fun bit comes in – deciding what you want to do next and then looking up the best way to do it. Copy and paste the examples you find and expect a little trial-and-error to get it to run. Gradually, you'll start to understand how it's all working and you'll find yourself adding more and more elaborate features. Once you've Goolged and implemented your first line of code, you're officially an app developer.

Welcome to the club!



via Android Authority http://ift.tt/1Uo9nGp

IFTTT

Put the internet to work for you.

Turn off or edit this Recipe

Aucun commentaire:

Enregistrer un commentaire