Java tutorial
// Copyright (c) 2014-2015 Akop Karapetyan // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. package org.akop.crosswords.view; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; import android.os.AsyncTask; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.v4.view.ViewCompat; import android.text.InputType; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.Scroller; import org.akop.crosswords.Crosswords; import org.akop.crosswords.R; import org.akop.crosswords.view.inputmethod.CrosswordInputConnection; import org.akop.crosswords.widget.Zoomer; import org.akop.xross.object.Crossword; public class CrosswordView extends View implements View.OnKeyListener { public interface OnSelectionChangeListener { void onWordDeselected(CrosswordView view); void onWordSelectionChanged(CrosswordView view, Crossword.Word word); } public interface OnLongPressListener { void onCellLongPressed(CrosswordView view, Crossword.Word word, int cell); } private static final Cell[][] EMPTY_CELLS = new Cell[0][0]; private static final char[] EMPTY_CHARS = new char[0]; private static final int NAVIGATION_SCROLL_DURATION_MS = 500; private static final int MAX_BITMAP_DIMENSION = 2048; // largest allowed bitmap width or height private static final float MARKER_TRIANGLE_LENGTH_FRACTION = 0.3f; private static final float FLING_VELOCITY_DOWNSCALE = 2.0f; private static final float CELL_SIZE = 10; private static final float NUMBER_TEXT_PADDING = 2; private static final float NUMBER_TEXT_SIZE = 5; private static final float ANSWER_TEXT_PADDING = 2; private static final float ANSWER_TEXT_SIZE = 10; private static final float NUMBER_TEXT_STROKE_WIDTH = 1; private static int NORMAL_CELL_FILL_COLOR = Color.parseColor("#ffffff"); private static int CHEATED_CELL_FILL_COLOR = Color.parseColor("#ff8b85"); private static int MISTAKE_CELL_FILL_COLOR = Color.parseColor("#ff0000"); private static int SELECTED_WORD_FILL_COLOR = Color.parseColor("#faeace"); private static int SELECTED_CELL_FILL_COLOR = Color.parseColor("#ecae44"); private static int TEXT_COLOR = Color.parseColor("#000000"); private static int CELL_STROKE_COLOR = Color.parseColor("#000000"); private static int CIRCLE_STROKE_COLOR = Color.parseColor("#555555"); private RectF mContentRect; // Content rectangle - bounds of the view private RectF mPuzzleRect; // Puzzle rectangle - basically the output size of the bitmap private float mScaledCellSize; private Crossword mCrossword; private Paint mCellStrokePaint; private Paint mCircleStrokePaint; private Paint mCellFillPaint; private Paint mCheatedCellFillPaint; private Paint mMistakeCellFillPaint; private Paint mSelectedWordFillPaint; private Paint mSelectedCellFillPaint; private Paint mNumberTextPaint; private Paint mNumberStrokePaint; private Paint mAnswerTextPaint; private float mCellSize; private float mMarkerSideLength; private float mCircleRadius; private float mNumberTextPadding; private float mAnswerTextPadding; private int mPuzzleWidth; // Total number of cells across private int mPuzzleHeight; // Total number of cells down private Cell[][] mPuzzleCells; // Map of the cells private Crossword.Word mSelectedWord; private int mSelectedCell; private char[] mAllowedChars; private char[] mAutocorrectBuffer; private float mMinScaleFactor; // Scale at which the puzzle takes up the entire screen private float mFitWidthScaleFactor; // Scale at which the puzzle fits in horizontally private float mMaxScaleFactor; // Scale beyond which the puzzle shouldn't be enlarged private PointF mCenteredOffset; // Offset at which the puzzle appears exactly in the middle private float mRenderScale; // This is the scale at which the bitmap is rendered private float mBitmapScale; // This is the scaling applied to the bitmap in touch resize mode private float mScaleStart; // The value of mRenderScale when touch resizing begins private PointF mBitmapOffset; // Offset of the rendered bitmap private RectF mTranslationBounds; // Bitmap translation limits private ScaleGestureDetector mScaleDetector; private GestureDetector mGestureDetector; private BitmapRenderer mAsyncRenderer; private Canvas mPuzzleCanvas; private Bitmap mPuzzleBitmap; private Paint mBitmapPaint; private boolean mEnableErrorHighlighting; private boolean mIsZooming; private Zoomer mZoomer; private Scroller mScroller; private Rect mTempRect = new Rect(); // For temp. calculation private OnSelectionChangeListener mSelectionChangeListener; private OnLongPressListener mLongpressListener; private CrosswordInputConnection.OnInputEventListener mInputEventListener = new CrosswordInputConnection.OnInputEventListener() { @Override public void onWordEntered(CharSequence text) { if (text != null) { if (mSelectedWord != null) { // Words like "ain't" contain punctuation marks that aren't // valid, but may appear in a crossword in punctuation-less // form. For this reason, we strip out invalid characters // before considering whether we want to fill them into the // selection. char[] chars = text.toString().toCharArray(); // Copy all acceptable chars to a separate array char[] tempBuffer = new char[chars.length]; int k = 0; for (char ch : chars) { if (isAcceptableChar(ch)) { tempBuffer[k++] = ch; } } char[] validChars; if (k == tempBuffer.length) { validChars = tempBuffer; } else { // Truncate to the last valid character validChars = new char[k]; System.arraycopy(tempBuffer, 0, validChars, 0, k); } // If the length of resulting word matches that of // selection, fill it in if (mSelectedWord.getLength() == validChars.length) { fillSelectedWord(validChars, false); } } } } @Override public void onWordCancelled() { if (mAutocorrectBuffer != null) { // Replace the contents of the selection with whatever is in // mAutocorrectBuffer fillSelectedWord(mAutocorrectBuffer, true); mAutocorrectBuffer = null; } else { handleBackspace(); } } @Override public void onEditorAction(int actionCode) { if (actionCode == EditorInfo.IME_ACTION_NEXT) { selectNextWord(); } } }; public CrosswordView(Context context, AttributeSet attrs) { super(context, attrs); if (!isInEditMode()) { setLayerType(View.LAYER_TYPE_HARDWARE, null); } // Set drawing defaults Resources r = context.getResources(); DisplayMetrics dm = r.getDisplayMetrics(); int cellFillColor = NORMAL_CELL_FILL_COLOR; int cheatedCellFillColor = CHEATED_CELL_FILL_COLOR; int mistakeCellFillColor = MISTAKE_CELL_FILL_COLOR; int selectedWordFillColor = SELECTED_WORD_FILL_COLOR; int selectedCellFillColor = SELECTED_CELL_FILL_COLOR; int textColor = TEXT_COLOR; int cellStrokeColor = CELL_STROKE_COLOR; int circleStrokeColor = CIRCLE_STROKE_COLOR; float numberTextSize = NUMBER_TEXT_SIZE * dm.scaledDensity; float answerTextSize = ANSWER_TEXT_SIZE * dm.scaledDensity; mCellSize = CELL_SIZE * dm.density; mNumberTextPadding = NUMBER_TEXT_PADDING * dm.density; mAnswerTextPadding = ANSWER_TEXT_PADDING * dm.density; // Read supplied attributes if (attrs != null) { Resources.Theme theme = context.getTheme(); TypedArray a = theme.obtainStyledAttributes(attrs, R.styleable.Crossword, 0, 0); mCellSize = a.getDimension(R.styleable.Crossword_cellSize, mCellSize); mNumberTextPadding = a.getDimension(R.styleable.Crossword_numberTextPadding, mNumberTextPadding); numberTextSize = a.getDimension(R.styleable.Crossword_numberTextSize, numberTextSize); mAnswerTextPadding = a.getDimension(R.styleable.Crossword_answerTextPadding, mAnswerTextPadding); answerTextSize = a.getDimension(R.styleable.Crossword_answerTextSize, answerTextSize); cellFillColor = a.getColor(R.styleable.Crossword_defaultCellFillColor, cellFillColor); cheatedCellFillColor = a.getColor(R.styleable.Crossword_cheatedCellFillColor, cheatedCellFillColor); mistakeCellFillColor = a.getColor(R.styleable.Crossword_mistakeCellFillColor, mistakeCellFillColor); selectedWordFillColor = a.getColor(R.styleable.Crossword_selectedWordFillColor, selectedWordFillColor); selectedCellFillColor = a.getColor(R.styleable.Crossword_selectedCellFillColor, selectedCellFillColor); cellStrokeColor = a.getColor(R.styleable.Crossword_cellStrokeColor, cellStrokeColor); circleStrokeColor = a.getColor(R.styleable.Crossword_circleStrokeColor, circleStrokeColor); textColor = a.getColor(R.styleable.Crossword_textColor, textColor); a.recycle(); } mMarkerSideLength = mCellSize * MARKER_TRIANGLE_LENGTH_FRACTION; // Init paints mCellFillPaint = new Paint(); mCellFillPaint.setColor(cellFillColor); mCellFillPaint.setStyle(Paint.Style.FILL); mCheatedCellFillPaint = new Paint(); mCheatedCellFillPaint.setColor(cheatedCellFillColor); mCheatedCellFillPaint.setStyle(Paint.Style.FILL); mMistakeCellFillPaint = new Paint(); mMistakeCellFillPaint.setColor(mistakeCellFillColor); mMistakeCellFillPaint.setStyle(Paint.Style.FILL); mSelectedWordFillPaint = new Paint(); mSelectedWordFillPaint.setColor(selectedWordFillColor); mSelectedWordFillPaint.setStyle(Paint.Style.FILL); mSelectedCellFillPaint = new Paint(); mSelectedCellFillPaint.setColor(selectedCellFillColor); mSelectedCellFillPaint.setStyle(Paint.Style.FILL); mCellStrokePaint = new Paint(); mCellStrokePaint.setColor(cellStrokeColor); mCellStrokePaint.setStyle(Paint.Style.STROKE); // mCellStrokePaint.setStrokeWidth(1); mCircleStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mCircleStrokePaint.setColor(circleStrokeColor); mCircleStrokePaint.setStyle(Paint.Style.STROKE); mCircleStrokePaint.setStrokeWidth(1); mNumberTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mNumberTextPaint.setColor(textColor); mNumberTextPaint.setTextAlign(Paint.Align.CENTER); mNumberTextPaint.setTextSize(numberTextSize); mNumberStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mNumberStrokePaint.setColor(cellFillColor); mNumberStrokePaint.setTextAlign(Paint.Align.CENTER); mNumberStrokePaint.setTextSize(numberTextSize); mNumberStrokePaint.setStyle(Paint.Style.STROKE); mNumberStrokePaint.setStrokeWidth(NUMBER_TEXT_STROKE_WIDTH * dm.scaledDensity); mAnswerTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mAnswerTextPaint.setColor(textColor); mAnswerTextPaint.setTextSize(answerTextSize); mAnswerTextPaint.setTextAlign(Paint.Align.CENTER); // http://www.google.com/fonts/specimen/Kalam Typeface typeface = Typeface.createFromAsset(context.getAssets(), "kalam-regular.ttf"); mAnswerTextPaint.setTypeface(typeface); // Init rest of the values mCircleRadius = (mCellSize / 2) - mCircleStrokePaint.getStrokeWidth(); mPuzzleCells = EMPTY_CELLS; mPuzzleWidth = 0; mPuzzleHeight = 0; mAllowedChars = EMPTY_CHARS; mRenderScale = 0; mBitmapOffset = new PointF(); mCenteredOffset = new PointF(); mTranslationBounds = new RectF(); mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); mGestureDetector = new GestureDetector(context, new GestureListener()); mContentRect = new RectF(); mPuzzleRect = new RectF(); mBitmapPaint = new Paint(Paint.FILTER_BITMAP_FLAG); mScroller = new Scroller(context, null, true); mZoomer = new Zoomer(context); setFocusableInTouchMode(true); setOnKeyListener(this); } @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState savedState = new SavedState(superState); savedState.mPuzzleCells = mPuzzleCells; savedState.mPuzzleHeight = mPuzzleHeight; savedState.mPuzzleWidth = mPuzzleWidth; savedState.mBitmapOffset = mBitmapOffset; savedState.mRenderScale = mRenderScale; savedState.mSelectedWord = mSelectedWord; savedState.mSelectedCell = mSelectedCell; savedState.mAllowedChars = mAllowedChars; savedState.mCrossword = mCrossword; savedState.mEnableErrorHighlighting = mEnableErrorHighlighting; return savedState; } @Override protected void onRestoreInstanceState(Parcelable state) { if (!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } SavedState savedState = (SavedState) state; super.onRestoreInstanceState(savedState.getSuperState()); mPuzzleCells = savedState.mPuzzleCells; mPuzzleHeight = savedState.mPuzzleHeight; mPuzzleWidth = savedState.mPuzzleWidth; mBitmapOffset = savedState.mBitmapOffset; mRenderScale = savedState.mRenderScale; mSelectedWord = savedState.mSelectedWord; mSelectedCell = savedState.mSelectedCell; mAllowedChars = savedState.mAllowedChars; mCrossword = savedState.mCrossword; mEnableErrorHighlighting = savedState.mEnableErrorHighlighting; } @Override public boolean onTouchEvent(@NonNull MotionEvent ev) { boolean retVal = mScaleDetector.onTouchEvent(ev); retVal = mGestureDetector.onTouchEvent(ev) || retVal; return retVal || super.onTouchEvent(ev); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mPuzzleBitmap != null) { canvas.save(); canvas.clipRect(mContentRect); canvas.translate(mBitmapOffset.x, mBitmapOffset.y); canvas.scale(mBitmapScale, mBitmapScale); canvas.drawBitmap(mPuzzleBitmap, 0, 0, mBitmapPaint); canvas.restore(); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // Get the content rect mContentRect.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()); resetConstraintsAndRedraw(); } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { outAttrs.actionLabel = null; outAttrs.inputType = InputType.TYPE_NULL; //InputType.TYPE_CLASS_TEXT; outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_FULLSCREEN; outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI; outAttrs.imeOptions &= ~EditorInfo.IME_MASK_ACTION; outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT; outAttrs.packageName = getContext().getPackageName(); CrosswordInputConnection inputConnection = new CrosswordInputConnection(this); inputConnection.setOnInputEventListener(mInputEventListener); return inputConnection; } @Override public boolean onCheckIsTextEditor() { return true; } @Override public void computeScroll() { super.computeScroll(); boolean invalidate = false; if (mZoomer.computeZoom()) { mRenderScale = mScaleStart + mZoomer.getCurrZoom(); recomputePuzzleRect(); if (constrainScaling()) { recomputePuzzleRect(); } mBitmapScale = mRenderScale / mScaleStart; invalidate = true; } else { if (mIsZooming) { regenerateBitmaps(); mIsZooming = false; } } if (mScroller.computeScrollOffset()) { mBitmapOffset.set(mScroller.getCurrX(), mScroller.getCurrY()); invalidate = true; } if (invalidate) { ViewCompat.postInvalidateOnAnimation(this); } } @Override public boolean onKey(View v, int keyCode, KeyEvent event) { boolean handled = false; if (event.getAction() == KeyEvent.ACTION_UP) { if (keyCode == KeyEvent.KEYCODE_SPACE) { switchWordDirection(); handled = true; } else if (keyCode == KeyEvent.KEYCODE_DEL) { handleBackspace(); handled = true; } else { int uniChar = event.getUnicodeChar(); if (uniChar != 0) { handleInput((char) event.getUnicodeChar()); handled = true; } } } return handled; } public void solveWord(Crossword.Word word) { char matrix[][]; int wordLen = word.getLength(); if (word.getDirection() == Crossword.Word.DIR_ACROSS) { matrix = new char[1][wordLen]; for (int i = 0; i < wordLen; i++) { matrix[0][i] = word.cellAt(i).charAt(0); // FIXME: multicells } } else if (word.getDirection() == Crossword.Word.DIR_DOWN) { matrix = new char[wordLen][1]; for (int i = 0; i < wordLen; i++) { matrix[i][0] = word.cellAt(i).charAt(0); // FIXME: multicells } } else { throw new IllegalArgumentException("Word direction not valid"); } setChars(word.getStartRow(), word.getStartColumn(), matrix, true); } public void solveChar(Crossword.Word word, int charIndex) { int row = word.getStartRow(); int column = word.getStartColumn(); if (word.getDirection() == Crossword.Word.DIR_ACROSS) { column += charIndex; } else if (word.getDirection() == Crossword.Word.DIR_DOWN) { row += charIndex; } // FIXME: multicells char ch = word.cellAt(charIndex).charAt(0); setChars(row, column, new char[][] { { ch, }, }, true); } public void solveCrossword() { // FIXME: multicells char[][] matrix = new char[mPuzzleHeight][mPuzzleWidth]; for (Crossword.Word word : mCrossword.getWordsAcross()) { int row = word.getStartRow(); int startCol = word.getStartColumn(); for (int i = 0, n = word.getLength(); i < n; i++) { matrix[row][startCol + i] = word.cellAt(i).charAt(0); } } for (Crossword.Word word : mCrossword.getWordsDown()) { int startRow = word.getStartRow(); int col = word.getStartColumn(); for (int i = 0, n = word.getLength(); i < n; i++) { matrix[startRow + i][col] = word.cellAt(i).charAt(0); } } setChars(0, 0, matrix, true); } public void reset() { for (int i = 0; i < mPuzzleHeight; i++) { for (int j = 0; j < mPuzzleWidth; j++) { Cell cell = mPuzzleCells[i][j]; if (cell != null) { cell.reset(); } } } redrawInPlace(); } public boolean getErrorHighlightingEnabled() { return mEnableErrorHighlighting; } public void setErrorHighlightingEnabled(boolean enabled) { if (mEnableErrorHighlighting != enabled) { mEnableErrorHighlighting = enabled; resetErrorMarkers(); } } public void setChars(int startRow, int startColumn, char charMatrix[][], boolean setCheatFlag) { // Check startRow/startColumn if (startRow < 0 || startColumn < 0) { throw new IllegalArgumentException("Invalid startRow/startColumn"); } // Check dimensions if (charMatrix.length < 1 || charMatrix[0].length < 1) { throw new IllegalArgumentException("Invalid matrix size"); } int charHeight = charMatrix.length; int charWidth = charMatrix[0].length; int endRow = startRow + charHeight - 1; int endColumn = startColumn + charWidth - 1; // Check bounds if (endRow >= mPuzzleHeight || endColumn >= mPuzzleWidth) { throw new IllegalArgumentException("Chars out of bounds"); } // Fill in the char array mAutocorrectBuffer = null; Crossword.Cell[][] map = mCrossword.getCellMap(); for (int i = startRow, k = 0; i <= endRow; i++, k++) { for (int j = startColumn, l = 0; j <= endColumn; j++, l++) { Cell vwCell = mPuzzleCells[i][j]; if (vwCell != null) { char ch = Cell.canonicalize(charMatrix[k][l]); if (ch != vwCell.mChar && isAcceptableChar(ch)) { vwCell.setChar(ch); if (setCheatFlag) { vwCell.mCheated = true; } if (mEnableErrorHighlighting) { vwCell.markError(map[i][j]); } } } } } // Redraw selection redrawInPlace(); } private void fillSelectedWord(char[] chars, boolean rewindCursor) { if (mSelectedWord == null) { return; } mAutocorrectBuffer = getChars(mSelectedWord); int startRow = mSelectedWord.getStartRow(); int startCol = mSelectedWord.getStartColumn(); int dir = mSelectedWord.getDirection(); int endRow = startRow; int endCol = startCol; int length = Math.min(mSelectedWord.getLength(), chars.length); if (dir == Crossword.Word.DIR_ACROSS) { endCol += length - 1; } else if (dir == Crossword.Word.DIR_DOWN) { endRow += length - 1; } int k = 0; Crossword.Cell[][] map = mCrossword.getCellMap(); for (int i = startRow; i <= endRow; i++) { for (int j = startCol; j <= endCol; j++) { Cell cell = mPuzzleCells[i][j]; char ch = chars[k++]; if (cell != null) { if (isAcceptableChar(ch)) { cell.setChar(ch); } else { cell.clearChar(); } if (mEnableErrorHighlighting) { cell.markError(map[i][j]); } } } } int position = 0; if (!rewindCursor && k > 1) { position = k - 1; } mSelectedCell = position; redrawInPlace(); } public void setCrossword(Crossword crossword) { mPuzzleHeight = 0; mPuzzleWidth = 0; mPuzzleCells = EMPTY_CELLS; mAllowedChars = EMPTY_CHARS; mCrossword = crossword; if (crossword != null) { mPuzzleWidth = crossword.getWidth(); mPuzzleHeight = crossword.getHeight(); mPuzzleCells = new Cell[mPuzzleHeight][mPuzzleWidth]; // Copy allowed characters char allowedChars[] = crossword.getAlphabet(); mAllowedChars = new char[allowedChars.length]; for (int i = 0, n = allowedChars.length; i < n; i++) { mAllowedChars[i] = Character.toUpperCase(allowedChars[i]); } // Copy across for (Crossword.Word word : crossword.getWordsAcross()) { int row = word.getStartRow(); int startColumn = word.getStartColumn(); // Chars for (int column = startColumn, p = 0, n = word.getLength(); p < n; column++, p++) { Cell cell = new Cell(); cell.mWordAcrossNumber = word.getNumber(); cell.mCircled = word.cellAt(p).isCircled(); mPuzzleCells[row][column] = cell; } // Number mPuzzleCells[row][startColumn].mNumber = word.getNumber() + ""; } // Copy down for (Crossword.Word word : crossword.getWordsDown()) { int startRow = word.getStartRow(); int column = word.getStartColumn(); // Chars for (int row = startRow, p = 0, n = word.getLength(); p < n; row++, p++) { // It's possible that we already have a cell in that position from 'Across' // Check before creating a new cell Cell cell = mPuzzleCells[row][column]; if (cell == null) { cell = new Cell(); if (word.cellAt(p).isCircled()) { cell.mCircled = true; } mPuzzleCells[row][column] = cell; } cell.mWordDownNumber = word.getNumber(); } // Number if (mPuzzleCells[startRow][column].mNumber == null) { mPuzzleCells[startRow][column].mNumber = word.getNumber() + ""; } } } mRenderScale = 0; // Will recompute when reset resetConstraintsAndRedraw(); } public Crossword.State getState() { if (mCrossword == null) { return null; } Crossword.State state = mCrossword.newState(); if (mSelectedWord != null) { state.setSelection(mSelectedWord.getDirection(), mSelectedWord.getNumber(), mSelectedCell); } for (int i = 0; i < mPuzzleHeight; i++) { for (int j = 0; j < mPuzzleWidth; j++) { Cell cell = mPuzzleCells[i][j]; if (cell != null) { if (!cell.isEmpty()) { state.setCharAt(i, j, cell.mChar); } state.setCheatedAt(i, j, cell.mCheated); } } } mCrossword.updateStateStatistics(state); return state; } public void restoreState(Crossword.State state) { if (state.getHeight() != mPuzzleHeight || state.getWidth() != mPuzzleWidth) { throw new RuntimeException("Dimensions for puzzle and state don't match"); } Crossword.Cell[][] map = mCrossword.getCellMap(); for (int i = 0; i < mPuzzleHeight; i++) { for (int j = 0; j < mPuzzleWidth; j++) { Cell cell = mPuzzleCells[i][j]; if (cell != null) { cell.mCheated = state.cheatedAt(i, j); cell.setChar(state.charAt(i, j)); if (mEnableErrorHighlighting) { cell.markError(map[i][j]); } } } } if (state.hasSelection()) { Crossword.Word word = mCrossword.findWord(state.getSelectedDirection(), state.getSelectedNumber()); int cell = state.getSelectedCell(); if (word != null && cell < word.getLength()) { resetSelection(word, cell, false); } } Crosswords.logv("State loaded successfully"); resetConstraintsAndRedraw(); } public void setOnSelectionChangeListener(OnSelectionChangeListener listener) { mSelectionChangeListener = listener; } public void setOnLongPressListener(OnLongPressListener listener) { mLongpressListener = listener; } public void selectPreviousWord() { if (mCrossword != null) { Crossword.Word previous = mCrossword.previousWord(mSelectedWord); resetSelection(previous, Math.max(firstFreeCell(previous), 0)); } } public void selectNextWord() { if (mCrossword != null) { Crossword.Word next = mCrossword.nextWord(mSelectedWord); resetSelection(next, Math.max(firstFreeCell(next), 0)); } } public void selectWord(Crossword.Word word) { if (mCrossword != null) { resetSelection(word, 0); } } public Rect getCellRect(Crossword.Word word, int cell) { Rect cellRect = null; if (word != null && cell < word.getLength()) { CellOffset co = getCellOffset(word, cell); float left = co.mColumn * mScaledCellSize + mBitmapOffset.x; float top = co.mRow * mScaledCellSize + mBitmapOffset.y; cellRect = new Rect(); cellRect.left = (int) (left); cellRect.top = (int) (top); cellRect.right = (int) (left + mScaledCellSize); cellRect.bottom = (int) (top + mScaledCellSize); } return cellRect; } protected void handleInput(char ch) { if (mSelectedWord != null) { if (isAcceptableChar(ch)) { mAutocorrectBuffer = null; Crossword.Word selectedWord = mSelectedWord; int selectedCell = mSelectedCell; CellOffset co = getCellOffset(mSelectedWord, selectedCell); Cell cell = mPuzzleCells[co.mRow][co.mColumn]; // boolean wasEmpty = cell.isEmpty(); cell.setChar(ch); if (mEnableErrorHighlighting) { Crossword.Cell[][] map = mCrossword.getCellMap(); cell.markError(map[co.mRow][co.mColumn]); // if (!wasEmpty && !cell.mCheated) { // // User practically cheated // cell.mCheated = !cell.mError; // } } if (selectedCell + 1 < selectedWord.getLength()) { selectedCell++; } else { selectedWord = mCrossword.nextWord(selectedWord); selectedCell = 0; } resetSelection(selectedWord, selectedCell); } } } protected void handleBackspace() { if (mSelectedWord == null) { return; } Crossword.Word selectedWord = mSelectedWord; int selectedIndex = mSelectedCell; CellOffset co = getCellOffset(mSelectedWord, mSelectedCell); if (!mPuzzleCells[co.mRow][co.mColumn].isEmpty()) { // Delete currently selected char mPuzzleCells[co.mRow][co.mColumn].clearChar(); } else if (mSelectedCell > 0) { // Go back one cell and remove the char selectedIndex--; co = getCellOffset(selectedWord, selectedIndex); mPuzzleCells[co.mRow][co.mColumn].clearChar(); } else { // At the first letter of a word. Select the previous word and do // what we did if (mSelectedCell > 0) selectedWord = mCrossword.previousWord(mSelectedWord); selectedIndex = selectedWord.getLength() - 1; co = getCellOffset(selectedWord, selectedIndex); mPuzzleCells[co.mRow][co.mColumn].clearChar(); } resetSelection(selectedWord, selectedIndex); } protected void switchWordDirection() { if (mSelectedWord != null) { Crossword.Word selWord = mSelectedWord; int selCell = mSelectedCell; Crossword.Word orthoWord = null; int orthoCell = 0; if (selWord.getDirection() == Crossword.Word.DIR_ACROSS) { Cell cell = mPuzzleCells[selWord.getStartRow()][selWord.getStartColumn() + selCell]; if (cell.mWordDownNumber != Cell.WORD_NUMBER_NONE) { orthoWord = mCrossword.findWord(Crossword.Word.DIR_DOWN, cell.mWordDownNumber); orthoCell = selWord.getStartRow() - orthoWord.getStartRow(); } } else if (selWord.getDirection() == Crossword.Word.DIR_DOWN) { Cell cell = mPuzzleCells[selWord.getStartRow() + selCell][selWord.getStartColumn()]; if (cell.mWordAcrossNumber != Cell.WORD_NUMBER_NONE) { orthoWord = mCrossword.findWord(Crossword.Word.DIR_ACROSS, cell.mWordAcrossNumber); orthoCell = selWord.getStartColumn() - orthoWord.getStartColumn(); } } if (orthoWord != null) { resetSelection(orthoWord, orthoCell); } } } protected void zoomTo(float finalRenderScale) { float delta = Math.abs(finalRenderScale - mRenderScale); if (delta < .01f) { Crosswords.logv("Ignoring autozoom request (delta: %.02f)", delta); return; } mZoomer.forceFinished(true); mIsZooming = true; mScaleStart = mRenderScale; mZoomer.startZoom(finalRenderScale - mScaleStart); ViewCompat.postInvalidateOnAnimation(CrosswordView.this); } protected void showKeyboard() { InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(CrosswordView.this, InputMethodManager.SHOW_IMPLICIT); } private void resetErrorMarkers() { Crossword.Cell[][] map = mCrossword.getCellMap(); for (int i = 0; i < map.length; i++) { for (int j = 0; j < map[i].length; j++) { Cell vwCell = mPuzzleCells[i][j]; if (vwCell != null) { if (mEnableErrorHighlighting) { vwCell.markError(map[i][j]); } else { vwCell.mError = false; } } } } // Redraw selection redrawInPlace(); } private CellOffset getCellOffset(Crossword.Word word, int position) { CellOffset offset = null; if (position >= 0 && position < word.getLength()) { offset = new CellOffset(word.getStartRow(), word.getStartColumn()); if (word.getDirection() == Crossword.Word.DIR_ACROSS) { offset.mColumn += position; } else if (word.getDirection() == Crossword.Word.DIR_DOWN) { offset.mRow += position; } } return offset; } private void resetConstraintsAndRedraw() { // Determine the scale at which the puzzle takes up the entire width float unscaledWidth = mPuzzleWidth * mCellSize + 1; // +1px for stroke brush mFitWidthScaleFactor = mContentRect.width() / unscaledWidth; // Set the default scale to be "fit to width" if (mRenderScale < .01) { mRenderScale = mFitWidthScaleFactor; } // Determine the smallest scale factor if (mContentRect.width() < mContentRect.height()) { mMinScaleFactor = mFitWidthScaleFactor; } else { float unscaledHeight = mPuzzleHeight * mCellSize + 1; mMinScaleFactor = mContentRect.height() / unscaledHeight; // +1px for stroke brush } int largestDimension = Math.max(mPuzzleWidth, mPuzzleHeight); float maxAvailableDimension = (float) (MAX_BITMAP_DIMENSION - 1); // stroke brush again mMaxScaleFactor = maxAvailableDimension / (largestDimension * mCellSize); mMaxScaleFactor = Math.max(mMaxScaleFactor, mMinScaleFactor); mBitmapScale = 1.0f; mIsZooming = false; if (mRenderScale < mMinScaleFactor) { mRenderScale = mMinScaleFactor; } // Recompute scaled puzzle size recomputePuzzleRect(); if (constrainScaling()) { recomputePuzzleRect(); } regenerateBitmaps(); } private void regenerateBitmaps() { synchronized (this) { if (mAsyncRenderer != null) { mAsyncRenderer.cancel(false); } mAsyncRenderer = new BitmapRenderer(mRenderScale); mAsyncRenderer.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } private void recomputePuzzleRect() { // Compute scaled puzzle rect mScaledCellSize = mRenderScale * mCellSize; mPuzzleRect.set(0, 0, mPuzzleWidth * mScaledCellSize + 1, // w/h get an extra pixel due to the mPuzzleHeight * mScaledCellSize + 1); // hairline-wide stroke of the cell // Determine center locations mCenteredOffset.set(mContentRect.left + (mContentRect.width() - mPuzzleRect.width()) / 2.0f, mContentRect.top + (mContentRect.height() - mPuzzleRect.height()) / 2.0f); // Compute translation bounds mTranslationBounds.set(mContentRect.right - mPuzzleRect.right, mContentRect.bottom - mPuzzleRect.bottom, mContentRect.left - mPuzzleRect.left, mContentRect.top - mPuzzleRect.top); constrainTranslation(); } private void constrainTranslation() { // Clamp the offset to fit within the view clampPointF(mBitmapOffset, mTranslationBounds); if (mPuzzleRect.width() < mContentRect.width()) { // Puzzle is narrower than the available width - center it horizontally mBitmapOffset.x = mCenteredOffset.x; } // Vertical if (mPuzzleRect.height() < mContentRect.height()) { // Puzzle is shorter than the available height - center it vertically mBitmapOffset.y = mCenteredOffset.y; } } private boolean constrainScaling() { if (mPuzzleRect.width() < mContentRect.width() && mPuzzleRect.height() < mContentRect.height()) { if (mRenderScale < mMinScaleFactor) { mRenderScale = mMinScaleFactor; return true; } } if (mRenderScale > mMaxScaleFactor) { mRenderScale = mMaxScaleFactor; return true; } return false; } private void bringIntoView(Crossword.Word word, int selectedCell) { if (word == null) { return; } RectF wordRect = new RectF(); wordRect.left = word.getStartColumn() * mScaledCellSize - mContentRect.left; wordRect.top = word.getStartRow() * mScaledCellSize - mContentRect.top; if (word.getDirection() == Crossword.Word.DIR_ACROSS) { wordRect.right = wordRect.left + word.getLength() * mScaledCellSize; wordRect.bottom = wordRect.top + mScaledCellSize; } else if (word.getDirection() == Crossword.Word.DIR_DOWN) { wordRect.right = wordRect.left + mScaledCellSize; wordRect.bottom = wordRect.top + word.getLength() * mScaledCellSize; } RectF objectRect = new RectF(wordRect); RectF visibleArea = new RectF(-mBitmapOffset.x, -mBitmapOffset.y, -mBitmapOffset.x + mContentRect.width(), -mBitmapOffset.y + mContentRect.height()); if (visibleArea.contains(objectRect)) { return; // Already visible } if (objectRect.width() > visibleArea.width() || objectRect.height() > visibleArea.height()) { // Available area isn't large enough to fit the entire word // Is the selected cell visible? If not, bring it into view CellOffset co = getCellOffset(word, selectedCell); RectF cellRect = new RectF(); cellRect.left = co.mColumn * mScaledCellSize; cellRect.top = co.mRow * mScaledCellSize; cellRect.right = cellRect.left + mScaledCellSize; cellRect.bottom = cellRect.top + mScaledCellSize; if (visibleArea.contains(cellRect)) { return; // Already visible } objectRect.set(cellRect); } // Compute view that includes the object in the center PointF end = new PointF((visibleArea.width() - objectRect.width()) / 2.0f - objectRect.left, (visibleArea.height() - objectRect.height()) / 2.0f - objectRect.top); // Clamp the values clampPointF(end, mTranslationBounds); // Compute the distance to travel from current location float distanceX = end.x - mBitmapOffset.x; float distanceY = end.y - mBitmapOffset.y; // Scroll the point into view mScroller.startScroll((int) mBitmapOffset.x, (int) mBitmapOffset.y, (int) distanceX, (int) distanceY, NAVIGATION_SCROLL_DURATION_MS); } private static void clampPointF(PointF point, RectF rect) { // Clamp the values if (point.x < rect.left) { point.x = rect.left; } else if (point.x > rect.right) { point.x = rect.right; } if (point.y < rect.top) { point.y = rect.top; } else if (point.y > rect.bottom) { point.y = rect.bottom; } } private void redrawInPlace() { mAsyncRenderer = new BitmapRenderer(mRenderScale); mAsyncRenderer.renderPuzzle(mPuzzleCanvas); ViewCompat.postInvalidateOnAnimation(this); } private void renderCell(Canvas canvas, Cell cell, RectF cellRect, Paint fillPaint) { canvas.drawRect(cellRect, fillPaint); canvas.drawRect(cellRect, mCellStrokePaint); if (cell.mCircled) { canvas.drawCircle(cellRect.centerX(), cellRect.centerY(), mCircleRadius, mCircleStrokePaint); } if (cell.mNumber != null) { mNumberTextPaint.getTextBounds(cell.mNumber, 0, cell.mNumber.length(), mTempRect); float numberX = cellRect.left + mNumberTextPadding + (mTempRect.width() / 2); float numberY = cellRect.top + mNumberTextPadding + mTempRect.height(); if (cell.mCircled) { canvas.drawText(cell.mNumber, numberX, numberY, mNumberStrokePaint); } canvas.drawText(cell.mNumber, numberX, numberY, mNumberTextPaint); } if (cell.mCheated) { Path path = new Path(); path.moveTo(cellRect.right, cellRect.bottom); path.lineTo(cellRect.right - mMarkerSideLength, cellRect.bottom); path.lineTo(cellRect.right, cellRect.bottom - mMarkerSideLength); path.close(); canvas.drawPath(path, mCheatedCellFillPaint); canvas.drawPath(path, mCellStrokePaint); } if (cell.mError) { Path path = new Path(); path.moveTo(cellRect.left, cellRect.bottom); path.lineTo(cellRect.left + mMarkerSideLength, cellRect.bottom); path.lineTo(cellRect.left, cellRect.bottom - mMarkerSideLength); path.close(); canvas.drawPath(path, mMistakeCellFillPaint); canvas.drawPath(path, mCellStrokePaint); } if (!cell.isEmpty()) { mAnswerTextPaint.getTextBounds(cell.mCharStr, 0, cell.mCharStr.length(), mTempRect); canvas.drawText(cell.mCharStr, cellRect.left + mCellSize / 2, cellRect.top + mCellSize - mAnswerTextPadding, mAnswerTextPaint); } } private void resetSelection(Crossword.Word newSelectedWord, int newSelectedCell) { resetSelection(newSelectedWord, newSelectedCell, true); } private void resetSelection(Crossword.Word newSelectedWord, int newSelectedCell, boolean bringIntoView) { boolean selectionChanged = !Crossword.Word.equals(mSelectedWord, newSelectedWord); if (mPuzzleCanvas != null) { // Create a canvas on top of the existing bitmap if (mSelectedWord != null && selectionChanged) { // Clear the selection from the deselected word renderSelection(mPuzzleCanvas, true); mAutocorrectBuffer = null; } } // Set new selection mSelectedWord = newSelectedWord; mSelectedCell = newSelectedCell; if (mPuzzleCanvas != null) { // Bring new selection into view, if requested if (bringIntoView) { bringIntoView(mSelectedWord, mSelectedCell); } // Render the new selection renderSelection(mPuzzleCanvas, false); } // Notify the listener of the change in selection if (selectionChanged && mSelectionChangeListener != null) { if (mSelectedWord == null) { mSelectionChangeListener.onWordDeselected(this); } else { mSelectionChangeListener.onWordSelectionChanged(this, mSelectedWord); } } // Invalidate the view invalidate(); } private void renderSelection(Canvas canvas, boolean clearSelection) { if (mSelectedWord == null) { return; } int startRow = mSelectedWord.getStartRow(); int endRow = startRow; int startColumn = mSelectedWord.getStartColumn(); int endColumn = startColumn; RectF cellRect = new RectF(); canvas.save(); canvas.scale(mRenderScale, mRenderScale); if (mSelectedWord.getDirection() == Crossword.Word.DIR_ACROSS) { endColumn += mSelectedWord.getLength() - 1; } else { endRow += mSelectedWord.getLength() - 1; } float top = mSelectedWord.getStartRow() * mCellSize; for (int row = startRow, index = 0; row <= endRow; row++, top += mCellSize) { float left = mSelectedWord.getStartColumn() * mCellSize; for (int column = startColumn; column <= endColumn; column++, left += mCellSize) { Cell cell = mPuzzleCells[row][column]; if (cell != null) { // Draw the unselected cell Paint paint; if (clearSelection) { paint = mCellFillPaint; } else { if (index == mSelectedCell) { paint = mSelectedCellFillPaint; } else { paint = mSelectedWordFillPaint; } } cellRect.set(left, top, left + mCellSize, top + mCellSize); renderCell(canvas, cell, cellRect, paint); } index++; } } canvas.restore(); } private boolean isAcceptableChar(char ch) { char upper = Character.toUpperCase(ch); for (char allowedChar : mAllowedChars) { if (upper == allowedChar) { return true; } } return false; } private boolean getCellOffset(float viewX, float viewY, CellOffset offset) { viewX -= mBitmapOffset.x; viewY -= mBitmapOffset.y; int column = (int) (viewX / mScaledCellSize); int row = (int) (viewY / mScaledCellSize); if (row >= 0 && row < mPuzzleHeight && column >= 0 && column < mPuzzleWidth) { offset.mRow = row; offset.mColumn = column; return true; } return false; } private void handleCellTap(CellOffset offset) { Cell cell = mPuzzleCells[offset.mRow][offset.mColumn]; if (cell == null) { return; } int preferredDir = Crossword.Word.DIR_ACROSS; if (mSelectedWord != null) { CellOffset co = getCellOffset(mSelectedWord, mSelectedCell); if (offset.mRow == co.mRow && offset.mColumn == co.mColumn) { // Same cell tapped - flip direction switchWordDirection(); showKeyboard(); return; } // Select a word in the same direction preferredDir = mSelectedWord.getDirection(); } Selectable sel = new Selectable(); if (getSelectable(offset, preferredDir, sel)) { resetSelection(sel.mWord, sel.mCellIndex); } showKeyboard(); } private void handleCellLongPress(CellOffset offset) { Cell cell = mPuzzleCells[offset.mRow][offset.mColumn]; if (cell == null) { return; } int preferredDir = Crossword.Word.DIR_ACROSS; if (mSelectedWord != null) { preferredDir = mSelectedWord.getDirection(); } Selectable sel = new Selectable(); if (getSelectable(offset, preferredDir, sel)) { resetSelection(sel.mWord, sel.mCellIndex, false); if (mLongpressListener != null) { // Notify the listener mLongpressListener.onCellLongPressed(this, mSelectedWord, sel.mCellIndex); } } } private char[] getChars(Crossword.Word word) { int length = word.getLength(); char[] chars = new char[length]; for (int i = 0; i < length; i++) { Cell cell = getCell(word, i); if (cell != null) { chars[i] = cell.mChar; } } return chars; } private int firstFreeCell(Crossword.Word word) { int firstFree = -1; if (word != null) { for (int i = 0, n = word.getLength(); i < n; i++) { Cell cell = getCell(word, i); if (cell.isEmpty()) { firstFree = i; break; } } } return firstFree; } private Cell getCell(Crossword.Word word, int cell) { int row = word.getStartRow(); int col = word.getStartColumn(); int dir = word.getDirection(); if (dir == Crossword.Word.DIR_ACROSS) { col += cell; } else if (dir == Crossword.Word.DIR_DOWN) { row += cell; } else { throw new IllegalArgumentException("Word direction not valid"); } return mPuzzleCells[row][col]; } private boolean getSelectable(CellOffset cellOffset, int preferredDir, Selectable sel) { Cell cell = mPuzzleCells[cellOffset.mRow][cellOffset.mColumn]; if (cell == null) { return false; } sel.mCellIndex = 0; int acrossNumber = cell.mWordAcrossNumber; int downNumber = cell.mWordDownNumber; // Select an Across word if we're currently selecting Across words, // or if we're selecting Down words, but no Down words are // available at that cell if (acrossNumber != Cell.WORD_NUMBER_NONE) { if (preferredDir == Crossword.Word.DIR_ACROSS || (preferredDir == Crossword.Word.DIR_DOWN && downNumber == Cell.WORD_NUMBER_NONE)) { sel.mWord = mCrossword.findWord(Crossword.Word.DIR_ACROSS, acrossNumber); sel.mCellIndex = cellOffset.mColumn - sel.mWord.getStartColumn(); } } // Select a Down word if we're currently selecting Down words, // or if we're selecting Across words, but no Across words are // available at that cell if (downNumber != Cell.WORD_NUMBER_NONE) { if (preferredDir == Crossword.Word.DIR_DOWN || (preferredDir == Crossword.Word.DIR_ACROSS && acrossNumber == Cell.WORD_NUMBER_NONE)) { sel.mWord = mCrossword.findWord(Crossword.Word.DIR_DOWN, downNumber); sel.mCellIndex = cellOffset.mRow - sel.mWord.getStartRow(); } } return true; } static class SavedState extends BaseSavedState { public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { @Override public SavedState createFromParcel(Parcel source) { return new SavedState(source); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; private int mPuzzleWidth; private int mPuzzleHeight; private Cell[][] mPuzzleCells; private float mRenderScale; private PointF mBitmapOffset; private Crossword.Word mSelectedWord; private int mSelectedCell; private char[] mAllowedChars; private Crossword mCrossword; private boolean mEnableErrorHighlighting; SavedState(Parcelable superState) { super(superState); } private SavedState(Parcel in) { super(in); mPuzzleWidth = in.readInt(); mPuzzleHeight = in.readInt(); mPuzzleCells = new Cell[mPuzzleHeight][mPuzzleWidth]; // Read the flattened array and write its elements to mPuzzleCells Parcelable[] flatCells = in.readParcelableArray(Cell.class.getClassLoader()); if (flatCells != null) { for (int i = 0, k = 0; i < mPuzzleHeight; i++) { for (int j = 0; j < mPuzzleWidth; j++) { mPuzzleCells[i][j] = (Cell) flatCells[k++]; } } } mRenderScale = in.readFloat(); mBitmapOffset = in.readParcelable(PointF.class.getClassLoader()); mSelectedWord = in.readParcelable(Crossword.Word.class.getClassLoader()); mSelectedCell = in.readInt(); mAllowedChars = in.createCharArray(); mCrossword = in.readParcelable(Crossword.class.getClassLoader()); mEnableErrorHighlighting = in.readByte() != 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeInt(mPuzzleWidth); dest.writeInt(mPuzzleHeight); // Flatten to a one-dimensional array Cell[] flatCells = null; if (mPuzzleCells != null) { flatCells = new Cell[mPuzzleHeight * mPuzzleWidth]; for (int i = 0, k = 0; i < mPuzzleHeight; i++) { for (int j = 0; j < mPuzzleWidth; j++) { flatCells[k++] = mPuzzleCells[i][j]; } } } dest.writeParcelableArray(flatCells, 0); dest.writeFloat(mRenderScale); dest.writeParcelable(mBitmapOffset, 0); dest.writeParcelable(mSelectedWord, 0); dest.writeInt(mSelectedCell); dest.writeCharArray(mAllowedChars); dest.writeParcelable(mCrossword, 0); dest.writeByte(mEnableErrorHighlighting ? (byte) 1 : 0); } } static class Selectable { public Crossword.Word mWord; public int mCellIndex; } static class CellOffset { private int mRow; private int mColumn; public CellOffset() { } public CellOffset(CellOffset offset) { mRow = offset.mRow; mColumn = offset.mColumn; } public CellOffset(int row, int column) { mRow = row; mColumn = column; } } static class Cell implements Parcelable { public static final Creator<Cell> CREATOR = new Creator<Cell>() { public Cell createFromParcel(Parcel in) { return new Cell(in); } public Cell[] newArray(int size) { return new Cell[size]; } }; private static final int WORD_NUMBER_NONE = -1; private String mNumber; private char mChar; private String mCharStr; private int mWordAcrossNumber; private int mWordDownNumber; private boolean mCheated; private boolean mCircled; private boolean mError; public Cell() { mChar = Crossword.State.EMPTY_CELL; mWordAcrossNumber = mWordDownNumber = WORD_NUMBER_NONE; } public static char canonicalize(char ch) { return Character.toUpperCase(ch); } public boolean isEmpty() { return mChar == Crossword.State.EMPTY_CELL; } public void clearChar() { setChar(Crossword.State.EMPTY_CELL); } public void reset() { clearChar(); mCheated = false; } public void setChar(char ch) { if (ch == Crossword.State.EMPTY_CELL) { mChar = ch; mCharStr = null; } else { mChar = canonicalize(ch); mCharStr = String.valueOf(mChar); } } public void markError(Crossword.Cell cwCell) { mError = !isEmpty() && !cwCell.contains(mChar); } private Cell(Parcel in) { mNumber = in.readString(); setChar(in.createCharArray()[0]); mWordAcrossNumber = in.readInt(); mWordDownNumber = in.readInt(); mCheated = in.readByte() != 0; mCircled = in.readByte() != 0; mError = in.readByte() != 0; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(mNumber); dest.writeCharArray(new char[] { mChar, }); dest.writeInt(mWordAcrossNumber); dest.writeInt(mWordDownNumber); dest.writeByte((byte) (mCheated ? 1 : 0)); dest.writeByte((byte) (mCircled ? 1 : 0)); dest.writeByte((byte) (mError ? 1 : 0)); } } private class BitmapRenderer extends AsyncTask<Void, Void, Void> { private float mScale; private Canvas mRenderingCanvas; private Bitmap mRenderedPuzzle; public BitmapRenderer(float scaleFactor) { mScale = scaleFactor; } @Override protected Void doInBackground(Void... params) { Canvas canvas = null; Bitmap puzzleBitmap = null; if (mPuzzleWidth > 0 && mPuzzleHeight > 0) { int width = (int) mPuzzleRect.width(); int height = (int) mPuzzleRect.height(); Crosswords.logv("Creating a new %dx%d puzzle bitmap...", width, height); puzzleBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); canvas = new Canvas(puzzleBitmap); renderPuzzle(canvas); } else { Crosswords.logv("Not creating an empty puzzle bitmap"); } if (!isCancelled()) { mRenderedPuzzle = puzzleBitmap; mRenderingCanvas = canvas; } return null; } @Override protected void onPostExecute(Void param) { super.onPostExecute(param); mPuzzleCanvas = mRenderingCanvas; mPuzzleBitmap = mRenderedPuzzle; mBitmapScale = 1.0f; Crosswords.logv("Invalidating..."); ViewCompat.postInvalidateOnAnimation(CrosswordView.this); } @Override protected void onCancelled() { super.onCancelled(); Crosswords.logv("Task cancelled"); } public void renderPuzzle(Canvas canvas) { RectF cellRect = new RectF(); Crosswords.logv("Rendering puzzle..."); canvas.save(); canvas.scale(mScale, mScale); float top = 0; for (int i = 0; i < mPuzzleHeight; i++, top += mCellSize) { float left = 0; for (int j = 0; j < mPuzzleWidth; j++, left += mCellSize) { if (isCancelled()) { return; } Cell cell = mPuzzleCells[i][j]; if (cell != null) { cellRect.set(left, top, left + mCellSize, top + mCellSize); renderCell(canvas, cell, cellRect, mCellFillPaint); } } } canvas.restore(); renderSelection(canvas, false); } } private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { @Override public boolean onScaleBegin(ScaleGestureDetector detector) { mZoomer.forceFinished(true); mIsZooming = false; mScaleStart = mRenderScale; return true; } @Override public boolean onScale(ScaleGestureDetector detector) { mRenderScale *= detector.getScaleFactor(); recomputePuzzleRect(); if (constrainScaling()) { recomputePuzzleRect(); } mBitmapScale = mRenderScale / mScaleStart; invalidate(); return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { regenerateBitmaps(); } } private class GestureListener extends GestureDetector.SimpleOnGestureListener { private CellOffset mTapLocation; public GestureListener() { mTapLocation = new CellOffset(); } @Override public boolean onDown(MotionEvent e) { if (!mScroller.isFinished()) { mScroller.forceFinished(true); } return true; } @Override public boolean onSingleTapUp(MotionEvent e) { if (getCellOffset(e.getX(), e.getY(), mTapLocation)) { handleCellTap(new CellOffset(mTapLocation)); return true; } return false; } @Override public void onLongPress(MotionEvent e) { if (!mScaleDetector.isInProgress() && getCellOffset(e.getX(), e.getY(), mTapLocation)) { handleCellLongPress(mTapLocation); // Cancel the scale gesture by sending the detector // an ACTION_CANCEL event, to prevent double-tap scaling // from interfering. There should be a nicer way to do // this, but there isn't.. MotionEvent cancelEvent = MotionEvent.obtain(e.getDownTime(), e.getEventTime(), MotionEvent.ACTION_CANCEL, e.getX(), e.getY(), e.getMetaState()); mScaleDetector.onTouchEvent(cancelEvent); } } @Override public boolean onDoubleTap(MotionEvent e) { zoomTo(mFitWidthScaleFactor); return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distX, float distY) { mBitmapOffset.offset(-distX, -distY); constrainTranslation(); invalidate(); return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { // Horizontal int startX = (int) mBitmapOffset.x; int minX = startX; int maxX = startX; if (mPuzzleRect.width() >= mContentRect.width()) { // Puzzle exceeds content, set horizontal flinging bounds minX = (int) mTranslationBounds.left; maxX = (int) mTranslationBounds.right; } // Vertical int startY = (int) mBitmapOffset.y; int minY = startY; int maxY = startY; if (mPuzzleRect.height() >= mContentRect.height()) { // Puzzle exceeds content, set vertical flinging bounds minY = (int) mTranslationBounds.top; maxY = (int) mTranslationBounds.bottom; } mScroller.fling(startX, startY, (int) (velocityX / FLING_VELOCITY_DOWNSCALE), (int) (velocityY / FLING_VELOCITY_DOWNSCALE), minX, maxX, minY, maxY); return true; } } }