package com.yidianling.common.view.ticker; import android.graphics.Canvas; import android.graphics.Paint; import java.util.Map; /** * Represents a column of characters to be drawn on the screen. This class primarily handles * animating within the column from one character to the next and drawing all of the intermediate * states. */ class RxTickerColumn { private static final int UNKNOWN_START_INDEX = -1; private static final int UNKNOWN_END_INDEX = -2; private final char[] characterList; private final Map<Character, Integer> characterIndicesMap; private final RxTickerDrawMetrics metrics; private char currentChar = RxTickerUtils.EMPTY_CHAR; private char targetChar = RxTickerUtils.EMPTY_CHAR; // The indices characters simply signify what positions are for the current and target // characters in the assigned characterList. This tells us how to animate from the current // to the target characters. private int startIndex; private int endIndex; // Drawing state variables that get updated whenever animation progress gets updated. private int bottomCharIndex; private float bottomDelta; private float charHeight; // Drawing state variables for handling size transition private float sourceWidth, currentWidth, targetWidth, minimumRequiredWidth; // The bottom delta variables signifies the vertical offset that the bottom drawn character // is seeing. If the delta is 0, it means that the character is perfectly centered. If the // delta is negative, it means that the bottom character is poking out from the bottom and // part of the top character is visible. The delta should never be positive because it means // that the bottom character is not actually the bottom character. private float currentBottomDelta; private float previousBottomDelta; private int directionAdjustment; RxTickerColumn(char[] characterList, Map<Character, Integer> characterIndicesMap, RxTickerDrawMetrics metrics) { this.characterList = characterList; this.characterIndicesMap = characterIndicesMap; this.metrics = metrics; } /** * Tells the column that the next character it should show is {@param targetChar}. This can * change can either be animated or instant depending on the animation progress set by * {@link #setAnimationProgress(float)}. */ void setTargetChar(char targetChar) { // Set the current and target characters for the animation this.targetChar = targetChar; this.sourceWidth = this.currentWidth; this.targetWidth = metrics.getCharWidth(targetChar); this.minimumRequiredWidth = Math.max(this.sourceWidth, this.targetWidth); // Calculate the current indices setCharacterIndices(); final boolean scrollDown = endIndex >= startIndex; directionAdjustment = scrollDown ? 1 : -1; // Save the currentBottomDelta as previousBottomDelta in case this call to setTargetChar // interrupted a previously running animation. The deltas will then be used to compute // offset so that the interruption feels smooth on the UI. previousBottomDelta = currentBottomDelta; currentBottomDelta = 0f; } char getCurrentChar() { return currentChar; } char getTargetChar() { return targetChar; } float getCurrentWidth() { return currentWidth; } float getMinimumRequiredWidth() { return minimumRequiredWidth; } /** * A helper method for populating {@link #startIndex} and {@link #endIndex} given the * current and target characters for the animation. */ private void setCharacterIndices() { startIndex = characterIndicesMap.containsKey(currentChar) ? characterIndicesMap.get(currentChar) : UNKNOWN_START_INDEX; endIndex = characterIndicesMap.containsKey(targetChar) ? characterIndicesMap.get(targetChar) : UNKNOWN_END_INDEX; } void onAnimationEnd() { minimumRequiredWidth = currentWidth; } void setAnimationProgress(float animationProgress) { if (animationProgress == 1f) { // Animation finished (or never started), set to stable state. this.currentChar = this.targetChar; currentBottomDelta = 0f; previousBottomDelta = 0f; } final float charHeight = metrics.getCharHeight(); // First let's find the total height of this column between the start and end chars. final float totalHeight = charHeight * Math.abs(endIndex - startIndex); // The current base is then the part of the total height that we have progressed to // from the animation. For example, there might be 5 characters, each character is // 2px tall, so the totalHeight is 10. If we are at 50% progress, then our baseline // in this column is at 5 out of 10 (which is the 3rd character with a -50% offset // to the baseline). final float currentBase = animationProgress * totalHeight; // Given the current base, we now can find which character should drawn on the bottom. // Note that this position is a float. For example, if the bottomCharPosition is // 4.5, it means that the bottom character is the 4th character, and it has a -50% // offset relative to the baseline. final float bottomCharPosition = currentBase / charHeight; // By subtracting away the integer part of bottomCharPosition, we now have the // percentage representation of the bottom char's offset. final float bottomCharOffsetPercentage = bottomCharPosition - (int) bottomCharPosition; // We might have interrupted a previous animation if previousBottomDelta is not 0f. // If that's the case, we need to take this delta into account so that the previous // character offset won't be wiped away when we start a new animation. // We multiply by the inverse percentage so that the offset contribution from the delta // progresses along with the rest of the animation (from full delta to 0). final float additionalDelta = previousBottomDelta * (1f - animationProgress); // Now, using the bottom char's offset percentage and the delta we have from the // previous animation, we can now compute what's the actual offset of the bottom // character in the column relative to the baseline. bottomDelta = bottomCharOffsetPercentage * charHeight * directionAdjustment + additionalDelta; // Figure out what the actual character index is in the characterList, and then // draw the character with the computed offset. bottomCharIndex = startIndex + ((int) bottomCharPosition * directionAdjustment); this.charHeight = charHeight; this.currentWidth = sourceWidth + (targetWidth - sourceWidth) * animationProgress; } /** * Draw the current state of the column as it's animating from one character in the list * to another. This method will take into account various factors such as animation * progress and the previously interrupted animation state to render the characters * in the correct position on the canvas. */ void draw(Canvas canvas, Paint textPaint) { if (drawText(canvas, textPaint, characterList, bottomCharIndex, bottomDelta)) { // Save the current drawing state in case our animation gets interrupted if (bottomCharIndex >= 0) { currentChar = characterList[bottomCharIndex]; } else if (bottomCharIndex == UNKNOWN_END_INDEX) { currentChar = targetChar; } currentBottomDelta = bottomDelta; } // Draw the corresponding top and bottom characters if applicable drawText(canvas, textPaint, characterList, bottomCharIndex + 1, bottomDelta - charHeight); // Drawing the bottom character here might seem counter-intuitive because we've been // computing for the bottom character this entire time. But the bottom character // computed above might actually be above the baseline if we interrupted a previous // animation that gave us a positive additionalDelta. drawText(canvas, textPaint, characterList, bottomCharIndex - 1, bottomDelta + charHeight); } /** * @return whether the text was successfully drawn on the canvas */ private boolean drawText(Canvas canvas, Paint textPaint, char[] characterList, int index, float verticalOffset) { if (index >= 0 && index < characterList.length) { canvas.drawText(characterList, index, 1, 0f, verticalOffset, textPaint); return true; } else if (startIndex == UNKNOWN_START_INDEX && index == UNKNOWN_START_INDEX) { canvas.drawText(Character.toString(currentChar), 0, 1, 0f, verticalOffset, textPaint); return true; } else if (endIndex == UNKNOWN_END_INDEX && index == UNKNOWN_END_INDEX) { canvas.drawText(Character.toString(targetChar), 0, 1, 0f, verticalOffset, textPaint); return true; } return false; } }