/*
* Copyright (C) 2019 The Android Open Source Project
*
* 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.google.android.material.internal;
import static androidx.core.util.Preconditions.checkNotNull;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.text.Layout;
import android.text.Layout.Alignment;
import android.text.StaticLayout;
import android.text.TextDirectionHeuristic;
import android.text.TextDirectionHeuristics;
import android.text.TextPaint;
import android.text.TextUtils;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import java.lang.reflect.Constructor;
/**
* Class to create StaticLayout using StaticLayout.Builder on API23+ and a hidden StaticLayout
* constructor before that.
*
*
Usage:
*
*
{@code
* StaticLayout staticLayout =
* StaticLayoutBuilderCompat.obtain("Lorem Ipsum", new TextPaint(), 100)
* .setAlignment(Alignment.ALIGN_NORMAL)
* .build();
* }
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
final class StaticLayoutBuilderCompat {
static final int DEFAULT_HYPHENATION_FREQUENCY =
VERSION.SDK_INT >= VERSION_CODES.M ? StaticLayout.HYPHENATION_FREQUENCY_NORMAL : 0;
// Default line spacing values to match android.text.Layout constants.
static final float DEFAULT_LINE_SPACING_ADD = 0.0f;
static final float DEFAULT_LINE_SPACING_MULTIPLIER = 1.0f;
private static final String TEXT_DIR_CLASS = "android.text.TextDirectionHeuristic";
private static final String TEXT_DIRS_CLASS = "android.text.TextDirectionHeuristics";
private static final String TEXT_DIR_CLASS_LTR = "LTR";
private static final String TEXT_DIR_CLASS_RTL = "RTL";
private static boolean initialized;
@Nullable private static Constructor constructor;
@Nullable private static Object textDirection;
private CharSequence source;
private final TextPaint paint;
private final int width;
private int start;
private int end;
private Alignment alignment;
private int maxLines;
private float lineSpacingAdd;
private float lineSpacingMultiplier;
private int hyphenationFrequency;
private boolean includePad;
private boolean isRtl;
@Nullable private TextUtils.TruncateAt ellipsize;
@Nullable private StaticLayoutBuilderConfigurer staticLayoutBuilderConfigurer;
private StaticLayoutBuilderCompat(CharSequence source, TextPaint paint, int width) {
this.source = source;
this.paint = paint;
this.width = width;
this.start = 0;
this.end = source.length();
this.alignment = Alignment.ALIGN_NORMAL;
this.maxLines = Integer.MAX_VALUE;
this.lineSpacingAdd = DEFAULT_LINE_SPACING_ADD;
this.lineSpacingMultiplier = DEFAULT_LINE_SPACING_MULTIPLIER;
this.hyphenationFrequency = DEFAULT_HYPHENATION_FREQUENCY;
this.includePad = true;
this.ellipsize = null;
}
/**
* Obtain a builder for constructing StaticLayout objects.
*
* @param source The text to be laid out, optionally with spans
* @param paint The base paint used for layout
* @param width The width in pixels
* @return a builder object used for constructing the StaticLayout
*/
@NonNull
public static StaticLayoutBuilderCompat obtain(
@NonNull CharSequence source, @NonNull TextPaint paint, @IntRange(from = 0) int width) {
return new StaticLayoutBuilderCompat(source, paint, width);
}
/**
* Set the alignment. The default is {@link Layout.Alignment#ALIGN_NORMAL}.
*
* @param alignment Alignment for the resulting {@link StaticLayout}
* @return this builder, useful for chaining
*/
@NonNull
public StaticLayoutBuilderCompat setAlignment(@NonNull Alignment alignment) {
this.alignment = alignment;
return this;
}
/**
* Set whether to include extra space beyond font ascent and descent (which is needed to avoid
* clipping in some languages, such as Arabic and Kannada). The default is {@code true}.
*
* @param includePad whether to include padding
* @return this builder, useful for chaining
* @see android.widget.TextView#setIncludeFontPadding
*/
@NonNull
public StaticLayoutBuilderCompat setIncludePad(boolean includePad) {
this.includePad = includePad;
return this;
}
/**
* Set the index of the start of the text
*
* @return this builder, useful for chaining
*/
@NonNull
public StaticLayoutBuilderCompat setStart(@IntRange(from = 0) int start) {
this.start = start;
return this;
}
/**
* Set the index + 1 of the end of the text
*
* @return this builder, useful for chaining
* @see android.widget.TextView#setIncludeFontPadding
*/
@NonNull
public StaticLayoutBuilderCompat setEnd(@IntRange(from = 0) int end) {
this.end = end;
return this;
}
/**
* Set maximum number of lines. This is particularly useful in the case of ellipsizing, where it
* changes the layout of the last line. The default is unlimited.
*
* @param maxLines maximum number of lines in the layout
* @return this builder, useful for chaining
* @see android.widget.TextView#setMaxLines
*/
@NonNull
public StaticLayoutBuilderCompat setMaxLines(@IntRange(from = 0) int maxLines) {
this.maxLines = maxLines;
return this;
}
/**
* Set the line spacing addition and multiplier frequency. Only available on API level 23+.
*
* @param spacingAdd Line spacing addition for the resulting {@link StaticLayout}
* @param lineSpacingMultiplier Line spacing multiplier for the resulting {@link StaticLayout}
* @return this builder, useful for chaining
* @see android.widget.TextView#setLineSpacing(float, float)
*/
@NonNull
public StaticLayoutBuilderCompat setLineSpacing(float spacingAdd, float lineSpacingMultiplier) {
this.lineSpacingAdd = spacingAdd;
this.lineSpacingMultiplier = lineSpacingMultiplier;
return this;
}
/**
* Set the hyphenation frequency. Only available on API level 23+.
*
* @param hyphenationFrequency Hyphenation frequency for the resulting {@link StaticLayout}
* @return this builder, useful for chaining
* @see android.widget.TextView#setHyphenationFrequency(int)
*/
@NonNull
public StaticLayoutBuilderCompat setHyphenationFrequency(int hyphenationFrequency) {
this.hyphenationFrequency = hyphenationFrequency;
return this;
}
/**
* Set ellipsizing on the layout. Causes words that are longer than the view is wide, or exceeding
* the number of lines (see #setMaxLines).
*
* @param ellipsize type of ellipsis behavior
* @return this builder, useful for chaining
* @see android.widget.TextView#setEllipsize
*/
@NonNull
public StaticLayoutBuilderCompat setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) {
this.ellipsize = ellipsize;
return this;
}
/**
* Set the {@link StaticLayoutBuilderConfigurer} which allows additional custom configurations on
* the static layout.
*/
@NonNull
public StaticLayoutBuilderCompat setStaticLayoutBuilderConfigurer(
@Nullable StaticLayoutBuilderConfigurer staticLayoutBuilderConfigurer) {
this.staticLayoutBuilderConfigurer = staticLayoutBuilderConfigurer;
return this;
}
/** A method that allows to create a StaticLayout with maxLines on all supported API levels. */
public StaticLayout build() throws StaticLayoutBuilderCompatException {
if (source == null) {
source = "";
}
int availableWidth = Math.max(0, width);
CharSequence textToDraw = source;
if (maxLines == 1) {
textToDraw = TextUtils.ellipsize(source, paint, availableWidth, ellipsize);
}
end = Math.min(textToDraw.length(), end);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (isRtl && maxLines == 1) {
alignment = Alignment.ALIGN_OPPOSITE;
}
// Marshmallow introduced StaticLayout.Builder which allows us not to use
// the hidden constructor.
StaticLayout.Builder builder =
StaticLayout.Builder.obtain(
textToDraw, start, end, paint, availableWidth);
builder.setAlignment(alignment);
builder.setIncludePad(includePad);
TextDirectionHeuristic textDirectionHeuristic = isRtl
? TextDirectionHeuristics.RTL
: TextDirectionHeuristics.LTR;
builder.setTextDirection(textDirectionHeuristic);
if (ellipsize != null) {
builder.setEllipsize(ellipsize);
}
builder.setMaxLines(maxLines);
if (lineSpacingAdd != DEFAULT_LINE_SPACING_ADD
|| lineSpacingMultiplier != DEFAULT_LINE_SPACING_MULTIPLIER) {
builder.setLineSpacing(lineSpacingAdd, lineSpacingMultiplier);
}
if (maxLines > 1) {
builder.setHyphenationFrequency(hyphenationFrequency);
}
if (staticLayoutBuilderConfigurer != null) {
staticLayoutBuilderConfigurer.configure(builder);
}
return builder.build();
}
createConstructorWithReflection();
// Use the hidden constructor on older API levels.
try {
return checkNotNull(constructor)
.newInstance(
textToDraw,
start,
end,
paint,
availableWidth,
alignment,
checkNotNull(textDirection),
1.0f,
0.0f,
includePad,
null,
availableWidth,
maxLines);
} catch (Exception cause) {
throw new StaticLayoutBuilderCompatException(cause);
}
}
/**
* set constructor to this hidden {@link StaticLayout constructor.}
*
* {@code
* StaticLayout(
* CharSequence source,
* int bufstart,
* int bufend,
* TextPaint paint,
* int outerwidth,
* Alignment align,
* TextDirectionHeuristic textDir,
* float spacingmult,
* float spacingadd,
* boolean includepad,
* TextUtils.TruncateAt ellipsize,
* int ellipsizedWidth,
* int maxLines)
* }
*/
private void createConstructorWithReflection() throws StaticLayoutBuilderCompatException {
if (initialized) {
return;
}
try {
final Class> textDirClass;
boolean useRtl = isRtl && Build.VERSION.SDK_INT >= VERSION_CODES.M;
if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
textDirClass = TextDirectionHeuristic.class;
textDirection = useRtl ? TextDirectionHeuristics.RTL : TextDirectionHeuristics.LTR;
} else {
ClassLoader loader = StaticLayoutBuilderCompat.class.getClassLoader();
String textDirClassName = isRtl ? TEXT_DIR_CLASS_RTL : TEXT_DIR_CLASS_LTR;
textDirClass = loader.loadClass(TEXT_DIR_CLASS);
Class> textDirsClass = loader.loadClass(TEXT_DIRS_CLASS);
textDirection = textDirsClass.getField(textDirClassName).get(textDirsClass);
}
final Class>[] signature =
new Class>[] {
CharSequence.class,
int.class,
int.class,
TextPaint.class,
int.class,
Alignment.class,
textDirClass,
float.class,
float.class,
boolean.class,
TextUtils.TruncateAt.class,
int.class,
int.class
};
constructor = StaticLayout.class.getDeclaredConstructor(signature);
constructor.setAccessible(true);
initialized = true;
} catch (Exception cause) {
throw new StaticLayoutBuilderCompatException(cause);
}
}
public StaticLayoutBuilderCompat setIsRtl(boolean isRtl) {
this.isRtl = isRtl;
return this;
}
static class StaticLayoutBuilderCompatException extends Exception {
StaticLayoutBuilderCompatException(Throwable cause) {
super("Error thrown initializing StaticLayout " + cause.getMessage(), cause);
}
}
}