Colors: the Color class Drawing: the Paint and Canvas classes Drawing the Sudoku puzzle



Download 257.75 Kb.
Date13.05.2017
Size257.75 Kb.
#17913
2D Graphics
Contents

  1. Colors: the Color class

  2. Drawing: the Paint and Canvas classes

  3. Drawing the Sudoku puzzle

  4. Handling user input into the puzzle

    1. Defining and moving the selected tile

    2. Entering numbers in the selected tile (keyboard input, D-pad, touch screen)

  5. Game extensions

    1. Check for invalid user input and reject it

    2. Shake the screen when detecting invalid input

    3. Display only valid inputs in the keypad dialog

Android provides two very powerful graphics libraries: one for 2D graphics and one for 3D graphics. The 2D graphics library is available in the android.graphics package. Basic classes in this package include: Color, Paint, and Canvas.


1. Colors
The Color class defines methods for creating and converting colors. An Android color is represented as 4 components, one each for alpha, red, green, blue (ARGB). Each component ranges between 0..255 with 0 meaning no contribution for that component, and 255 meaning 100% contribution. Red, green and blue are primary colors which combined in certain proportions can generate any color. Alpha is a measure of transparency. The lowest value, 0, indicates the color is completely transparent. It does not really matter what the values of RGB are, if Alpha is 0. The highest value, 255, indicates the color is completely opaque. Values in the middle are used for translucent/semitransparent colors. They let you see some of what is underneath the object being drawn in the foreground. Note that Android colors are packed into a 32-bit integer (8 bits for each of the 4 components), and for efficiency, Android code uses integers to store colors instead of objects of the color class.
Examples:
opaque-blue: 255, 0, 0, 255

opaque-yellow: 255, 255, 255, 0

translucent-purple: 127, 255, 0, 255

translucent-cyan: 200, 0, 255, 255

opaque-white: 255, 255, 255, 255

opaque-black: 255, 0, 0, 0


Colors can be created in several ways:

  1. Using the static constants in the Color class, such as: BLACK, BLUE, CYAN, GRAY, GREEN, MAGENTA, RED, WHITE, YELLOW


int carColor = Color.RED;


  1. Using static methods of the Color class, such as, argb


int carColor = Color.argb(255, 255, 0, 0);

int hiliteColor = Color.argb(255, 253, 179, 7);
Remark: In an Android application, it is recommended that you define all colors used in an XML resource file. Having them in one place allows them to be easily changed later, if needed.
Use the tag to define a new color resource. Specify the color value using a # symbol followed by the (optional) alpha value, then the red, green, and blue values using one or two hexadecimal digits (#RGB, #RRGGBB, #ARGB, or #ARRGGBB).




#ff0000

#fffdb307


Colors can be referenced by name in other XML files (e.g., the GUI files). In the Java code they can be accessed using the getResources()method (of the Activity class) as follows:
int someColor = getResources().getColor(R.color.carColor);
The getResources() method returns the ResourceManager class for the current activity, and getColor() asks the manager to look up a color given its resource ID.
2. Drawing
To draw something you need 3 basic components: a Canvas to draw on, a Paint to describe the color and style for drawing, and drawing primitives, such as, rectangles (Rect), text or geometric contours (Path).
The Paint class holds the style, color, and other information needed to draw any graphics, including text, geometric shapes and bitmaps. A Paint object with default settings can be created using the default constructor of the Paint class. These settings can be then changed using public methods, such as, setColor(), setStyle() and setTextSize().
Paint myPreference = new Paint();

myPreference.setColor(Color.BLUE);
Paint yourPreference = new Paint();

yourPreference.setColor(Color.YELLOW);

yourPreference.setTextSize(12.5);
The Canvas class represents a surface on which you draw. In Android, the Canvas is hosted by a View (which in turn is hosted by an Activity). Thus, in order to draw on a canvas you need to create a View to host that canvas and then override its onDraw() method. This method takes only one parameter representing the canvas on which you can draw.
public class SomeApplication extends Activity {

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(new MyFirstView(this));

}

