marianomartin 7822b6d05e Modify CollapsingTextHelper to use a StaticLayout instead of canvas.drawText.
This will allow to support multiple lines.

PiperOrigin-RevId: 293151496
2020-02-04 17:32:07 -05:00

311 lines
9.6 KiB
Java

/*
* 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_CODES;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
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 java.lang.reflect.Constructor;
/**
* Class to create StaticLayout using StaticLayout.Builder on API23+ and a hidden StaticLayout
* constructor before that.
*
* <p>Usage:
*
* <pre>{@code
* StaticLayout staticLayout =
* StaticLayoutBuilderCompat.obtain("Lorem Ipsum", new TextPaint(), 100)
* .setAlignment(Alignment.ALIGN_NORMAL)
* .build();
* }</pre>
*
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
final class StaticLayoutBuilderCompat {
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<StaticLayout> 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 boolean includePad;
private boolean isRtl;
@Nullable private TextUtils.TruncateAt ellipsize;
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.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 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;
}
/** A method that allows to create a StaticLayout with maxLines on all supported API levels. */
public StaticLayout build() throws StaticLayoutBuilderCompatException {
if (source == null) {
source = "";
}
if (Build.VERSION.SDK_INT > VERSION_CODES.KITKAT && isRtl) {
alignment = Alignment.ALIGN_OPPOSITE;
}
int availableWidth = Math.max(0, width);
CharSequence textToDraw = TextUtils.ellipsize(source, paint, availableWidth, ellipsize);
end = Math.min(textToDraw.length(), end);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 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);
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.}
*
* <pre>{@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)
* }</pre>
*/
private void createConstructorWithReflection() throws StaticLayoutBuilderCompatException {
if (initialized) {
return;
}
try {
final Class<?> textDirClass;
boolean useRtl = isRtl && Build.VERSION.SDK_INT > VERSION_CODES.KITKAT;
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);
}
}
}