package com.charlesmadere.android.classygames.views; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; import android.view.ViewGroup; import com.charlesmadere.android.classygames.R; import com.charlesmadere.android.classygames.utilities.Utilities; /** * A custom ViewGroup that creates an entire game board. Performs sizing checks * so that no individual PositionView on the board is unsquare or non-uniform * in shape. Handles rotation changes and will dynamically resize itself to * continue to maintain good dimensions despite the orientation flip. */ public final class BoardView extends ViewGroup { private final static String LOG_TAG = Utilities.LOG_TAG + " - BoardView"; private final static int COLUMNS_DEFAULT = 2; private final static int ROWS_DEFAULT = 2; /** * The background that will be given to views whose X and Y coordinates add * up to give an odd number sum. So that means coordinates like (1, 0), * (5, 2), and (3, 2) will be bright. This is because 1 + 0 = 1 (an odd * number), 5 + 2 = 7 (an odd number), and 3 + 2 = 5 (an odd number). */ private Drawable brightBackground; private Drawable brightBackgroundSelected; /** * The background that will be given to views whose X and Y coordinates add * up to give an even number sum. What this means is that coordinates like * (0, 0), (3, 5), and (7, 1) will be dark. This is because 0 + 0 = 0 (an * even number), 3 + 5 = 8 (an even number), and 7 + 1 = 8 (an even * number). */ private Drawable darkBackground; private Drawable darkBackgroundSelected; /** * The total number of columns that this board has. */ private byte columns; /** * The total number of rows that this board has. */ private byte rows; /** * The inner-layout padding to be applied to the board's PositionViews. */ private float padding; /** * A two-dimensional array containing all of this board's positions. It's * meant to be accessed like [x][y]. In a board with 4 columns and 4 rows, * coordinate (0, 0) is at the bottom left, coordinate (3, 3) is at the * top right, coordinate (3, 0) is at the bottom right, and coordinate * (0, 3) is at the top left. */ private PositionView positionViews[][]; public BoardView(final Context context, final AttributeSet attrs) { super(context, attrs); parseAttributes(attrs); createPositions(); } public BoardView(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); parseAttributes(attrs); createPositions(); } /** * Interested in learning what this method is about? For starters, you * should read the documentation on this method. I promise it's not too * hairy! * * https://developer.android.com/reference/android/view/ViewGroup.html#onLayout(boolean, int, int, int, int) * * But quick-and-rough, this method does the actual placement of this * ViewGroup's many view children onto the screen. */ @Override protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b) { final PositionView position = getPosition(0, 0); final int width = position.getMeasuredWidth(); final int height = position.getMeasuredHeight(); for (int x = 0; x < getLengthHorizontal(); ++x) { final int left = width * x; final int right = left + width; for (int y = 0; y < getLengthVertical(); ++y) { final int top = height * (getLengthVertical() - (y + 1)); final int bottom = top + height; final PositionView positionView = getPosition(x, y); positionView.layout(left, top, right, bottom); } } } /** * Again, check them docs: * https://developer.android.com/reference/android/view/View.html#onMeasure(int, int) * * Finds out the size that each child view in this board should be. */ @Override @SuppressWarnings("SuspiciousNameCombination") protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { final int height; final int width; if (isOrientationLandscape()) { height = MeasureSpec.getSize(heightMeasureSpec); width = height; } else { width = MeasureSpec.getSize(widthMeasureSpec); height = width; } setMeasuredDimension(width, height); // We use Math.ceil() here to prevent rounding issues. If the width and // height measured out to be 50.5px, then that means that some sides of // the board wouldn't sit touching the walls of its container. So it'd // look funky. However, this also means that we may have a tiny bit of // overflow with some positions going a tiny bit off screen. // Unfortunately, there's not much that we can do about that one. final int widthSize = (int) Math.ceil((double) width / (double) columns); final int heightSize = (int) Math.ceil((double) height / (double) rows); final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); for (byte x = 0; x < getLengthHorizontal(); ++x) { for (byte y = 0; y < getLengthVertical(); ++y) { final PositionView positionView = getPosition(x, y); positionView.measure(widthSpec, heightSpec); } } } @Override public boolean shouldDelayChildPressedState() { return false; } public byte getLengthHorizontal() { return columns; } public byte getLengthVertical() { return rows; } public PositionView getPosition(final byte x, final byte y) { return positionViews[x][y]; } public PositionView getPosition(final int x, final int y) { return getPosition((byte) x, (byte) y); } public void setAllPositionViewOnClickListeners(final OnClickListener onClickListener) { for (byte x = 0; x < getLengthHorizontal(); ++x) { for (byte y = 0; y < getLengthVertical(); ++y) { final PositionView positionView = getPosition(x, y); positionView.setOnClickListener(onClickListener); } } } /** * Initializes the View objects for all of this board's children. */ private void createPositions() { positionViews = new PositionView[getLengthHorizontal()][getLengthVertical()]; final Context context = getContext(); for (byte x = 0; x < getLengthHorizontal(); ++x) { for (byte y = 0; y < getLengthVertical(); ++y) { final PositionView positionView = new PositionView(context, x, y, padding, brightBackground, darkBackground, brightBackgroundSelected, darkBackgroundSelected); positionViews[x][y] = positionView; addView(positionView); } } } /** * @return * Returns true if the device's current orientation is landscape. */ private boolean isOrientationLandscape() { return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; } /** * Reads the AttributeSet object given to us in the constructor and parses * the data out of it. */ private void parseAttributes(final AttributeSet attrs) { final Resources.Theme theme = getContext().getTheme(); final TypedArray attributes = theme.obtainStyledAttributes(attrs, R.styleable.BoardView, 0, 0); try { brightBackground = attributes.getDrawable(R.styleable.BoardView_bright_background); darkBackground = attributes.getDrawable(R.styleable.BoardView_dark_background); brightBackgroundSelected = attributes.getDrawable(R.styleable.BoardView_bright_background_selected); darkBackgroundSelected = attributes.getDrawable(R.styleable.BoardView_dark_background_selected); columns = (byte) attributes.getInt(R.styleable.BoardView_columns, COLUMNS_DEFAULT); rows = (byte) attributes.getInt(R.styleable.BoardView_rows, ROWS_DEFAULT); padding = attributes.getDimension(R.styleable.BoardView_position_padding, PositionView.PADDING_DEFAULT); } catch (final Exception e) { Log.e(LOG_TAG, "Exception when reading attributes!", e); brightBackground = null; darkBackground = null; columns = COLUMNS_DEFAULT; rows = ROWS_DEFAULT; padding = PositionView.PADDING_DEFAULT; } finally { attributes.recycle(); } } }