static public class MyFirstView extends View {

public MyFirstView(Context context) {

super(context);

}

protected void onDraw(Canvas canvas) {

//put here drawing commands to draw whatever you want

}

}

}
Remark: Recall that in order to assign a View to an Activity, you need to call the setContentView() method from the onCreate() method of your Activity.
Methods in the Canvas class let you draw lines, rectangles, circle, or other arbitrary graphics. For example:


  • drawCircle(float cx, float cy, float r, Paint p) draws a circle of center (cx, cy) and radius r using the specified paint p.

  • drawLine(float startX, float startY, float stopX, float stopY, Paint p) draws a line segment starting from the (startx, starty) coordinates and ending at the (stopx, stopy) coordinates, using the specified paint p.

  • drawText(String t, float x, float y, Paint p) draws the text t, with origin at (x, y), using the specified paint p.

  • drawRect(float left, float top, float right, float bottom, Paint p) draws a rectangle specified by its opposite corners (left, top) and (right, bottom) using the specified paint p.


public class SomeApplication extends Activity {

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(new MyFirstView(this));

}

static public class MyFirstView extends View {

public MyFirstView(Context context) {

super(context);

}

protected void onDraw(Canvas canvas) {

Paint myPaintbrush = new Paint();

myPaintbrush.setColor(Color.BLUE);

Paint yourPaintbrush = new Paint();

yourPaintbrush.setColor(Color.YELLOW);

yourPaintbrush.setTextSize(12.5);

canvas.drawLine(10,10,20,40,myPaintbrush);

canvas.drawText("Hello!",10,10,yourPaintbrush);

}

}

}
3. Drawing the Sudoko puzzle
First let’s define a few colors in an XML resource file, which we will use for drawing.
res/value/colors.xml

"1.0" encoding="utf-8"?>



"puzzle_background">#ffe6f0ff

"puzzle_hilite">#ffffffff

"puzzle_light">#64c6d4ef

"puzzle_dark">#6456648f

"puzzle_foreground">#ff000000

"puzzle_selected">#64ff8000


Then let’s create the view on whose canvas we can draw the Sudoku puzzle. We call this view PuzzleView and override its draw() method to draw a background for the puzzle, the grid lines that make up the 9x9=81 tiles and then the starting numbers of the game (the givens). Inside PuzzleView, we also need to implement the onSizeChanged() method inherited from the View class. This method is called after the view is created and Android knows how big everything is. We use onSizeChanged()to calculate the size of each tile on the screen (1/9th of the total view width and height). We store this size in two fields: width and height, as they will be needed when drawing the grid lines separating the tiles.
public class PuzzleView extends View {

private float width; // width of one tile

private float height; // height of one tile
public PuzzleView(Context context) {

super(context);

}
@Override



protected void onSizeChanged(int w, int h, int oldw, int oldh) {

width = w / 9f;

height = h / 9f;

//selected tile will be recomputed here



super.onSizeChanged(w, h, oldw, oldh);

}
@Override



protected void onDraw(Canvas canvas) {

// Draw the background

// Draw the grid lines

// Draw the numbers

// Draw the selected tile

}

}


The background is drawn, using the puzzle_background color, as a rectangle covering the entire view. Methods getWidth() and getHeight() inherited from the View class are used to obtain the width and height of the view (in pixels).
// Draw the background

Paint background = new Paint();

background.setColor(getResources().getColor(R.color.puzzle_background));

canvas.drawRect(0, 0, getWidth(), getHeight(), background);


Then we add the grid lines for the board using 3 different colors: a light color (puzzle_light) between each tile, a dark color (puzzle_dark) between the 3x3 blocks, and a highlight color (puzzle_hilite) drawn on the edge of each tile to make them look like they have a little depth. The order in which the lines are drawn it’s important, since lines drawn later will be drawn on top of the earlier lines. The width and height of a tile are used here as calculated by the onSizeChanged()method.
// Define colors for the grid lines

Paint dark = new Paint();

dark.setColor(getResources().getColor(R.color.puzzle_dark));
Paint hilite = new Paint();

