package de.nisble.droidsweeper.gui.grid;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
import de.nisble.droidsweeper.R;
import de.nisble.droidsweeper.config.GameConfig;
import de.nisble.droidsweeper.game.Position;
import de.nisble.droidsweeper.game.jni.FieldStatus;
import de.nisble.droidsweeper.game.jni.MineSweeperMatrix;
import de.nisble.droidsweeper.utilities.LogDog;
/** This class abstracts the logic that is needed to correctly arrange the
* {@link FieldView field widgets} in a table (called the game grid).
* It is an extension of the ViewGroup RelativeLayout thats attributes can be
* used in a XML-File to specify the LayoutParms of this view. Internally a
* TableLayout is used to arrange the {@link FieldView field widgets}. Use the
* {@link #update(GameConfig, FieldWidgetConnector)} method to build a new grid.
* The field widgets of the last grid are recycled if possible.<br>
* For the user to be able to connect the field widgets to different classes
* that can update the {@link FieldStatus} of the widgets, a
* {@link FieldWidgetConnector} must be passed to
* {@link #update(GameConfig, FieldWidgetConnector)}. From inside this callback
* the user should connect the field widget to e.g.
* {@link MineSweeperMatrix#setFieldListener(de.nisble.droidsweeper.game.jni.FieldListener)}
* or another instance that should update the status of the field widgets.<br>
* Also, the user should pass an implementation of {@link FieldClickListener} to
* {@link #setFieldClickListener(FieldClickListener)}. Clicks on the field
* widgets are passed to the methods of that interface.
* @author Moritz Nisblé moritz.nisble@gmx.de */
public class GameGridView extends RelativeLayout {
private static final String CLASSNAME = GameGridView.class.getSimpleName();
/** Interface that should be passed to
* {@link GameGridView#update(GameConfig, FieldWidgetConnector)}.
* @author Moritz Nisblé moritz.nisble@gmx.de */
public interface FieldWidgetConnector {
/** <a href=
* "http://steve-yegge.blogspot.de/2006/03/execution-in-kingdom-of-nouns.html"
* >Execution in the Kingdom of Nouns</a> */
void connect(FieldView field);
}
/** Interface that is invoked on clicks to the {@link FieldView field
* widgets}.
* @author Moritz Nisblé moritz.nisble@gmx.de */
public interface FieldClickListener {
/** Invoked on click to a {@link FieldView}
* @param field The {@link FieldView} that was clicked. */
void onClick(FieldView field);
/** Invoked on long click to a {@link FieldView}.
* @param field The {@link FieldView} that was long clicked.
* @return true if the callback consumed the long click, false
* otherwise. */
boolean onLongClick(FieldView field);
}
private class GridLayout extends TableLayout {
private GridLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/* Ensure that table layout has the minimum size (wraps its
* children). */
// Get count of children
int rowCount = getChildCount();
int columnCount = (rowCount > 0) ? ((TableRow) getChildAt(0)).getChildCount() : 0;
/* Calculate the maximum allowed side length of the fields by
* dividing the maximum allowed size passed by parent for both
* directions by the count of widgets in the corresponding
* direction.
* Finally take the minimum of both sizes to ensure that the
* complete matrix fits to into the bounds passed by parent while
* single views stays quadratic. */
int maxSideLength = Math.min(MeasureSpec.getSize(heightMeasureSpec) / ((rowCount > 0) ? rowCount : 1),
MeasureSpec.getSize(widthMeasureSpec) / ((columnCount > 0) ? columnCount : 1));
/* Take this maximum allowed side length for each field, multiply it
* with the count of fields in each direction and pass it back to
* parent. */
super.onMeasure(MeasureSpec.makeMeasureSpec(maxSideLength * ((columnCount > 0) ? columnCount : 1),
MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(maxSideLength
* ((rowCount > 0) ? rowCount : 1), MeasureSpec.UNSPECIFIED));
}
}
private final GridLayout mGrid;
private TextView mOverlayText = null;
private FieldClickListener mFieldClickListener = null;
/** Constructor. */
public GameGridView(Context context) {
this(context, null);
}
/** Constructor. */
public GameGridView(Context context, AttributeSet attrs) {
super(context, attrs);
RelativeLayout.LayoutParams gridLayoutParams = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
gridLayoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
mGrid = new GridLayout(context, attrs);
mGrid.setLayoutParams(gridLayoutParams);
mGrid.setShrinkAllColumns(true);
addView(mGrid);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.GameGridView, 0, 0);
try {
if (a.getBoolean(R.styleable.GameGridView_hasOverlay, false)) {
mOverlayText = new TextView(context);
RelativeLayout.LayoutParams overlayLayoutParams = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
overlayLayoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
mOverlayText.setLayoutParams(overlayLayoutParams);
mOverlayText.setClickable(false);
mOverlayText.setGravity(Gravity.CENTER);
mOverlayText.setTextSize(a.getDimension(R.styleable.GameGridView_overlayTextSize, 26));
mOverlayText.setTextColor(a.getColor(R.styleable.GameGridView_overlayTextColor, Color.RED));
mOverlayText.setBackgroundColor(a.getColor(R.styleable.GameGridView_overlayBackgroundColor,
Color.LTGRAY));
mOverlayText.setVisibility(INVISIBLE);
addView(mOverlayText);
}
} finally {
a.recycle();
}
bringChildToFront(mGrid);
}
/** Register a {@link FieldClickListener} thats methods are invoked on clicks
* to the {@link FieldView field widgets}.
* @param l An instance of a {@link FieldClickListener}. */
public void setFieldClickListener(FieldClickListener l) {
mFieldClickListener = l;
}
/** Update the game grid to the dimensions passed in the given
* {@link GameConfig}. The dimensions of the grid are adjusted to the
* orientation of the device.
* @param config The new {@link GameConfig}.
* @param connector A callback that is responsible for connecting the
* {@link FieldView field widgets} to a class that is able
* to update its {@link FieldStatus status} (e.g.
* {@link MineSweeperMatrix#setFieldListener(de.nisble.droidsweeper.game.jni.FieldListener)}
* ). */
public void update(GameConfig config, FieldWidgetConnector connector) {
// Get an orientation corrected version of the config
GameConfig cv = config.adjustOrientation(getContext());
// Adjust row count while recycling rows already present
if (mGrid.getChildCount() < cv.Y) {
LogDog.d(CLASSNAME, "Adding FieldView rows: " + mGrid.getChildCount() + " -> " + cv.Y);
// Add left rows
while (mGrid.getChildCount() < cv.Y) {
TableRow row = new TableRow(getContext());
row.setLayoutParams(new GameGridView.LayoutParams(LayoutParams.WRAP_CONTENT, 0));
mGrid.addView(row);
}
} else if (mGrid.getChildCount() > cv.Y) {
LogDog.d(CLASSNAME, "Removing FieldView rows: " + mGrid.getChildCount() + " -> " + cv.Y);
// Remove redundant rows
mGrid.removeViews(cv.Y, mGrid.getChildCount() - cv.Y);
}
// Adjusting column count (FieldViewS) in each row while recycling
// already present FieldViewS
for (int y = 0; y < mGrid.getChildCount(); ++y) {
TableRow row = (TableRow) mGrid.getChildAt(y);
if (row.getChildCount() < cv.X) {
LogDog.d(CLASSNAME, "Adding FieldViewS: " + row.getChildCount() + " -> " + cv.X);
// Add left columns to each row
while (row.getChildCount() < cv.X) {
FieldView field = new FieldView(getContext());
field.setLayoutParams(new TableRow.LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT));
field.setAdjustViewBounds(true);
field.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
field.setLongClickable(true);
field.setOnClickListener(mOnFieldClick);
field.setOnLongClickListener(mOnFieldLongClick);
row.addView(field);
}
} else if (row.getChildCount() > cv.X) {
LogDog.d(CLASSNAME, "Removing FieldViewS: " + row.getChildCount() + " -> " + cv.X);
// Remove redundant columns from each row
row.removeViews(cv.X, row.getChildCount() - cv.X);
}
}
/* Iterate over all FieldViewS in the TableLayout, inform the views
* about its Position in the Matrix and let the caller connect the view
* to any module that controls the status of the view. */
for (int y = 0; y < cv.Y; ++y) {
TableRow row = (TableRow) mGrid.getChildAt(y);
for (int x = 0; x < cv.X; ++x) {
FieldView field = (FieldView) row.getChildAt(x);
/* NOTE: Game/libmsm and Replay only know the PORTRAIT layout.
* Only the view adapts to the orientation of the device!
* So if the device is in LANDSCAPE mode, the internal Position
* of the FieldViewS differs from its position in the
* TableLayout. Therefore we have to correct this Position back
* to PORTRAIT, if the Grind was built for LANDSCAPE.
* Because the game logic gets the coordinates of each field by
* calling FieldListener.getPosition(), this is the only place
* where we have to consider this. */
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE)
/* Transpose or rotate?
* This is the code for rotate right by 90 degree:
* field.reset(new Position(y, config.Y - 1 - x)); */
field.reset(new Position(y, x));
else
field.reset(new Position(x, y));
/* Let the caller connect the field to any module that is able
* to control the FieldStatus. */
connector.connect(field);
}
}
invalidate();
requestLayout();
}
/** Bring an overlay with the given text in front.
* @param text The text to show. */
public void showOverlay(String text) {
if (mOverlayText != null) {
mOverlayText.setText(text);
mOverlayText.setVisibility(VISIBLE);
mOverlayText.bringToFront();
invalidate();
requestLayout();
}
}
/** Hide the overlay and bring the game grid in front. */
public void hideOverlay() {
if (mOverlayText != null) {
mOverlayText.setVisibility(INVISIBLE);
mGrid.bringToFront();
invalidate();
requestLayout();
}
}
private final OnClickListener mOnFieldClick = new OnClickListener() {
@Override
public void onClick(View v) {
if (mFieldClickListener != null) {
mFieldClickListener.onClick((FieldView) v);
} else {
LogDog.w(CLASSNAME, "Register a FieldClickListener to get informed about clicks on FieldViewS");
}
}
};
private final OnLongClickListener mOnFieldLongClick = new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if (mFieldClickListener != null) {
return mFieldClickListener.onLongClick((FieldView) v);
} else {
LogDog.w(CLASSNAME, "Register a FieldClickListener to get informed about clicks on FieldViewS");
return false;
}
}
};
}