Saturday, January 10, 2015

A Different Use for Analytic Functions

I subscribe to a couple of the LinkedIn Oracle discussion groups.  A recent question caught my eye, and I decided to investigate it further, mostly because it relates to a favorite activity of mine this time of year:  my (US) Federal Income Tax.

This is not the original statement of the problem, but it does describe the essence:

One is given the following table:

Income Limit    Tax Rate
18150              10%
73800              15%
148850             25%
226850             28%
405100             33%
457600             35%
over 457600      39.6%

The tax due is computed as follows:  For taxable incomes up to 18150, the tax is 10% of the taxable income.  For incomes between 18150 and 73800 the tax is 10% of 18150 plus 15% of the difference between the income and 18150, etc.

The question then, is, is to design the table and state the query to calculate the tax due.

The comments had a CASE statement being used.  This struck me as unnecessary, so I set out to see if one could solve this problem without one.

In the tax forms, the method is typically stated with a table such as this:

If Your Taxable     But    Your              Of the
Taxable Income is   Less    tax      Plus    Amount 
Greater than        Than     is              Over
      0            18150      0       10%        0
  18150            73800   1815       15%    18150
  73800           148850  10163       25%    73800
 148850           226850  28925       28%   148850 
 226850           405100  50765       33%   226850
 405100           457600 109587       35%   405100
 457600                  127962     39.6%   457600

It is easy to create and load such a table:

Create table tax_brackets (
  lowrange    number(9),
  highrange   number(9),
  taxbase     number(9),
  taxpct      number(4,4))
/

Insert into tax_brackets values
   (148850,  226850,  28925,  .28)
/
Insert into tax_brackets values
   (405100,  457600, 109587,   .35)
/
Insert into tax_brackets values
   (0, 18150, 0,  .10)
/
Insert into tax_brackets values
   (73800,  148850,  10163, .25)
/
Insert into tax_brackets values
   (18150,  73800,  1815,  .15)
/
Insert into tax_brackets values
   (457600,  999999999, 127962, .396)
/
Insert into tax_brackets values
   (226850,  405100, 50765, .33)
/

commit
/


Where I have just used the first four columns of my sample table, and given the columns the names LowRange, HighRange, TaxBase and TaxPct. Having done this, we can calculate the tax due for a given taxable Income with
 

Select TaxBase + TaxPct * (Income - LowRange)  TaxDue
from tax_Brackets
where Income > LowRange
  and Income <= HighRange
/

No CASEs to be found anywhere.

A few words of clarification here: The reader who is carefully reading will have noticed that I have appeared to have loaded the data in random order. This was deliberate. I wanted to make sure that there was no accidental ordering affecting my results. And I included a very large HighRange for my 39.6% row. This was because I didn't want to complicate matters with an almost value.

Of course, this table has some seemingly redundant columns. All the required information can be found in this table:
 

   BRACKET   TAX_RATE
---------- ----------
         0         .1
     18150        .15
     73800        .25
    148850        .28
    226850        .33
    405100        .35
    457600       .396

So, let's create it and see if we can use it instead:
 

Create table tax_bracket2 (
   bracket   number(9),
   tax_rate  number(4,4)  )
/

insert into tax_bracket2 values(   0,   .10)
/
insert into tax_bracket2 values(405100,   .35) 
/

insert into tax_bracket2 values(73800,   .25) 
/
insert into tax_bracket2 values(148850,   .28) 
/
insert into tax_bracket2 values(18150,   .15) 
/
insert into tax_bracket2 values(226850,   .33) 
/

insert into tax_bracket2 values(457600,   .396)
/

commit
/

We know how to solve this problem with the tax_bracket table. If we could write a query to drive the two missing columns, we could use it as a subquery in our original solution. The rest of this post describes how to do precisely that.

To clarify things, consider this query:
 

 select Bracket  LowRange,
            '?'      HighRange,
            '?'      TaxBase,
            Tax_Rate TaxPct
     from tax_bracket2
     order by 1;
/

  LOWRANGE H T     TAXPCT
---------- - - ----------
         0 ? ?         .1
     18150 ? ?        .15
     73800 ? ?        .25
    148850 ? ?        .28
    226850 ? ?        .33
    405100 ? ?        .35
    457600 ? ?       .396