hilite.setColor(getResources().getColor(R.color.puzzle_hilite));


Paint light = new Paint();

light.setColor(getResources().getColor(R.color.puzzle_light));


// Draw the minor grid lines

for (int i = 0; i < 9; i++) {

canvas.drawLine(0, i * height, getWidth(), i * height, light);

canvas.drawLine(0, i * height+ 1, getWidth(), i * height+ 1, hilite);

canvas.drawLine(i * width, 0, i * width, getHeight(), light);

canvas.drawLine(i * width + 1, 0, i * width + 1, getHeight(), hilite);

}
// Draw the major grid lines



for (int i = 0; i < 9; i++) {

if (i % 3 != 0)

continue;

canvas.drawLine(0, i * height, getWidth(), i * height, dark);

canvas.drawLine(0, i * height + 1, getWidth(), i * height+ 1, hilite);

canvas.drawLine(i * width, 0, i * width, getHeight(), dark);

canvas.drawLine(i * width + 1, 0, i * width + 1, getHeight(), hilite);

}


Next, we add the puzzle’s starting numbers (the givens) inside the grid lines (using the drawText() method of the Canvas class). The tricky part is getting each number positioned and sized so it goes in the exact center of its tile. To calculate the size of the numbers, we set the text height to ¾ the height of the tile (using the method setTextSize() of the Paint class), and then set the text’s aspect ratio to be the same as the tile’s aspect ratio (using the method setTextScaleX() of the Paint class). We don’t use absolute pixel or point sizes because we want the program to work at any resolution.
To determine the position of each number, we center it in both the x and y dimensions. The x direction is easy – just divide the tile width by 2 and set the text alignment to "CENTER" (using the method setTextAlign() of the Paint class). But for the y direction, we have to adjust the starting position down a little so that the midpoint of the tile will be the midpoint of the number instead of its baseline. We use the graphics library’s FontMetrics class (specifically, the getFontMetrics() method of the Paint class) to tell how much vertical space the letter will take in total. Then we divide that in half to get the adjustment. The ascent and descent fields of the FontMetrics class store information about the vertical space the text occupies above and below the baseline. The next step is to allow the player to enter their guesses for all the blank spaces, but first let’s see where do we get the numbers to write in each tile.
// Define color and style for numbers

Paint foreground = new Paint(Paint.ANTI_ALIAS_FLAG);

foreground.setColor(getResources().getColor(R.color.puzzle_foreground));

foreground.setStyle(Style.FILL);

foreground.setTextSize(height * 0.75f);

foreground.setTextScaleX(width/height);

foreground.setTextAlign(Paint.Align.CENTER);
// Positioning the numbers

// Centering in X: use alignment (and X at midpoint)



float x = width / 2;

// Centering in Y: measure ascent/descent first

FontMetrics fm = foreground.getFontMetrics();

float y = height / 2 - (fm.ascent + fm.descent) / 2;
for (int i = 0; i < 9; i++) {

for (int j = 0; j < 9; j++) {

canvas.drawText(/*numbers to write in each tile*/,

i * width + x, j * height + y, foreground);

}

}



The PuzzleView class defines the Sudoku puzzle. Now, to allow the user to view the puzzle and play the game we need to create an activity that will host the puzzle view. Let’s name that activity Game.
Note: Do not forget to add this activity to your AndroidManifest.xml file.
public class Game extends Activity {

private final String easyPuzzle =

"360000000004230800000004200" +

"070460003820000014500013020" +

"001900000007048300000000045";



private final String mediumPuzzle =

"650000070000506000014000005" +

"007009000002314700000700800" +

"500000630000201000030000097";



private final String hardPuzzle =

"009000000080605020501078000" +

"000000700706040102004000000" +

"000720903090301080000000600";


private int puzzle[] = new int[9 * 9];

private PuzzleView puzzleView;
@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
int diff = getIntent().getIntExtra(KEY_DIFFICULTY, 2);

puzzle = getPuzzle(diff);


puzzleView = new PuzzleView(this);

setContentView(puzzleView);

puzzleView.requestFocus();

}

