package dan.dit.whatsthat.solution; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.util.DisplayMetrics; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import dan.dit.whatsthat.util.image.ImageUtil; /** * Created by daniel on 09.01.16. */ class SolutionInputLetterClickLayout extends SolutionInputLayout { private static final int USER_LETTER_COLOR_COMPLETED = Color.GREEN; private static final int USER_LETTER_COLOR_INCOMPLETE = 0xFFFF4400; private static final int USER_LETTER_COLOR_MISSING = Color.RED; /* values stored are in pixel units, constants are in density independent */ private static final float PADDING_TB = 5.f; //dp , padding top + bottom private static final float PADDING_LR = 30.f; //dp, padding left + right private static final float PADDING_USER_ALL = 5.f; //dp, space between user letters and all letters private static final float LETTER_MAX_RADIUS = 35.f; //dp, maximum radius for letters private static final float LETTER_MAX_GAP = 10.f; //dp, maximum gap between letters private static final float ALL_LETTER_BASE_SIZE = 20f; //dp, base size of letters private static final float USER_LETTER_BASE_SIZE = 25f; //dp, base size of letters private static final float CIRCLE_BORDER_WIDTH = 1f; //dp, width of the circle border private static final int ALL_LETTERS_MAX_ROWS = 3; private static final float ALL_LETTERS_ROW_PADDING = 2.f; //dp, padding between rows private static final float USER_LETTER_FRACTION = 1.f/3.f; private static final float CLICK_DISTANCE_MAX_DELTA = 25.f; // dp, maximum additional distance so that a click counts private final SolutionInputLetterClick mLetterClick; private float mUserLetterCircleRadius; private List<Float> mUserLetterX=new LinkedList<>(); private float mUserLetterY; private List<Float> mAllLettersX=new LinkedList<>(); private List<Float> mAllLettersY=new LinkedList<>(); private float mAllLettersCircleRadius; private Paint mUserLetterCirclePaint = new Paint(); private Paint mUserLetterPaint = new Paint(); private final Rect mTextBounds = new Rect(); private Paint mUserLetterCircleBorderPaint = new Paint(); private Paint mAllLetterPaint = new Paint(); private Paint mAllLetterCirclePaint = new Paint(); private Paint mAllLetterCircleBorderPaint = new Paint(); private float mWidth; private float mHeight; private DisplayMetrics mMetrics; public SolutionInputLetterClickLayout(SolutionInputLetterClick letterClick) { mLetterClick = letterClick; initPaints(); } private void initPaints() { mUserLetterCirclePaint.setAntiAlias(true); mUserLetterPaint.setAntiAlias(true); mAllLetterPaint.setAntiAlias(true); mAllLetterCirclePaint.setAntiAlias(true); mAllLetterCircleBorderPaint.setAntiAlias(true); mUserLetterCircleBorderPaint.setAntiAlias(true); } @Override // this holy mother of all stupid functions was written by looking at a hand drawn rectangle and thinking about stuff, had fun, never again public void calculateLayout(float width, float height, DisplayMetrics metrics) { // basic initializiation mWidth = width; mHeight = height; mMetrics = metrics; mUserLetterCirclePaint.setColor(Color.WHITE); mUserLetterCirclePaint.setAntiAlias(true); mAllLetterCirclePaint.setColor(Color.WHITE); mAllLetterCirclePaint.setAntiAlias(true); mUserLetterCircleBorderPaint.setStyle(Paint.Style.STROKE); mUserLetterCircleBorderPaint.setStrokeWidth(ImageUtil.convertDpToPixel(CIRCLE_BORDER_WIDTH, metrics)); mUserLetterCircleBorderPaint.setAntiAlias(true); mAllLetterCircleBorderPaint.setColor(Color.BLACK); mAllLetterCircleBorderPaint.setStyle(Paint.Style.STROKE); mAllLetterCircleBorderPaint.setStrokeWidth(ImageUtil.convertDpToPixel(CIRCLE_BORDER_WIDTH, metrics)); mAllLetterCircleBorderPaint.setAntiAlias(true); mAllLetterPaint.setColor(Color.BLACK); calculateUserLetterLayout(); calculateAllLetterLayout(); } private void calculateAllLetterLayout() { if (mMetrics == null) { return; } final float letter_base_size = ImageUtil.convertDpToPixel(ALL_LETTER_BASE_SIZE, mMetrics); final float padding_lr = ImageUtil.convertDpToPixel(PADDING_LR, mMetrics); final float padding_tb = ImageUtil.convertDpToPixel(PADDING_TB, mMetrics); final float padding_user_all = ImageUtil.convertDpToPixel(PADDING_USER_ALL, mMetrics); final float gapBetweenRows = ImageUtil.convertDpToPixel(ALL_LETTERS_ROW_PADDING, mMetrics); // -------------- all letters --------------- mAllLettersX.clear(); mAllLettersY.clear(); // calculate available height for all letters and the global y offset float heightForUserLetters = (mHeight - padding_tb - padding_user_all) * USER_LETTER_FRACTION; float heightForAllLetters = (mHeight - padding_tb - padding_user_all) * (1.f - USER_LETTER_FRACTION); float offsetAllLettersY = padding_tb / 2.f + heightForUserLetters + + padding_user_all; // calculate radius for all letters and number of requires rows to attempt to stay bigger than minimum radius int allLetterCount = mLetterClick.getAllLettersCount(); int rowCount = 1; int lettersPerRow = 0; mAllLettersCircleRadius = 0.f; if (allLetterCount > 0) { // find maximum radius that fits all letters into the area, test all rows counts float maxRadius = 0.f; int rowCountForMaxRadius = 1; int lettersPerRowForMaxRadius = allLetterCount; for (rowCount = 1; rowCount <= ALL_LETTERS_MAX_ROWS; rowCount++) { lettersPerRow = (int) Math.ceil(allLetterCount / ((float) rowCount)); mAllLettersCircleRadius = Math.min((heightForAllLetters - (rowCount - 1) * gapBetweenRows) / rowCount, (mWidth - padding_lr) / lettersPerRow ) / 2.f; // maximize the radius if (mAllLettersCircleRadius > maxRadius) { maxRadius = mAllLettersCircleRadius; rowCountForMaxRadius = rowCount; lettersPerRowForMaxRadius = lettersPerRow; } } // set the maximum radius values rowCount = rowCountForMaxRadius; mAllLettersCircleRadius = maxRadius; lettersPerRow = lettersPerRowForMaxRadius; mAllLetterPaint.setTextSize(letter_base_size); } // calculate if there is a gap between letters float widthSpaceAvailable = mWidth - padding_lr - lettersPerRow * 2.f * mAllLettersCircleRadius; float gapBetweenAllLetters = 0.f; if (widthSpaceAvailable > 0 && lettersPerRow > 1) { gapBetweenAllLetters = widthSpaceAvailable / (lettersPerRow - 1); } // add the values of all letters if (rowCount > 0) { float currX; for (int i = 0; i < rowCount; i++) { currX = padding_lr / 2.f + mAllLettersCircleRadius; float currY = offsetAllLettersY + mAllLettersCircleRadius + i * 2.f * mAllLettersCircleRadius + i * gapBetweenRows; for (int j = 0; j < Math.min(lettersPerRow, allLetterCount - lettersPerRow * i); j++) { mAllLettersX.add(currX); mAllLettersY.add(currY); currX += 2.f * mAllLettersCircleRadius + gapBetweenAllLetters; } } } } void calculateUserLetterLayout() { if (mMetrics == null) { return; } final boolean showLength = mLetterClick.showMainSolutionWordLength(); final int minLengthToShow = showLength ? mLetterClick.getMainSolutionWordLength() : 0; mUserLetterX.clear(); float letter_base_size = ImageUtil.convertDpToPixel(USER_LETTER_BASE_SIZE, mMetrics); float padding_lr = ImageUtil.convertDpToPixel(PADDING_LR, mMetrics); float padding_tb = ImageUtil.convertDpToPixel(PADDING_TB, mMetrics); float gap_lr = ImageUtil.convertDpToPixel(LETTER_MAX_GAP, mMetrics); float padding_user_all = ImageUtil.convertDpToPixel(PADDING_USER_ALL, mMetrics); float letter_max_radius = ImageUtil.convertDpToPixel(LETTER_MAX_RADIUS, mMetrics); // -------user letters --------------------------------- // calculate available height for user letters float heightForUserLetters = (mHeight - padding_tb - padding_user_all) * USER_LETTER_FRACTION; // calculate radius for user letters int userLetterCount = Math.max(mLetterClick.getUserLettersCount(), minLengthToShow); mUserLetterCircleRadius = 0.f; if (userLetterCount > 0) { userLetterCount = (showLength || mLetterClick.isStateCompleted() || userLetterCount == mLetterClick.getAllLettersCount()) ? userLetterCount : userLetterCount + 1; // simulate one more user letter so that we show that there is room for more letters in the solution word mUserLetterCircleRadius = Math.min(heightForUserLetters, (mWidth - padding_lr) / userLetterCount) / 2.f; mUserLetterCircleRadius = Math.min(mUserLetterCircleRadius, letter_max_radius); if (mUserLetterCircleRadius > 0.f) { mUserLetterPaint.setTextSize(letter_base_size * mUserLetterCircleRadius * 2 / heightForUserLetters); } } // calculate if there is a gap between letters float widthSpaceAvailable = mWidth - padding_lr - userLetterCount * 2f * mUserLetterCircleRadius; float gapBetweenUserLetters = 0.f; if (widthSpaceAvailable > 0 && userLetterCount > 1) { gapBetweenUserLetters = Math.min(gap_lr, widthSpaceAvailable / (userLetterCount - 1)); } // add the values of the user letters float currX = padding_lr / 2.f + mUserLetterCircleRadius; mUserLetterY = padding_tb / 2.f + (heightForUserLetters - 2.f*mUserLetterCircleRadius) / 2.f + mUserLetterCircleRadius; for (int i = 0; i < userLetterCount; i++) { mUserLetterX.add(currX); currX += 2f*mUserLetterCircleRadius + gapBetweenUserLetters; } } @Override public void draw(Canvas canvas) { drawUserLetters(canvas); drawAllLetters(canvas); } private void drawAllLetters(Canvas canvas) { int index = 0; float allTextOffsetY = -((mAllLetterPaint.descent() + mAllLetterPaint.ascent()) / 2); Iterator<Float> xIt = mAllLettersX.iterator(); Iterator<Float> yIt = mAllLettersY.iterator(); while (xIt.hasNext()) { float x = xIt.next(); float y = yIt.next(); if (mLetterClick.isAllLetterNotSelected(index)) { canvas.drawCircle(x, y, mAllLettersCircleRadius, mAllLetterCirclePaint); canvas.drawCircle(x, y, mAllLettersCircleRadius, mAllLetterCircleBorderPaint); String text = String.valueOf(mLetterClick.getAllLetter(index)); mAllLetterPaint.getTextBounds(text, 0, text.length(), mTextBounds); canvas.drawText(text, x - mTextBounds.exactCenterX(), y + allTextOffsetY, mAllLetterPaint); } index++; } } private void drawUserLetters(Canvas canvas) { int validUserLettersCount = mLetterClick.getUserLettersCount(); int userLetterCountToShow = mLetterClick.showMainSolutionWordLength() ? mLetterClick.getMainSolutionWordLength() : 0; userLetterCountToShow = Math.max(userLetterCountToShow, validUserLettersCount); userLetterCountToShow = Math.max(userLetterCountToShow, mUserLetterX.size()); if (userLetterCountToShow > 0) { if (mLetterClick.showCompleted() && mLetterClick.isStateCompleted()) { mUserLetterPaint.setColor(USER_LETTER_COLOR_COMPLETED); mUserLetterCircleBorderPaint.setColor(USER_LETTER_COLOR_COMPLETED); } else { mUserLetterPaint.setColor(USER_LETTER_COLOR_INCOMPLETE); mUserLetterCircleBorderPaint.setColor(USER_LETTER_COLOR_INCOMPLETE); } float userTextOffsetY = -((mUserLetterPaint.descent() + mUserLetterPaint.ascent()) / 2); for (int i = 0; i < userLetterCountToShow; i++) { if (i < validUserLettersCount) { float x = mUserLetterX.get(i); canvas.drawCircle(x, mUserLetterY, mUserLetterCircleRadius, mUserLetterCirclePaint); canvas.drawCircle(x, mUserLetterY, mUserLetterCircleRadius, mUserLetterCircleBorderPaint); String text = String.valueOf(mLetterClick.getUserLetter(i)); mUserLetterPaint.getTextBounds(text, 0, text.length(), mTextBounds); canvas.drawText(text, x - mTextBounds.exactCenterX(), mUserLetterY + userTextOffsetY, mUserLetterPaint); } else if (!mLetterClick.showMainSolutionWordLength() && i < mUserLetterX.size()) { float x = mUserLetterX.get(i); // show that there is room for more final int COUNT = Math.min(5, mLetterClick.getAllLettersCount() - mLetterClick.getUserLettersCount()); float availableWidth = 2 * mUserLetterCircleRadius; // we got space in interval [x-mUserLetterCircleRadius, x+mUserLetterCircleRadius] for COUNT circles float currX = x - mUserLetterCircleRadius; mUserLetterCircleBorderPaint.setColor(USER_LETTER_COLOR_MISSING); for (int j = 0; j < COUNT; j++) { float radius = availableWidth / 4; availableWidth -= 2 * radius; currX += radius; canvas.drawCircle(currX, mUserLetterY, radius, mUserLetterCirclePaint); canvas.drawCircle(currX, mUserLetterY, radius, mUserLetterCircleBorderPaint); currX += radius; } } else if (mLetterClick.showMainSolutionWordLength() && i < mUserLetterX.size()) { float x = mUserLetterX.get(i); canvas.drawCircle(x, mUserLetterY, mUserLetterCircleRadius, mUserLetterCirclePaint); canvas.drawCircle(x, mUserLetterY, mUserLetterCircleRadius, mUserLetterCircleBorderPaint); } } } } public List<Integer> getTouchedUserIndicies(float startEventX, float startEventY, float endEventX, float endEventY) { float userLetterHeight = mHeight * USER_LETTER_FRACTION; if (startEventY <= userLetterHeight && endEventY <= userLetterHeight) { float leftX = Math.min(startEventX, endEventX); float rightX = Math.max(startEventX, endEventX); List<Integer> indicesToRemove = new ArrayList<>(mLetterClick.getUserLettersCount()); for (int i = 0; i < mLetterClick.getUserLettersCount(); i++) { float currX = mUserLetterX.get(i); if (currX >= leftX && currX < rightX) { indicesToRemove.add(i); } } return indicesToRemove; } return null; } private double getDistance(float x1, float y1, float x2, float y2) { return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1- y2)); } public boolean executeClick(float x, float y) { // find out if any of the all letters was clicked double allLetterMinDistance = Double.MAX_VALUE; int allLetterMinDistanceIndex = -1; Iterator<Float> xIt = mAllLettersX.iterator(); Iterator<Float> yIt = mAllLettersY.iterator(); for (int index = 0; index < mLetterClick.getAllLettersCount(); index++) { // if letter is not yet selected check the distance to the actual click float xCurr = xIt.next(); float yCurr = yIt.next(); if (mLetterClick.isAllLetterNotSelected(index)) { double currDistance = getDistance(x, y, xCurr, yCurr); if (currDistance <= mAllLettersCircleRadius && mLetterClick.performAllLettersClicked(index)) { return true; } if (currDistance < allLetterMinDistance) { allLetterMinDistance = currDistance; allLetterMinDistanceIndex = index; } } } // find out if any of the user letters was clicked double userLetterMinDistance = Double.MAX_VALUE; int userLetterMinDistanceIndex = -1; for (int i = 0; i < mUserLetterX.size(); i++) { float xCurr = mUserLetterX.get(i); float yCurr = mUserLetterY; double currDistance = getDistance(x, y, xCurr, yCurr); if (currDistance <= mUserLetterCircleRadius && i < mLetterClick.getUserLettersCount() && mLetterClick.performUserLetterClick(i)) { return true; } if (currDistance < userLetterMinDistance) { userLetterMinDistance = currDistance; userLetterMinDistanceIndex = i; } } // apply click tolerance if (allLetterMinDistance < userLetterMinDistance) { if (allLetterMinDistanceIndex >= 0 && allLetterMinDistanceIndex < mLetterClick.getAllLettersCount() && allLetterMinDistance < mAllLettersCircleRadius + ImageUtil.convertDpToPixel(CLICK_DISTANCE_MAX_DELTA, mMetrics)) { if (mLetterClick.performAllLettersClicked(allLetterMinDistanceIndex)) { return true; } } } else { if (userLetterMinDistanceIndex >= 0 && userLetterMinDistanceIndex < mLetterClick.getUserLettersCount() && userLetterMinDistance < mUserLetterCircleRadius + ImageUtil.convertDpToPixel(CLICK_DISTANCE_MAX_DELTA, mMetrics)) { return mLetterClick.performUserLetterClick(userLetterMinDistanceIndex); } } return false; } }