7 rows selected.

 

Clearly (?) this is going to require Analytic Functions. Unfortunately, everytime I need to use an analytic function, I have to start at the beginning. So let's start with the HighRange Column. Reading the definition of the LEAD Analytic Function we see

LAG/LEAD
Finding a value in a row a specified number of rows from a current row.

This looks promising. Here is the syntax:

{LAG | LEAD} ( value_expr [, offset] [, default] )
[RESPECT NULLS|IGNORE NULLS]
OVER ( [query_partition_clause] order_by_clause )

Let's try and plug in some values. For a given row, the value of its HighRange function is the value of the bracket column in the 'next' row. So, we are going to use the LEAD function, and the expression is going to simply be 'bracket'. And since we want the next row, we are going to try an offset of '1'.

At this stage, our function is

LEAD(bracket,1) OVER (PARTITION BY??? ORDER BY ???)

I used to think of the 'PARTITION BY' clause as being a Group By clause for analytic functions. But, after doing my research on this, I've come to think of it more as a Break command in a SQL*Plus report. Using both approaches, it is clear that this is going to be one large grouping, so there should be no PARTITION BY clause. And we can order either by 'bracket' or 'TaxPct'. So, let's try:
 

  select bracket LowRange,
        LEAD(bracket,1) OVER (ORDER BY bracket) HighRange,
        '?'  TaxBase,
        tax_rate TaxPct
    from tax_bracket2
    order by tax_rate
/

  LOWRANGE  HIGHRANGE T     TAXPCT
---------- ---------- - ----------
         0      18150 ?         .1
     18150      73800 ?        .15
     73800     148850 ?        .25
    148850     226850 ?        .28
    226850     405100 ?        .33
    405100     457600 ?        .35
    457600          0 ?       .396

7 rows selected.

I see what went wrong. We are selecting all seven rows from tax_bracket2. The LEAD function is operating on each row. It has sorted all seven rows, and is returning the next 'bracket' value (which I have aliased to LowRange) as the HighRange value in the result set. But, when it operates on the row with 'bracket'=457600, there is no next row. So, it returns the default value. And since we haven't supplied a default value, it defaults to 0.

So, let's try this instead:
 select bracket LowRange,
        LEAD(bracket,1,999999999) OVER (ORDER BY bracket) HighRange,
        '?'  TaxBase,
        tax_rate TaxPct
     from tax_bracket2
     order by tax_rate
/

  LOWRANGE  HIGHRANGE T     TAXPCT
---------- ---------- - ----------
         0      18150 ?         .1
     18150      73800 ?        .15
     73800     148850 ?        .25
    148850     226850 ?        .28
    226850     405100 ?        .33
    405100     457600 ?        .35
    457600  999999999 ?       .396

7 rows selected.

Just for fun, let's see what happens if we make the second parameter = 2,
 

  LOWRANGE  HIGHRANGE T     TAXPCT
---------- ---------- - ----------
         0      73800 ?         .1
     18150     148850 ?        .15
     73800     226850 ?        .25
    148850     405100 ?        .28
    226850     457600 ?        .33
    405100  999999999 ?        .35
    457600  999999999 ?       .396

7 rows selected.

And, just to spare you from having to try it, using '-1' results in an error.

 Now, consider this query and result set from our original table:
 

  1  select LowRange,
  2         HighRange,
  3         (HighRange - LowRange) * TaxPct,
  4         TaxPct
  5  from tax_brackets
  6* order by TaxPct
 /

  LOWRANGE  HIGHRANGE (HIGHRANGE-LOWRANGE)*TAXPCT     TAXPCT
---------- ---------- --------------------------- ----------
         0      18150                        1815         .1
     18150      73800                      8347.5        .15
     73800     148850                     18762.5        .25
    148850     226850                       21840        .28
    226850     405100                     58822.5        .33
    405100     457600                       18375        .35
    457600  999999999                   395818790       .396

7 rows selected.

If we compute a running total of our third column, we get the column I am calling 'TaxBase'. And the SUM analytic function can compute this:
 


  1  select LowRange,
  2         HighRange,
  3         nvl(sum((HighRange - LowRange)*TaxPct)
  4            OVER (order by TaxPct
  5                  rows between unbounded preceding and 1 preceding),0) TaxBase,
  6         TaxPct
  7    from tax_brackets
  8*   order by TaxPct