/** Given a difficulty level, select a puzzle */



private int[] getPuzzle(int diff) {

String puz;



switch (diff) {

case 0: puz = hardPuzzle; break;

case 1: puz = mediumPuzzle; break;

case 2:

default: puz = easyPuzzle; break;

}

return fromPuzzleString(puz);

}

/** Convert a puzzle string into an array */



static protected int[] fromPuzzleString(String puzzleString) {

int[] puz = new int[puzzleString.length()];

for (int i = 0; i < puz.length; i++) {

puz[i] = puzzleString.charAt(i) - '0';

}

return puz;

}
/** Change the tile at the given coordinates */



private void setTile(int x, int y, int value) {

puzzle[y * 9 + x] = value;

}

/** Return a string for the tile at the given coordinates */



protected String getTileString(int x, int y) {

int v = puzzle[y * 9 + x];

if (v == 0)

return "";

else

return String.valueOf(v);

}

}


The static fields easyPuzzle, mediumPuzzle and hardPuzzle, in our Game class, store our hard-coded Sudoku puzzles for easy, medium and hard difficulty levels respectively. The field puzzle is a 9x9 array of integers that will store the numbers in the 9x9 Sudoku puzzle. Originally, this array is populated with the given numbers by the method getPuzzle(). This method simply takes a difficulty level, selects the corresponding puzzle string (easyPuzzle, mediumPuzzle or hardPuzzle) and converts it into a 9x9 array of integers by using the helper method fromPuzzleString(). This action is performed in the onCreate() method of the Game activity, after the value of the difficulty level is fetched from the intent invoking this activity.
Method setTile() will be used later to keep the puzzle field up-to-date with the numbers entered by the player. Method getTileString() is the opposite of setTile() and will also be used later. It returns a string representation of the number at specified coordinates in the puzzle array. Note that a value of 0 in the puzzle array, represents an empty tile, thus method getTileString() will return an empty string for such value.
Once we know the puzzle’s numbers we can create an instance of the PuzzleView class and assign it to the Game activity (using the setContentView() method). But, class PuzzleView needs access to the array puzzle in order to know what to draw in each tile. To allow access to the members of the Game class we add a reference to this class as one of the fields of the PuzzleView class. Then, when drawing the numbers in each tile, we can use the getTileString()method from the Game class.
public class PuzzleView extends View {

private float width; // width of one tile

private float height; // height of one tile
private final Game game;

public PuzzleView(Context context) {

super(context);

game = (Game) context;

}


...
@Override

protected void onDraw(Canvas canvas) {

...


// Centering the numbers in X: use alignment (and X at midpoint)

float x = width / 2;

// Centering the numbers in Y: measure ascent/descent first

FontMetrics fm = foreground.getFontMetrics();

float y = height / 2 - (fm.ascent + fm.descent) / 2;
// Draw the numbers

for (int i = 0; i < 9; i++) {

for (int j = 0; j < 9; j++) {

canvas.drawText(game.getTileString(i, j),

i * width + x, j * height + y, foreground);

}

}



}
4. Handling User Input into the Puzzle
Now that the Sudoku puzzle and the given numbers are finally drawn, let’s see how do we allow the player to enter their guesses for all the empty tiles. Remember that Android phones come in many shapes and sizes and have a variety of input methods. They might have a keyboard, a D-pad, a touch screen, a trackball, or some combination of these. A good Android program, therefore, needs to be ready to support whatever input hardware is available, just like it needs to be ready to support any screen resolution.
For a touch capable device, once the user touches the screen, the device will enter touch mode. Any time a user hits a directional key, such as a D-pad direction, the view device will exit touch mode. To allow user input in the puzzle view we must set the view as focusable both when in touch mode and when not in touch mode. This is done in the PuzzleView constructor.
public class PuzzleView extends View {

...
public PuzzleView(Context context) {



super(context);

game = (Game) context;



setFocusable(true);

setFocusableInTouchMode(true);

}


...

}
4.1. Defining and Moving the Selected Tile


First, we’re going to implement a little cursor that shows the player which tile is currently selected. The selected tile is the one that will be modified when the player enters a number. As the selected tile needs to change throughout the game, we need to add two new fields to the PuzzleView class, selX and selY, that will hold the coordinates (1..9) of the selected tile.
The selected tile will be drawn in the onDraw() method of the PuzzleView class, as a rectangle having the color puzzle_selected. The coordinates of this rectangle are computed by the helper method getRect() based on the values of selX, selY, width and height. Thus, this rectangle is first computed in the onSizeChanged() method of the PuzzleView class (after the width and the height of each tile are calculated) and will be updated according to user input. Although the coordinates of the selection can be computed whenever needed, the data field selRect of type Rect is used to store them to reduce computation.
public class PuzzleView extends View {

private float width; // width of one tile

private float height; // height of one tile

private int selX; // X index of selection

private int selY; // Y index of selection

private final Rect selRect = new Rect();

...
@Override

protected void onSizeChanged(int w, int h, int oldw, int oldh) {

width = w / 9f;

height = h / 9f;

getRect(selX, selY, selRect);

ocusable(true) in the PuzzleView constructor. the ly those areas that change.w() again for you. The dirty rectangles become super.onSizeChanged(w, h, oldw, oldh);

}

@Override



protected void onDraw(Canvas canvas) {

// Draw the background ...

// Draw the grid lines ...

// Draw the numbers ...


// Draw the selected tile

Paint selected = new Paint();

selected.setColor(getResources().getColor(R.color.puzzle_selected));

canvas.drawRect(selRect, selected);
}

private void getRect(int x, int y, Rect rect) {

rect.set((int) (x * width), (int) (y * height), (int) (x

* width + width), (int) (y * height + height));

}

}

Next we provide a way to move the selection by overriding the onKeyDown() method (in the PuzzleView class). If the user has a directional pad (D-pad) and presses the up, down, left or right button, we call helper method select() to modify selX and selY according the selected direction.


Inside the select() method, we calculate the new x and y coordinate of the selection and then call getRect() again to calculate the new selection rectangle. Notice the two calls to invalidate(). The first one tells Android that the area covered by the old selection rectangle needs to be redrawn (to basically erase the selection). The second one says that the new selection area needs to be redrawn too.
Note that we don’t actually draw anything in method select(). This is an important point: never call any drawing functions except in the onDraw() method. Instead, you use the invalidate() method to mark rectangles as dirty. The window manager will combine all the dirty rectangles at some point in the future and call onDraw() again for you. Thus, screen updates are optimized to only those areas that change.
public class PuzzleView extends View {

private float width; // width of one tile

private float height; // height of one tile

private int selX; // X index of selection

private int selY; // Y index of selection

private final Rect selRect = new Rect();

...


@Override

public boolean onKeyDown(int keyCode, KeyEvent event) {

switch (keyCode) {

case KeyEvent.KEYCODE_DPAD_UP: select(selX, selY - 1); break;

case KeyEvent.KEYCODE_DPAD_DOWN: select(selX, selY + 1); break;

case KeyEvent.KEYCODE_DPAD_LEFT: select(selX - 1, selY); break;

case KeyEvent.KEYCODE_DPAD_RIGHT: select(selX + 1, selY); break;

the color classanvas classesg invalid iialogbe computed whenever needed, the default: return super.onKeyDown(keyCode, event);

}

return true;

}
private void select(int x, int y) {

invalidate(selRect);

selX = Math.min(Math.max(x, 0), 8);

selY = Math.min(Math.max(y, 0), 8);

getRect(selX, selY, selRect);

invalidate(selRect);

}

}
4.2. Entering Numbers in the Selected Tile


4.2.1. To handle keyboard input, we just add a few more cases on the onKeyDown() method, for the numbers 0 through 9 (0 or space means erase the number from the selected tile).
public class PuzzleView extends View {

...


@Override

public boolean onKeyDown(int keyCode, KeyEvent event) {

switch (keyCode) {

case KeyEvent.KEYCODE_DPAD_UP: …

case KeyEvent.KEYCODE_DPAD_DOWN: …

case KeyEvent.KEYCODE_DPAD_LEFT: …

case KeyEvent.KEYCODE_DPAD_RIGHT: …

case KeyEvent.KEYCODE_0:

case KeyEvent.KEYCODE_SPACE: setSelectedTile(0); break;

case KeyEvent.KEYCODE_1: setSelectedTile(1); break;

case KeyEvent.KEYCODE_2: setSelectedTile(2); break;

case KeyEvent.KEYCODE_3: setSelectedTile(3); break;

case KeyEvent.KEYCODE_4: setSelectedTile(4); break;

case KeyEvent.KEYCODE_5: setSelectedTile(5); break;

case KeyEvent.KEYCODE_6: setSelectedTile(6); break;

case KeyEvent.KEYCODE_7: setSelectedTile(7); break;

case KeyEvent.KEYCODE_8: setSelectedTile(8); break;

case KeyEvent.KEYCODE_9: setSelectedTile(9); break;

default: return super.onKeyDown(keyCode, event);

}

return true;

}
public void setSelectedTile(int tile) {

game.setTile(selX, selY, tile));

invalidate(selRect);

}

}
The setSelectedTile() method changes a tile, by calling the setTile() method from the Game class to update the puzzle field, and then calling invalidate() to mark that tile as dirty, for redraw.


