
com.uncopt.android.widget.text.justify.Justify Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of android.justified Show documentation
Show all versions of android.justified Show documentation
Android TextView and EditText with justified text
/*
* Copyright (C) 2013 UNCOPT LLC.
*
* 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.
*/
package com.uncopt.android.widget.text.justify;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.text.Layout;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
import android.text.style.ScaleXSpan;
import android.widget.TextView;
class Justify {
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s");
static final float DEFAULT_MAX_PROPORTION = 10f;
/**
* Adds ScaleX spans to expand widespaces and justify the lines.
* @param justified the justified TextView.
* @param textViewSpanEnds a preallocated array that will hold the span end positions.
* @param textViewSpanStarts a preallocated array that will hold the span start positions.
* @param textViewSpans a preallocated array that will hold the spans.
*/
static void setupScaleSpans(final @NotNull Justified justified,
final @NotNull int[] textViewSpanStarts,
final @NotNull int[] textViewSpanEnds,
final @NotNull ScaleSpan[] textViewSpans) {
final TextView textView = justified.getTextView();
final CharSequence text = textView.getText();
// The text should be a spannable already because we set a movement method.
if (!(text instanceof Spannable)) return;
final Spannable spannable = (Spannable)text;
final int length = spannable.length();
if (length == 0) return;
// Remove any existing ScaleXSpan (from a previous pass).
final ScaleSpan[] scaleSpans = spannable.getSpans(0, spannable.length(), ScaleSpan.class);
if (scaleSpans != null) {
for (final ScaleSpan span: scaleSpans) {
spannable.removeSpan(span);
}
}
// We use the layout to get line widths before justification
final Layout layout = textView.getLayout();
assert(layout != null);
final int count = layout.getLineCount();
if (count < 2) return;
// Layout line widths do not include the padding
final int want = textView.getMeasuredWidth() -
textView.getCompoundPaddingLeft() - textView.getCompoundPaddingRight();
// We won't justify lines if it requires expanding the spaces beyond the maximum proportion.
final float maxProportion;
if (textView instanceof Justified) {
maxProportion = ((Justified)textView).getMaxProportion();
}
else {
maxProportion = DEFAULT_MAX_PROPORTION;
}
for (int line=0; line lineStart &&
// Character.isWhitespace(spannable.charAt(visibleLineEnd-1))) --visibleLineEnd;
// Don't justify lines that only contain whitespace
if (visibleLineEnd == lineStart) continue;
// Layout line width
// final float w = layout.getLineWidth(line); // Works fine, but only for API > 11
// final float w = layout.getLineMax(line); // Doesn't work well
final float w = Layout.getDesiredWidth(spannable, lineStart, lineEnd, layout.getPaint());
// Remaining space to fill
int remaining = (int)Math.floor(want - w);
if (remaining > 0) {
// Make sure trailing whitespace doesn't use any space by setting its scaleX to 0
if (visibleLineEnd < lineEnd) {
spannable.setSpan(
new ScaleXSpan(0f),
visibleLineEnd,
lineEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
// Line text
final CharSequence sub = spannable.subSequence(lineStart, visibleLineEnd);
// Accumulated total whitespace width
float spaceWidth = 0f;
// Number of whitespace sections
int n = 0;
// Find whitespace sections and store their start and end positions
final Matcher matcher = WHITESPACE_PATTERN.matcher(sub);
while (matcher.find()) {
final int matchStart = matcher.start();
final int matchEnd = matcher.end();
// If the line starts with whitespace, it's probably an indentation
// and we don't want to expand indentation space to preserve alignment
if (matchStart == 0) continue;
// skip single thin and hair spaces, as well as a single non breaking space
if ((matchEnd - matchStart) == 1) {
final int c = sub.charAt(matchStart);
if (c == '\u200a' || c == '\u2009' || c == '\u00a0') continue;
}
assert(layout.getPaint() != null);
final float matchWidth =
layout.getPaint().measureText(spannable, lineStart + matchStart, lineStart + matchEnd);
spaceWidth += matchWidth;
textViewSpanStarts[n] = matchStart;
textViewSpanEnds[n] = matchEnd;
++n;
}
if (n > textViewSpans.length) {
n = textViewSpans.length;
}
// Excess space is distributed evenly
// (with the same proportions for all whitespace sections)
final float proportion = (spaceWidth + remaining) / spaceWidth;
// Don't justify the line if we can't do it without expanding whitespaces too much.
if (proportion > maxProportion) continue;
// Add ScaleX spans on the whitespace sections we want to expand.
for (int span=0; span 0) {
if (++loop == 4) {
android.util.Log.e("ERROR",
"Could not compensate for excess space (" + excess + "px).");
}
// Clear the spans from the previous attempt.
for (int span=0; span
© 2015 - 2025 Weber Informatics LLC | Privacy Policy