/

  LOWRANGE  HIGHRANGE    TAXBASE     TAXPCT
---------- ---------- ---------- ----------
         0      18150          0         .1
     18150      73800       1815        .15
     73800     148850    10162.5        .25
    148850     226850      28925        .28
    226850     405100      50765        .33
    405100     457600   109587.5        .35
    457600  999999999   127962.5       .396

7 rows selected.

And, since we know how to compute our 'HighRange' column from our simple tax_bracket2 table, we can derive the entire tax_brackets table thusly:
 

  1  select LowRange,
  2         HighRange,
  3         nvl(sum((HighRange - LowRange)*TaxPct)
  4            OVER (order by TaxPct
  5                  rows between unbounded preceding and 1 preceding),0) TaxBase,
  6         TaxPct
  7    from  (
  8           select bracket LowRange,
  9              LEAD(bracket,1,9999999990) OVER (ORDER BY bracket) HighRange,
 10              tax_rate TaxPct
 11           from tax_bracket2
 12          )
 13*   order by TaxPct

  LOWRANGE  HIGHRANGE    TAXBASE     TAXPCT
---------- ---------- ---------- ----------
         0      18150          0         .1
     18150      73800       1815        .15
     73800     148850    10162.5        .25
    148850     226850      28925        .28
    226850     405100      50765        .33
    405100     457600   109587.5        .35
    457600 9999999990   127962.5       .396

7 rows selected.

And we can write our final tax calculation query thusly:
 

Select TaxBase + TaxPct * (Income - LowRange)  TaxDue
 from (
    select LowRange,
           HighRange,
           nvl(sum((HighRange - LowRange)*TaxPct)
           OVER (order by TaxPct
                rows between unbounded preceding and 1 preceding),0) TaxBase,
           TaxPct
     from  (
            select bracket LowRange,
                  LEAD(bracket,1,9999999990) OVER (ORDER BY bracket) HighRange,
                  tax_rate TaxPct
              from tax_bracket2
            )
      order by TaxPct
        )
   where Income > LowRange
     and Income <= HighRange
/

Packaging this as a function is an excerise left to the reader.

Sunday, October 14, 2012


 I want to follow up on my previous post by looking at the code for the onClick method.  However, a quick look tells me that it is too complex for a tyro to make any sense out of.  So, I'm going to start with an earlier proof of concept:

 [1]  public void onClick(View v){
 [2]       int   dispNumber;
 [3]       TextView text = (TextView)findViewById(R.id.LineOne);
 [4]       EditText formText = (EditText)this.findViewById(R.id.entry);
 [5]       TextView lastText = (TextView)findViewById(R.id.EndOfGame);
 [6]       String DoDahTxt = formText.getText().toString();
 [7]       dispNumber      = Integer.parseInt(DoDahTxt);
 [8]       dispNumber      = dispNumber + 1;
 [9]       DoDahTxt        = Integer.toString(dispNumber);
 [10]      text.setText(DoDahTxt);
 [11]      lastText.setText("You Loose!");  }
 

This went (replaced) the existing onClick method.  It did everything that was needed in the real onClick method: it displayed something, took input from the text box, did something with that input, and displayed some kind of a result.  So let's go over this line by line:

[1]   First line of the block.  Note that this is a Public method, and that it does not return anything.

[2]   The final result will need to keep track of the current working numbers: the number behind what the Rat is displaying (either RAT, CAT, RATCAT, or the number itself if it is not divisible by 3 or 5).  So, we define a local integer variable.

[3]  The first line of our layout (in horizontal mode) consists two views: id/TopLine, which we plan to use as a static text field 'The RAT Says', and the view id/LineOne, which we will use as a dynamic field, and which starts out with the value '1'.  Our code needs to get a handle on this dynamic field.  And we can't just change it's value with simple assignment statements.  Line [3] gives us a variable (which I like to think  of as a handle) named 'text'

 [3]       TextView text = (TextView)findViewById(R.id.LineOne);

Which gives the variable 'text' the id of the text part of the LineOne view.  We will need this later on.  For now, I was getting all the handles at one time.

 [4]       EditText formText = (EditText)this.findViewById(R.id.entry);