4.2.2. To support the D-pad, we check for Enter or center D-pad button in onKeyDown() and have it pop up a keypad that lets the user select which number to place. The keypad is handy for phones that don’t have keyboards.
public class PuzzleView extends View {
private int selX; // X index of selection

private int selY; // Y index of selection

private final Game game;

...


@Override

public boolean onKeyDown(int keyCode, KeyEvent event) {

switch (keyCode) {

case KeyEvent.KEYCODE_DPAD_UP: …

case KeyEvent.KEYCODE_DPAD_DOWN:…

case KeyEvent.KEYCODE_DPAD_LEFT:…

case KeyEvent.KEYCODE_DPAD_RIGHT:…

case KeyEvent.KEYCODE_0:

case KeyEvent.KEYCODE_SPACE: setSelectedTile(0); break;

case KeyEvent.KEYCODE_1: setSelectedTile(1); break;

case KeyEvent.KEYCODE_2: setSelectedTile(2); break;

case KeyEvent.KEYCODE_3: setSelectedTile(3); break;

case KeyEvent.KEYCODE_4: setSelectedTile(4); break;

case KeyEvent.KEYCODE_5: setSelectedTile(5); break;

case KeyEvent.KEYCODE_6: setSelectedTile(6); break;

case KeyEvent.KEYCODE_7: setSelectedTile(7); break;

case KeyEvent.KEYCODE_8: setSelectedTile(8); break;

case KeyEvent.KEYCODE_9: setSelectedTile(9); break;

case KeyEvent.KEYCODE_ENTER:

case KeyEvent.KEYCODE_DPAD_CENTER: game.showKeypad(selX, selY); break;

default:

return super.onKeyDown(keyCode, event);

}

return true;

}

}
4.2.3. To support touch screens, we override the onTouchEvent() method and show the same keypad as in the previous case (4.2.2). Remember that method select() is used to keep the fields selX and selY (representing the selected tile) up-to-date according to player’s input (thus, it is called here to update the selected tile).



