package com.bluejamesbond.text;
/*
* Copyright 2015 Mathew Kurian
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* StringDocumentLayout.java
* @author Mathew Kurian
*
* From TextJustify-Android Library v2.0
* https://github.com/bluejamesbond/TextJustify-Android
*
* Please report any issues
* https://github.com/bluejamesbond/TextJustify-Android/issues
*
* Date: 1/27/15 3:35 AM
*/
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.text.TextPaint;
import java.util.List;
import java.util.ListIterator;
@SuppressWarnings("unused")
public abstract class StringDocumentLayout extends IDocumentLayout {
// Parsing objects
private Token[] tokens;
private ConcurrentModifiableLinkedList<String> chunks;
public StringDocumentLayout(Context context, TextPaint paint) {
super(context, paint);
tokens = new Token[0];
chunks = new ConcurrentModifiableLinkedList<>();
}
@Override
public boolean onMeasure(IProgress<Float> progress, ICancel<Boolean> cancelled) {
boolean done = true;
String textCpy = this.text.toString();
if (textChange) {
chunks.clear();
int start = 0;
while (start > -1) {
int next = textCpy.indexOf('\n', start);
if (next < 0) {
chunks.add(textCpy.substring(start, textCpy.length()));
} else {
chunks.add(textCpy.substring(start, next++));
}
start = next;
}
textChange = false;
}
// Empty out any existing tokens
List<Token> tokensList = new ConcurrentModifiableLinkedList<>();
Paint paint = getPaint();
paint.setTextAlign(Paint.Align.LEFT);
// Get basic settings widget properties
int lineNumber = 0;
float width = params.parentWidth - params.insetPaddingRight - params.insetPaddingLeft;
float lineHeight = getTokenAscent(0) + getTokenDescent(0);
float x, prog = 0, chunksLen = chunks.size();
float y = params.insetPaddingTop + getTokenAscent(0);
float spaceOffset = paint.measureText(" ") * params.wordSpacingMultiplier;
main:
for (String paragraph : chunks) {
if (cancelled.isCancelled()) {
done = false;
break;
}
progress.onUpdate(prog++ / chunksLen);
if (lineNumber >= params.maxLines) {
break;
}
// Start at x = 0 for drawing textCpy
x = params.insetPaddingLeft;
String trimParagraph = paragraph.trim();
// If the line contains only spaces or line breaks
if (trimParagraph.length() == 0) {
tokensList.add(new LineBreak(lineNumber++, y));
y += lineHeight;
continue;
}
float wrappedWidth = paint.measureText(trimParagraph);
// Line fits, then don't wrap
if (wrappedWidth < width) {
// activeCanvas.drawText(paragraph, x, y, paint);
tokensList.add(new SingleLine(lineNumber++, x, y, trimParagraph));
y += lineHeight;
continue;
}
// Allow leading spaces
int start = 0;
int overallCounter = 0;
ConcurrentModifiableLinkedList<Unit> units = tokenize(paragraph);
ListIterator<Unit> unitIterator = units.listIterator();
ListIterator<Unit> justifyIterator = units.listIterator();
while (true) {
x = params.insetPaddingLeft;
// Line doesn't fit, then apply wrapping
LineAnalysis format = fit(justifyIterator, start, spaceOffset, width);
int tokenCount = format.end - format.start;
boolean leftOverTokens = justifyIterator.hasNext();
if (tokenCount == 0 && leftOverTokens) {
new PlainDocumentException("Cannot fit word(s) into one line. Font size too large?")
.printStackTrace();
done = false;
break main;
}
// Draw each word here
float offset = 0;
switch (params.textAlignment) {
case CENTER: {
x += format.remainWidth / 2;
break;
}
case RIGHT: {
x += format.remainWidth;
break;
}
case JUSTIFIED: {
offset = tokenCount > 2 && leftOverTokens ?
format.remainWidth / (tokenCount - 1) : 0;
break;
}
default: {
// LEFT
}
}
for (int i = format.start; i < format.end; i++) {
Unit unit = unitIterator.next();
unit.x = x;
unit.y = y;
unit.lineNumber = lineNumber;
x += offset + paint.measureText(unit.unit) + spaceOffset;
// Add to all tokens
tokensList.add(unit);
}
// Increment to next line
y += lineHeight;
// Next line
lineNumber++;
if (lineNumber >= params.maxLines) {
break main;
}
// Check cancelled
if (cancelled.isCancelled()) {
done = false;
break;
}
// If there are more tokens leftover,
// continue
if (leftOverTokens) {
// Next start index for tokens
start = format.end;
continue;
}
// If all fit, then continue to next
// paragraph
break;
}
}
Token[] tokensArr = new Token[tokensList.size()];
tokensList.toArray(tokensArr);
tokensList.clear();
lineCount = lineNumber;
tokens = tokensArr;
params.changed = !done;
measuredHeight = (int) (y - getTokenAscent(0) + params.insetPaddingBottom);
return done;
}
@Override
public void onDraw(Canvas canvas, int startTop, int startBottom) {
int tokenStart = getTokenForVertical(startTop, TokenPosition.START_OF_LINE);
int tokenEnd = getTokenForVertical(startBottom, TokenPosition.END_OF_LINE);
for (int i = Math.max(0, tokenStart - 25); i < tokenEnd + 25 && i < tokens.length; i++) {
Token token = tokens[i];
token.draw(canvas, -startTop, paint, params);
if (params.debugging) {
if (token instanceof LineBreak) {
int lastColor = paint.getColor();
boolean lastFakeBold = paint.isFakeBoldText();
Paint.Style lastStyle = paint.getStyle();
Paint.Align lastAlign = paint.getTextAlign();
paint.setColor(Color.YELLOW);
paint.setStyle(Paint.Style.FILL);
canvas.drawRect(params.insetPaddingLeft, token.y - startTop - getTokenAscent(0), params.parentWidth - params.insetPaddingRight, token.y - startTop + getTokenDescent(0), paint);
paint.setColor(Color.BLACK);
paint.setFakeBoldText(true);
paint.setTextAlign(Paint.Align.CENTER);
canvas.drawText("LINEBREAK", params.insetPaddingLeft + (params.parentWidth - params.insetPaddingRight - params.insetPaddingLeft) / 2, token.y - startTop, paint);
paint.setStyle(lastStyle);
paint.setColor(lastColor);
paint.setTextAlign(lastAlign);
paint.setFakeBoldText(lastFakeBold);
}
}
}
}
@Override
public float getTokenAscent(int tokenIndex) {
return -paint.ascent() * params.lineHeightMultiplier;
}
@Override
public float getTokenDescent(int tokenIndex) {
return paint.descent() * params.lineHeightMultiplier;
}
@Override
public int getTokenForVertical(float y, TokenPosition position) {
int high = Math.max(0, tokens.length - 1);
int low = 0;
while (low + 1 < high) {
int mid = (high + low) / 2;
float fY = tokens[mid].getY();
if (fY > y) {
high = mid;
} else {
low = mid;
}
}
switch (position) {
default:
case START_OF_LINE: {
for (int s = low; s > 0 && tokens[s].getY() >= y; s--) {
low--;
}
return low;
}
case END_OF_LINE: {
for (int s = high; s < tokens.length && tokens[s].getY() <= y; s++) {
high++;
}
return high;
}
}
}
@Override
public int getLineForToken(int tokenIndex) {
throw new RuntimeException("Use SpannableDocumentLayout for now. Method under construction.");
}
@Override
public int getTokenStart(int tokenIndex) {
throw new RuntimeException("Use SpannableDocumentLayout for now. Method under construction.");
}
@Override
public int getTokenEnd(int tokenIndex) {
throw new RuntimeException("Use SpannableDocumentLayout for now. Method under construction.");
}
@Override
public float getTokenTopAt(int tokenIndex) {
return tokens[tokenIndex].getY();
}
@Override
public CharSequence getTokenTextAt(int index) {
return tokens[index].toString();
}
@Override
public boolean isTokenized() {
return tokens != null;
}
private ConcurrentModifiableLinkedList<Unit> tokenize(String s) {
ConcurrentModifiableLinkedList<Unit> units = new ConcurrentModifiableLinkedList<>();
// If empty string, just return one group
if (s.trim().length() <= 1) {
units.add(new Unit(s));
return units;
}
int start = 0;
boolean charSearch = s.charAt(0) == ' ';
for (int i = 1; i < s.length(); i++) {
// If the end add the word group
if (i + 1 == s.length()) {
units.add(new Unit(s.substring(start, i + 1)));
start = i + 1;
}
// Search for the start of non-space
else if (charSearch && s.charAt(i) != ' ') {
String substring = s.substring(start, i);
if (substring.length() != 0) {
units.add(new Unit(s.substring(start, i)));
}
start = i;
charSearch = false;
}
// Search for the end of non-space
else if (!charSearch && s.charAt(i) == ' ') {
units.add(new Unit(s.substring(start, i)));
start = i + 1; // Skip the space
charSearch = true;
}
}
return units;
}
/**
* Returns the length that the specified CharSequence would have if
* spaces and control characters were trimmed from the start and end,
* as by {@link String#trim}.
*/
protected int getTrimmedLength(CharSequence s, int start, int end) {
while (start < end && s.charAt(start) <= ' ') {
start++;
}
int endCpy = end;
while (endCpy > start && s.charAt(endCpy - 1) <= ' ') {
endCpy--;
}
return endCpy - start;
}
/**
* By contract, parameter "block" must not have any line breaks
*/
private LineAnalysis fit(ListIterator<Unit> iterator, int startIndex, float spaceOffset,
float availableWidth) {
int i = startIndex;
// Greedy search to see if the word
// can actually fit on a line
while (iterator.hasNext()) {
// Get word
Unit unit = iterator.next();
String word = unit.unit;
float wordWidth = paint.measureText(word);
float remainingWidth = availableWidth - wordWidth;
// Word does not fit in line
if (remainingWidth < 0 && word.trim().length() != 0) {
// Handle hyphening in the event
// the current word does not fit
if (params.hyphenated) {
float lastFormattedPartialWidth = 0.0f;
String lastFormattedPartial = null;
String lastConcatPartial = null;
String concatPartial = "";
List<String> partials = params.hyphenator.hyphenate(word);
for (String partial : partials) {
concatPartial += partial;
// Create the hyphenated word
// aka. partial
String formattedPartial = concatPartial + params.hyphen;
float formattedPartialWidth = paint
.measureText(formattedPartial);
// See if the partial fits
if (availableWidth - formattedPartialWidth > 0) {
lastFormattedPartial = formattedPartial;
lastFormattedPartialWidth = formattedPartialWidth;
lastConcatPartial = concatPartial;
}
// If the partial doesn't fit
else {
// Check if the lastPartial
// was even onUpdate
if (lastFormattedPartial != null) {
unit.unit = lastFormattedPartial;
iterator.add(new Unit(word.substring(lastConcatPartial.length())));
availableWidth -= lastFormattedPartialWidth;
iterator.previous();
return new LineAnalysis(startIndex, i + 1, availableWidth);
}
}
}
}
// Redo this word on the next run
iterator.previous();
return new LineAnalysis(startIndex, i, availableWidth + spaceOffset);
}
// Word fits in the line
else {
availableWidth -= wordWidth + spaceOffset;
// NO remaining space
if (remainingWidth == 0) {
return new LineAnalysis(startIndex, i + 1, availableWidth
+ spaceOffset);
}
}
// Increment i
i++;
}
return new LineAnalysis(startIndex, i, availableWidth + spaceOffset);
}
private static abstract class Token {
public int lineNumber;
public float y;
public Token(int lineNumber, float y) {
this.lineNumber = lineNumber;
this.y = y;
}
public float getY() {
return y;
}
public int getLineNumber() {
return lineNumber;
}
abstract void draw(Canvas canvas, float offsetY, Paint paint, LayoutParams params);
}
private static class Unit extends Token {
public float x;
public String unit;
public Unit(String unit) {
super(0, 0);
this.unit = unit;
}
public Unit(int lineNumber, float x, float y, String unit) {
super(lineNumber, y);
this.x = x;
this.unit = unit;
}
@Override
void draw(Canvas canvas, float offsetY, Paint paint, LayoutParams params) {
canvas.drawText(unit, x + params.getOffsetX(), y + params.getOffsetY() + offsetY, paint);
}
@Override
public String toString() {
return unit;
}
}
private static class LineBreak extends Token {
public LineBreak(int lineNumber, float y) {
super(lineNumber, y);
}
@Override
void draw(Canvas canvas, float offsetY, Paint paint, LayoutParams params) {
}
@Override
public String toString() {
return "\n";
}
}
private static class SingleLine extends Unit {
public SingleLine(int lineNumber, float x, float y, String unit) {
super(lineNumber, x, y, unit);
}
}
@SuppressWarnings("serial")
class PlainDocumentException extends Exception {
public PlainDocumentException(String message) {
super(message);
}
}
/**
* Class and function to process wrapping Implements a greedy algorithm to
* fit as many words as possible into one line
*/
private class LineAnalysis {
public int start;
public int end;
public float remainWidth;
public LineAnalysis(int start, int end, float remainWidth) {
this.start = start;
this.end = end;
this.remainWidth = remainWidth;
}
}
}