[4]  Remember that the data entry text box is part of an EditText view named 'entry'.  And line [4] get us a handle on this data entry box.  Note that the views are named @+id/viewname in the XML, but R.id.viewname in the code.  Then you won't spend as much time as I did worrying about this.

[5]  At this time I was planning on using a view to contain any system comments, such as end of game.  This line gave me a handle on that field. 

[6]  Something different:

[6]       String DoDahTxt = formText.getText().toString();

So, we take whatever was input in the input box, and whose handle is formText (and remember, we only execute this block of code if the button was pushed, so there should be something in the box.  And we get it with the getText method, and then use to String to turn whatever it is into a string.

[7]  What this silly block does is take whatever number the user enters (without checking that it really was a number), add one to it, display the result, and then display 'You Loose'.  Since we plan to add one to whatever, we must make it an integer

[8]  As planned, we add one to it.

[9]  And turn it into a string, so we can stick it back onto our form.


[10]      text.setText(DoDahTxt);
This is (duh!) the setText method, available to all strings.  Since 'text' is a string, it has the setText method (note to the one or two really experienced Java programmers reading this post:  inexperienced Java programmers really need to think like this once in a while).  And this is how whatever is inside the parenthesis gets stuck into 'text'.  No assignment operator needed.

[11]  Then we put something out on the last line, just to prove that we can.

O.K., but what this thingee is really supposed to do is the following:

Variables: 
currNumber:  The number under consideration, from the point of view of whoever is making a move.  At the start of the game, the currNumber is 1.  When the player is ready to make his first move, the currNumber becomes 2.  Then the RAT gets to play, and the currNumber is 3.

maxNumber:  The currNumber at which the game is over.

dispString: What is supposed to be entered or displayed on that turn.  dispString is related to currNumber as follows:

If currNumber is divisible by 15, dispString = 'RATCAT'
Otherwise, if currNumber is divisible by 5, dispString = 'CAT'
Otherwise, if currNumber is divisible by , dispString = 'RAT'
Otherwist, dispString = toString(currNumber).

PreCondition:  currNumber = 1.  Empty input box for the player.
The RAT has displayed '1'.

 Loop Invariant (condition at end of player's turn):  currNumber has been incremented by 2.  The RAT has displayed the dispString for the currNumber.  The player has entered the dispString for the currNumber - 1,  and that string was accepted.  After entering the correct number, the input box has been blanked out.

PostCondition:  either currNumber has exceeded maxNumber, or the player made a mistake.  Display an appropriate message, and restart the game.

Rather than display this message myself, I decided to use the Toast method to display the results of the game.

And, with all of this help, the reader should be able to make sense of the code:


    public void onClick(View v){
        String inputNumber = " ";
        String matchString = " ";
        RatCatApp app = (RatCatApp)getApplication();
        TextView text = (TextView)findViewById(R.id.LineOne);
        EditText formText = (EditText)this.findViewById(R.id.entry);
         if (app.currNumber < (app.maxNumber))
         {
                    app.currNumber++;
                 matchString = getMatchTxt(app.currNumber);
                inputNumber = formText.getText().toString();     
                if (!matchString.equals(inputNumber)){
                        Toast.makeText(this, "You Loose!", Toast.LENGTH_SHORT).show();
                         restartMeUp();
                         } else {
                           app.currNumber++;
                           formText.setText("");
                            text.setText(getMatchTxt(app.currNumber));
                       }
          }  /* All turns until the last one go thru here  */
    
         if (app.currNumber == app.maxNumber) {
             if (matchString.equals(inputNumber)) {
                     Toast.makeText(this, "You Beat Me!", Toast.LENGTH_SHORT).show();
                     restartMeUp();     /*  Correct last turn, let's end here  */
                                                     }
             else  {
                    Toast.makeText(this, "You Loose!", Toast.LENGTH_SHORT).show();
                    restartMeUp();
                     } /* Goofed on the last turn  */
          }
            
         if (app.currNumber >(app.maxNumber))
          {
            Toast.makeText(this, "You Beat Me!", Toast.LENGTH_SHORT).show();
            restartMeUp();
           }   /* should only get here if an odd number for maxNumber is set */
       }
        public static String getMatchTxt(int P1){
                 int  mod = P1%15;
                  if (mod == 0){ return "RATCAT";}
                  if (mod%5 == 0) {return "CAT";}
                  if (mod%3 == 0)  {return "RAT";}
                  return Integer.toString(P1);
              }
        public void restartMeUp() {
               Toast.makeText(this, "Starting Over...", Toast.LENGTH_SHORT).show();
            RatCatApp app = (RatCatApp)getApplication();
               app.currNumber  = 1;
               EditText formTxt = (EditText)this.findViewById(R.id.entry);
                  TextView txt = (TextView)findViewById(R.id.LineOne);
               txt.setText("1");
             formTxt.setText("");
        }

}

Thursday, September 27, 2012

Android Games: Some Code

Time to look at some code.  The way that I would prefer to do this is to start with my first attempt,  describe what went wrong, and expound on what I learned in fixing it.  Unfortunately, my notes are too incomplete to do this.  So, instead, I am going to list my code as it stands Right Now, and comment on each line (or group of lines).

So, here is the code (and note that my numbers make it invalid):

package rat.cat;                         [1]

import static java.lang.System.out;      [2]
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
                                        [3]
public class RatCatActivity extends Activity implements OnClickListener  {
    /** Called when the activity is first created. */
    @Override
        public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
       
        Button buttoff =                       [4]       (Button)findViewById(R.id.button1);    
        buttoff.setOnClickListener(this);       [4a]
    }
   
    public void onClick(View v){
        String inputNumber = " ";
        String matchString = " ";
        RatCatApp app = (RatCatApp)getApplication();
        TextView text = (TextView)findViewById(R.id.LineOne);
        EditText formText = (EditText)this.findViewById(R.id.entry);
         if (app.currNumber < (app.maxNumber))
         {
                    app.currNumber++;
                 matchString = getMatchTxt(app.currNumber);
                inputNumber = formText.getText().toString();     
                if (!matchString.equals(inputNumber)){
                        Toast.makeText(this, "You Loose!", Toast.LENGTH_SHORT).show();
                         restartMeUp();
                         } else {
                           app.currNumber++;
                           formText.setText("");
                            text.setText(getMatchTxt(app.currNumber));
                       }
          }  /* All turns until the last one go thru here  */
    
         if (app.currNumber == app.maxNumber) {
             if (matchString.equals(inputNumber)) {
                     Toast.makeText(this, "You Beat Me!", Toast.LENGTH_SHORT).show();
                     restartMeUp();     /*  Correct last turn, let's end here  */
                                                     }
             else  {
                    Toast.makeText(this, "You Loose!", Toast.LENGTH_SHORT).show();
                    restartMeUp();
                     } /* Goofed on the last turn  */
          }
            
         if (app.currNumber >(app.maxNumber))
          {
            Toast.makeText(this, "You Beat Me!", Toast.LENGTH_SHORT).show();
            restartMeUp();
           }   /* should only get here if an odd number for maxNumber is set */
       }
        public static String getMatchTxt(int P1){
                 int  mod = P1%15;
                  if (mod == 0){ return "RATCAT";}
                  if (mod%5 == 0) {return "CAT";}
                  if (mod%3 == 0)  {return "RAT";}
                  return Integer.toString(P1);
              }
        public void restartMeUp() {
               Toast.makeText(this, "Starting Over...", Toast.LENGTH_SHORT).show();
            RatCatApp app = (RatCatApp)getApplication();
               app.currNumber  = 1;
               EditText formTxt = (EditText)this.findViewById(R.id.entry);
                  TextView txt = (TextView)findViewById(R.id.LineOne);
               txt.setText("1");
             formTxt.setText("");
        }

}