public class PuzzleView extends View {
private float width; // width of one tile

private float height; // height of one tile

private int selX; // X index of selection

private int selY; // Y index of selection

...


@Override

public boolean onTouchEvent(MotionEvent event) {

if (event.getAction() != MotionEvent.ACTION_DOWN)

return super.onTouchEvent(event);

select((int) (event.getX() / width), (int) (event.getY() / height));

game.showKeypad(selX, selY);

return true;

}

}
Now, let’s implement the keypad as a dialog that appears on top of the puzzle, displays a grid of the numbers 1 through 9 and returns the number selected by the user. Method showKeypad() creates the keypad dialog.


public class Game extends Activity {



protected void showKeypad (int x, int y) {

Dialog v = new Keypad(this, puzzleView);

v.show();

}

}
The Keypad class will extend the Android Dialog class. But first let’s define the user interface layout for the keypad in XML.


res/layout/keypad.xml

xmlns:android="http://schemas.android.com/apk/res/android"

android:id="@+id/keypad"

android:orientation="vertical"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:stretchColumns="*">
































public class Keypad extends Dialog {

private final View keys[] = new View[9];

private View keypad;

private final PuzzleView puzzleView;
public Keypad(Context context, PuzzleView puzzleView) {

super(context);

this.puzzleView = puzzleView;

}
@Override



protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setTitle(R.string.keypad_title);

setContentView(R.layout.keypad);

findViews();

setListeners();

}

private void findViews() {

keypad = findViewById(R.id.keypad);

keys[0] = findViewById(R.id.keypad_1);

keys[1] = findViewById(R.id.keypad_2);

keys[2] = findViewById(R.id.keypad_3);

keys[3] = findViewById(R.id.keypad_4);

keys[4] = findViewById(R.id.keypad_5);

keys[5] = findViewById(R.id.keypad_6);

keys[6] = findViewById(R.id.keypad_7);

keys[7] = findViewById(R.id.keypad_8);

keys[8] = findViewById(R.id.keypad_9);

}

private void setListeners() {

for (int i = 0; i < keys.length; i++) {

final int t = i + 1;

keys[i].setOnClickListener(new View.OnClickListener(){



public void onClick(View v) {

puzzleView.setSelectedTile(t);

dismiss();

}});


}

keypad.setOnClickListener(new View.OnClickListener(){



public void onClick(View v) {

puzzleView.setSelectedTile(0);

dismiss();

}});


}

}
The findViews() method fetches and saves the views for all the keypad keys and the main keypad window, in the two fields keys and keypad.


Then method setListeners() loops through all the keypad keys and sets a listener for each one. Thus, when the player selects/clicks one of them, the number on that key is returned to the calling activity and written in the selected tile using method setSelectedTile() (from the PuzzleView class). The dismiss call terminates the Keypad dialog box.
To allow a player to erase a tile, we also set a listener for the main keypad window. Thus, when the player clicks on it (but not on a key), then method setSelectedTile() is called with argument zero, indicating the tile should be erased.
5. Game Extensions
5.1. Check for invalid user input and reject it
Now, let’s help the player by allowing him/her to select a number for a certain tile only if it does not violate the game conditions (i.e., a number should not appear more than once in a row, column, or 3x3 group). In order to do that, we can either:

  1. compute for each user input if it is valid or not

  2. or keep a list of valid (or invalid) numbers for each tile and update these lists after each user input

Let’s choose option (2) and add the field used in the Game class to keep track of used (invalid) numbers for each tile.


public class Game extends Activity {

...
private int puzzle[] = new int[9 * 9];



private final int used[][][] = new int[9][9][];
private PuzzleView puzzleView;
@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

int diff = getIntent().getIntExtra(KEY_DIFFICULTY, 2);

puzzle = getPuzzle(diff);



calculateUsedTiles();
puzzleView = new PuzzleView(this);

setContentView(puzzleView);

puzzleView.requestFocus();

}

private void calculateUsedTiles() {



for (int x = 0; x < 9; x++) {

for (int y = 0; y < 9; y++) {

used[x][y] = calculateUsedTiles(x, y);

}

}

}
protected int[] getUsedTiles(int x, int y) {

return used[x][y];

}

}
Method calculateUsedTiles() will be updating used and method getUsedTiles() will be used to retrieve invalid numbers for a specified tile.