[1] Note that this is package rat.cat.  When you create an application in Eclipse, you have to specify a name, and the name must contain a period.  This name is going to become important; the name of other objects will depend on it.

[2] Next, we must import a number of methods and classes.  Ctrl-Shift-0 will automagically import any missing items.  Of course, sometimes one has to keep adding new imports, because it isn't until item-A gets imported that we find we need item-B

[3]  This method gets created by Eclipse when the project gets created.  AFAIK, every Android App will have some variation of this method.

Now, the basic class (what Eclipse creates) looks like this:

public class RelativeLayoutsActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}

And, a few notes here:

1)   The first thing Android does when it starts up an app is call the onCreate method.  Since every app does something unique from all other apps, they will need a more-or-less unique onCreate class.  So, the native onCreate method must be overridden, and Eclipse throws in the @Override directive to let the compiler know that you want to do this.

2)  The main thing which this stock onCreate does is to call setContentView to put our layout (defined in layout.main) on the screen.  This may be a good place to put an image of the app:


[4]  Note that the main thing we have done to the onCreate method  is to add a button, more specifically, a Push Button, to distinguish it from Radio Buttons and Checkboxes.  This is a subclass of TextView.  It gets placed in the onCreate method because the button must, in general, be ready to roll when the application launches.

Line [4] and [4a] are just about stock, that is, they look just about like every example I have seen.  With some exceptions.  The
"cannonical" example looks like this:

Button button = (Button)findViewById(R.id.button_id);    
        button.setOnClickListener(this);

 
The typical examples use the object types as the names of the instantiated objects.  I have found it instructive to use a different name, to make it obvious to me which is which.  So in the code I have created a button named 'buttoff', whose look-and-feel is defined in my layout XML as 'button1'.

findViewById is a method which returns the view created by the onCreate method which corresponds to the ID in it's argument list.  In other words, it put a handle to the Button view 'button1' into the view variable 'buttoff'.  And the next statement sets up a listener for this button.  Puzzle over this long enough and it almost makes sense.

O.K., that sentence reads like the kind of documentation IBM became famous for: Syntactically correct, semantically accurate, and totally void of any useful information. After talking with someone who understands this better than me, I will now offer the following account:

setOnClickListener is a method which takes an object as an argument.  The expected type of object is an activity.  'This' refers to the current activity.  So, it wants to 'wire' a listenter between the button and something which can handle clicks in the current activity.  And, the classic something for doing that is the onClick method.  And we give Android a clue by stating that our activity (RatCatActivity) is extending onClickListener.

Much easier in Javascript.  But then I've been doing this there a lot longer.   Rather than run on and on, I will pause here and continue tomorrow.

Sunday, August 26, 2012

Android Layout - Part II.

In Part I., I rambled through setting up a layout for the RatCat game.  And I ended with a note that the layout will not work well should the device be held in the landscape position.   Fortunately,   there is a good reference here on how to do this.

It appears that three things are necessary to handle rotation

  1. Create layouts that handle rotation.

  2. Set the rotation behavior in your AndroidManifest file.

  3. Handle rotation in your application code by saving state or caching data.

Let's start with the first one.  I am going to want two layouts: one for portrait and one for landscape.  It turns out that I have to put this landscape layout in a different folder: /res/layout-land

To rotate the screen in my emulator, I use ctrl-F11.  Some emulators use ctrl-F12.  Some use shift-F11.  Some don't work if one uses the right ctrl or shift key.  Ctrl-F11 works for me.

So, I end up with this:

    <TextView
        android:id="@+id/LineOne"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/TopLine"  [1]
        android:layout_centerHorizontal="true"
        android:layout_marginLeft="20dp"
        android:layout_marginTop="15dp"
        android:text="1"                        [2]
        android:textColor="#FFFFFF"            
        android:textSize="36dp" />

    <TextView
        android:id="@+id/LineTwo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/TopLine" 
        android:layout_centerHorizontal="true"
        android:layout_alignLeft="@+id/TopLine" 
        android:layout_marginTop="10dp"
        android:text="@string/LineTwoTxt"
        android:textColor="#FFFFFF"
        android:textSize="30dp" />

    <EditText
        android:id="@+id/entry"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/LineOne"     [3] 
        android:layout_toRightOf="@+id/LineTwo"  
        android:layout_marginLeft="20dp"
        android:layout_marginTop="10dp"
        android:ems="6" >
        <requestFocus />
    </EditText>

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_marginRight="20dp"
        android:layout_below="@+id/entry"
        android:text="@string/Button" />


Notes:
[1]   The ToRightOf method puts the item to the right of TopLine on the same line.