The algorithm for calculating the used numbers for a given tile (x,y) is provided below:
private int[] calculateUsedTiles(int x, int y) {

int c[] = new int[9];

// horizontal



for (int i = 0; i < 9; i++) {

if (i == y)

continue;

int t = puzzle[i * 9 + x];

if (t != 0)

c[t - 1] = t;

}

// vertical



for (int i = 0; i < 9; i++) {

if (i == x)

continue;

int t = puzzle[y * 9 + i];

if (t != 0)

c[t - 1] = t;

}

// same cell block



int startx = (x / 3) * 3;

int starty = (y / 3) * 3;

for (int i = startx; i < startx + 3; i++) {

for (int j = starty; j < starty + 3; j++) {

if (i == x && j == y)

continue;

int t = puzzle[j * 9 + i];

if (t != 0)

c[t - 1] = t;

}

}

// compress



int nused = 0;

for (int t : c) {

if (t != 0)

nused++;


}

int c1[] = new int[nused];

nused = 0;



for (int t : c) {

if (t != 0)

c1[nused++] = t;

}

return c1;

}
Now, before we assign a new value to a tile we must first check if it is valid (not used). So, let’s modify the setSelectedTile() method, to call a new method setTileIfValid() instead of setTile().


public void setSelectedTile(int tile) {

game.setTileIfValid(selX, selY, tile));

invalidate(selRect);

}
Method setTileIfValid() will first check if the number to be assigned to a tile is valid and then will call method setTile() to assign it.


public class Game extends Activity {



protected boolean setTileIfValid(int x, int y, int value) {



int tiles[] = getUsedTiles(x, y);

if (value != 0) {

for (int tile : tiles) {

if (tile == value)

return false;

}

}



setTile(x, y, value);

calculateUsedTiles();



return true;

}

}


Note that the field used must be updated every time the value of a tile changes. Therefore, we need to call the calculateUsedTiles() method after the setTile() method.
5.2. Shake the screen for invalid user input
Let’s also notify the user that a number is invalid by making the screen wiggle back and forth when they enter one. To do that we rewrite the method setSelectedTile() as follows:

public void setSelectedTile(int tile) {

if (game.setTileIfValid(selX, selY, tile)) {

invalidate(selRect);

} else {

// Number is not valid for this tile

startAnimation(AnimationUtils.loadAnimation(game,R.anim.shake));

}

}


Method startAnimation() will load and run a resource called R.anim.shake, defined in res/anim/shake.xml, that shakes the screen for 1 second by 10 pixels from side to side.

res/anim/shake.xml

?xml version="1.0" encoding="utf-8"?>



xmlns:android="http://schemas.android.com/apk/res/android"

android:fromXDelta="0"

android:toXDelta="10"

android:duration="1000"

android:interpolator="@anim/cycle_7" />


The number of times to run the animation and the velocity and acceleration of the animation are controlled by an animation interpolator defined in XML.
res/anim/cycle_7.xml

"1.0" encoding="utf-8"?>

xmlns:android="http://schemas.android.com/apk/res/android"

android:cycles="7" />
5.3. Display only valid inputs in the keypad dialog
Now, let’s modify the keypad dialog such that it displays only numbers that are valid for the selected tile. To achieve this we need to pass into the Keypad class the used numbers for the selected tile. We add the field useds to the Keypad class for this purpose. Then we make the keys corresponding to invalid numbers as invisible.
public class Keypad extends Dialog {
private final View keys[] = new View[9];

private View keypad;

private final PuzzleView puzzleView;

private final int useds[];
public Keypad(Context context, PuzzleView puzzleView, int useds[]) {

super(context);

this.puzzleView = puzzleView;

this.useds = useds;

}
@Override



protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
setTitle(R.string.keypad_title);

setContentView(R.layout.keypad);

findViews();

for (int element : useds) {

if (element != 0)

keys[element - 1].setVisibility(View.INVISIBLE);

}

setListeners();

}



}


Don’t forget to modify the showKeypad() method to pass the used umbers as the third argument when creating the keypad dialog.
public class Game extends Activity {



protected void showKeypad (int x, int y) {

Dialog v = new Keypad(this, puzzleView, getUsedTiles(x, y));

v.show();



}

}


Download 257.75 Kb.

Share with your friends:




The database is protected by copyright ©ininet.org 2024
send message

    Main page