[2]  Make a note of this:  when we shift orientation we reset the "Rat's" display value to '1'. Note that we do not change the internal value.  Which means, if the counter has gone up to 3 (so the Rat is showing 'RAT', and we rotate the device, the RAT is now showing '1', but the correct response is still '4'. 

[3]  We want this to be below Line One and to the right of Line Two.  One might expect that the ToRightOf method would do both.  It doesn't.

This post I actually proofread. And discovered that modern browsers + blogger have a terrible time trying to render the XML from an Android app. It seems that they don't know if it should be considered XHTML or XML, so they combine the worst features of each guess. I found the following problems:
The < and > symbols looked like some sort of a tag, so they and everything between them got stripped.
Trying to surround them with a <PRE> tag didn't work eitherl the browser thought that I had some kind of an XML document, and treated it accordingly.
So, what I finally did was to use the ampersand encoding for LT and GT to get my PRE and /PRE tags. I will be going back and fixing up part I of this.


The problem mentioned in note [2] will be discussed after  the Java code gets discussed, where it may actually make sense.

Friday, July 20, 2012

Android Layout - Pt. 1

So I've discovered that the first step in an Android project is to define the layout.  Which, compared to a web page, is about as difficult (or easy), it just has it different difficulties.

When one creates a project using Eclipse, Eclipse helpfully creates the beginning of a layout.  This layout  (I've starting talking like an Android developer; the term 'layout' is used whenever talking about the screen) looks like this:


    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

            android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/hello" />

Time to try to translate this into something more familiar to a web developer who has minimal experience with Java.

Note that we start out with an xml declaration.  Much of what goes on in an Android application is done in xml. And this is know as 'the mail.xml' file, located in the /res folder.  Main.xml defines the ViewGroup used by the project. 

The ViewGroup contains View groups.  There are five different View groups, but the primary ones are 'Linear Layout' and 'Relative Layout'.  Linear Layout lays out the fields one to a line.  As one seldom wants to have all one's fields one to a line, left justified, there are techniques for doing positioning on a line.

However, if one wants to place multiple items on the same line, or line up items with other items, the way to go is with Relative Layout.  Which is exactly what I have done.

Rather than deconstruct what I came up with, here is my initial layout.  The numbers in square brackets refer to the following notes, and are NOT part of the xml:


    android:layout_width="fill_parent"
    android:layout_height="match_parent"            [1]
    android:background="#A0A000"                    [2]
    android:orientation="vertical" >                [3]

            android:id="@+id/TopLine"                   [4]
        android:layout_width="wrap_content"         [5]
        android:layout_height="wrap_content"
        android:text="@string/TopLineTxt"           [6]
        android:textColor="#FFFFFF"                 [7]
        android:layout_marginTop="20dp"             [8]
        android:layout_centerHorizontal="true"      [9]
        android:textSize="36dp" />

            android:id="@+id/LineOne"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="1"                            [10]
        android:textColor="#FFFFFF"
        android:layout_below="@id/TopLine"          [11]
        android:layout_marginTop="10dp"
        android:layout_centerHorizontal="true"
        android:textSize="36dp" />
   
                android:id="@+id/LineTwo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/LineTwoTxt"
        android:textColor="#FFFFFF"
        android:layout_below="@+id/LineOne"
        android:layout_marginTop="30dp"
        android:textSize="36dp"
        android:layout_centerHorizontal="true" />

            android:id="@+id/entry"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/LineTwo"
        android:layout_marginTop="10dp"
        android:layout_centerHorizontal="true"
        android:ems="10" />                          [12]

   

Note that each item on the screen gets its own view definition.  Now to explain the meaning of those parameters, and try to justify the choices I made:

[1]  The Relative Layout item describes the properties of the screen as a whole.  From the XML point-of-view, it also functions as a container for the individual screen fields. In this case, the parent is the entire device, and I want to define via the layout_height and layout_width parameters that this Relative Layout is to fill up the screen.  Fill_parent and match_parent mean exactly the same thing (the original parameter was match_parent; it was renamed fill_parent in the API version 8), and it means, fill up all of the space of the parent (minus any specified padding).

[2]  We want to specifiy the background color, and this is done using the standard hex color codes.

[3]  This specifies that the device is in the vertical orientation.  More on this later.

[4]  As mentioned above, in Android-Talk, each field is called a view.  And each view (for that matter each object) can be given a name, or 'id'.  For a view, the syntax for specifying the name is  "@+id/idname".  Here, the idname is TopLine

[5] We are in the process of defining a Text View named TopLine.  The layout_width parameter specifies how wide the view will try to be.  Match_parent will make it try to be as wide as it's parent container.  Wrap_content will make it try to be just wide enough for whatever content is put inside of it.  Layout_height is similar.

[6]  An Android project can have another XML folder for the constants.  And if you try to use a literal for a string item, Eclipse will scream at you.  So, the text which this Text View is going to contain is defined in /res/strings.xml as the constant TopLineTxt.  Note that these are truely constants.  Should it become necessary to change the text contained in TopLine, one does NOT simply assign a diffferent string to TopLineTxt in the Java code.

[7]  And it is possible to define the color of this text.

[8]  I wanted to put some separation between the first line and this field.  Layout_marginTop specifies some extra space (here 20dp) on the top side of this view (field).  As to what a 'dp' may be:  According to http://developer.android.com/guide/topics/resources/more-resources.html#Dimension a 'dp' is a Density-Independent Pixel.  The idea is that on a 160 dpi screen 1dp = 1px (pixel).  On a screen with more pixels per inch, the number of physical pixels used to draw 1dp increases.  On screens with poorer resolution, the number decreases.  Google deprecates the use of px as a unit, prefering dp, which provides a painless way to make the image scale up or down depending on the device used.

[9]  Makes sure the text is centered in the field.

[10] Not every string on the screen must be defined in strings.xml.  As LineOne is going to constantly change, there is little to gain by defining its starting value in strings.xml, so it is simply entered as the constant "1".

[11]  This positions the top edge of the LineOne field (view) with the bottom edge of the TopLine field.  We are starting to exploit Relative Layout.

[12]  An EditText view is a box into which one can type some text.  Android:ems specifies how wide this box should be, in ems.  And one em is the width of the letter 'M' in the current font.

So, this a picture of what we have.  But there is a problem.  While this would work just fine on my Droid Pro, on a device with a soft keyboard, said keyboard will take up some of the space.  On my emulator, when the keyboard pops up, it hides the Submit button.  So, I need to alter the layout.  My plan is to reduce the line spacing, reduce the size of those lines, and move the button to the right of the text entry box.  The question is:  do I already know how to do all that?

The changes start out simple enough:



    android:layout_width="fill_parent"
    android:layout_height="match_parent"
    android:background="#A0A000"
    android:orientation="vertical" >

            android:id="@+id/TopLine"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="15dp"
/* Change from 20dp to 15dp  */
        android:text="@string/TopLineTxt"
        android:textColor="#FFFFFF"
        android:textSize="36dp" />

            android:id="@+id/LineOne"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/TopLine"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="10dp"
        android:text="1"
        android:textColor="#FFFFFF"
        android:textSize="30dp" />
/*  Change from 36 to 30dp */

            android:id="@+id/LineTwo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/LineOne"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="10dp"
        android:text="@string/LineTwoTxt"
        android:textColor="#FFFFFF"
        android:textSize="30dp" />
/*  Change from 36 to 30dp */

With the EditText box, we have a problem.  We cannot leave it centered, as (a) there isn't enough room for the button to fit on the right of it,  and (b) that would be really ugly if it did work.  So, I'm going to allign it on the left margin.

Now Eclipse is supposed to have some drag-and-drop functionality for layout. And, if I was starting completely fresh, it just might work.  But, when I try to drag the text box to the margin, I can't get it where I want it.  My suspicion is that Eclipse is trying to retain my prior parameters and add what it hopes I want for new ones, and the combination does not make sense.  So, I have to finish the job by hand:

            android:id="@+id/entry"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/LineTwo"
        android:layout_alignParentLeft="true"
        android:layout_marginLeft="20dp"
        android:layout_marginTop="10dp"
        android:ems="6" >
       
   

   

I did discover the tag in the process of trying to drag-and-drop.  It keeps the focus on the text box, which is something I want.

I have one last issue with my layout. I've been assuming the device has always been held in the portrait orientation.  And this game really needs a different layout if the device is held in the landscape orientation. So, next time we'll look at how to accomplish that.