mirror of
https://github.com/material-components/material-components-android.git
synced 2026-01-16 18:01:42 +08:00
1304 lines
44 KiB
Java
1304 lines
44 KiB
Java
/*
|
|
* Copyright (C) 2015 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 android.text.Layout.Alignment.ALIGN_CENTER;
|
|
import static android.text.Layout.Alignment.ALIGN_NORMAL;
|
|
import static android.text.Layout.Alignment.ALIGN_OPPOSITE;
|
|
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
|
|
import static androidx.core.util.Preconditions.checkNotNull;
|
|
import static java.lang.Math.max;
|
|
import static java.lang.Math.min;
|
|
|
|
import android.animation.TimeInterpolator;
|
|
import android.content.res.ColorStateList;
|
|
import android.content.res.Configuration;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.Color;
|
|
import android.graphics.Paint;
|
|
import android.graphics.Rect;
|
|
import android.graphics.RectF;
|
|
import android.graphics.Typeface;
|
|
import android.os.Build;
|
|
import android.os.Build.VERSION;
|
|
import android.os.Build.VERSION_CODES;
|
|
import android.text.Layout.Alignment;
|
|
import android.text.StaticLayout;
|
|
import android.text.TextPaint;
|
|
import android.text.TextUtils;
|
|
import android.text.TextUtils.TruncateAt;
|
|
import android.util.Log;
|
|
import android.view.Gravity;
|
|
import android.view.View;
|
|
import androidx.annotation.ColorInt;
|
|
import androidx.annotation.FloatRange;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.RequiresApi;
|
|
import androidx.annotation.RestrictTo;
|
|
import androidx.core.math.MathUtils;
|
|
import androidx.core.text.TextDirectionHeuristicsCompat;
|
|
import androidx.core.view.GravityCompat;
|
|
import androidx.core.view.ViewCompat;
|
|
import com.google.android.material.animation.AnimationUtils;
|
|
import com.google.android.material.color.MaterialColors;
|
|
import com.google.android.material.internal.StaticLayoutBuilderCompat.StaticLayoutBuilderCompatException;
|
|
import com.google.android.material.resources.CancelableFontCallback;
|
|
import com.google.android.material.resources.CancelableFontCallback.ApplyFont;
|
|
import com.google.android.material.resources.TextAppearance;
|
|
import com.google.android.material.resources.TypefaceUtils;
|
|
|
|
/**
|
|
* Helper class for rendering and animating collapsed text.
|
|
*
|
|
* @hide
|
|
*/
|
|
@RestrictTo(LIBRARY_GROUP)
|
|
public final class CollapsingTextHelper {
|
|
|
|
// Pre-JB-MR2 doesn't support HW accelerated canvas scaled text so we will workaround it
|
|
// by using our own texture
|
|
private static final boolean USE_SCALING_TEXTURE = Build.VERSION.SDK_INT < 18;
|
|
private static final String TAG = "CollapsingTextHelper";
|
|
private static final String ELLIPSIS_NORMAL = "\u2026"; // HORIZONTAL ELLIPSIS (...)
|
|
|
|
private static final float FADE_MODE_THRESHOLD_FRACTION_RELATIVE = 0.5f;
|
|
|
|
private static final boolean DEBUG_DRAW = false;
|
|
@NonNull private static final Paint DEBUG_DRAW_PAINT;
|
|
|
|
static {
|
|
DEBUG_DRAW_PAINT = DEBUG_DRAW ? new Paint() : null;
|
|
if (DEBUG_DRAW_PAINT != null) {
|
|
DEBUG_DRAW_PAINT.setAntiAlias(true);
|
|
DEBUG_DRAW_PAINT.setColor(Color.MAGENTA);
|
|
}
|
|
}
|
|
|
|
private final View view;
|
|
|
|
private float expandedFraction;
|
|
private boolean fadeModeEnabled;
|
|
private float fadeModeStartFraction;
|
|
private float fadeModeThresholdFraction;
|
|
private int currentOffsetY;
|
|
|
|
@NonNull private final Rect expandedBounds;
|
|
@NonNull private final Rect collapsedBounds;
|
|
@NonNull private final RectF currentBounds;
|
|
private int expandedTextGravity = Gravity.CENTER_VERTICAL;
|
|
private int collapsedTextGravity = Gravity.CENTER_VERTICAL;
|
|
private float expandedTextSize = 15;
|
|
private float collapsedTextSize = 15;
|
|
private ColorStateList expandedTextColor;
|
|
private ColorStateList collapsedTextColor;
|
|
private int expandedLineCount;
|
|
|
|
private float expandedDrawY;
|
|
private float collapsedDrawY;
|
|
private float expandedDrawX;
|
|
private float collapsedDrawX;
|
|
private float currentDrawX;
|
|
private float currentDrawY;
|
|
private Typeface collapsedTypeface;
|
|
private Typeface collapsedTypefaceBold;
|
|
private Typeface collapsedTypefaceDefault;
|
|
private Typeface expandedTypeface;
|
|
private Typeface expandedTypefaceBold;
|
|
private Typeface expandedTypefaceDefault;
|
|
private Typeface currentTypeface;
|
|
private CancelableFontCallback expandedFontCallback;
|
|
private CancelableFontCallback collapsedFontCallback;
|
|
|
|
private TruncateAt titleTextEllipsize = TruncateAt.END;
|
|
|
|
@Nullable private CharSequence text;
|
|
@Nullable private CharSequence textToDraw;
|
|
private boolean isRtl;
|
|
private boolean isRtlTextDirectionHeuristicsEnabled = true;
|
|
|
|
private boolean useTexture;
|
|
@Nullable private Bitmap expandedTitleTexture;
|
|
private Paint texturePaint;
|
|
|
|
private float scale;
|
|
private float currentTextSize;
|
|
private float currentShadowRadius;
|
|
private float currentShadowDx;
|
|
private float currentShadowDy;
|
|
private int currentShadowColor;
|
|
|
|
private int[] state;
|
|
|
|
private boolean boundsChanged;
|
|
|
|
@NonNull private final TextPaint textPaint;
|
|
@NonNull private final TextPaint tmpPaint;
|
|
|
|
private TimeInterpolator positionInterpolator;
|
|
private TimeInterpolator textSizeInterpolator;
|
|
|
|
private float collapsedShadowRadius;
|
|
private float collapsedShadowDx;
|
|
private float collapsedShadowDy;
|
|
private ColorStateList collapsedShadowColor;
|
|
|
|
private float expandedShadowRadius;
|
|
private float expandedShadowDx;
|
|
private float expandedShadowDy;
|
|
private ColorStateList expandedShadowColor;
|
|
|
|
private float collapsedLetterSpacing;
|
|
private float expandedLetterSpacing;
|
|
private float currentLetterSpacing;
|
|
|
|
private StaticLayout textLayout;
|
|
private float collapsedTextWidth;
|
|
private float collapsedTextBlend;
|
|
private float expandedTextBlend;
|
|
private CharSequence textToDrawCollapsed;
|
|
private int maxLines = 1;
|
|
private float lineSpacingAdd = StaticLayoutBuilderCompat.DEFAULT_LINE_SPACING_ADD;
|
|
private float lineSpacingMultiplier = StaticLayoutBuilderCompat.DEFAULT_LINE_SPACING_MULTIPLIER;
|
|
private int hyphenationFrequency = StaticLayoutBuilderCompat.DEFAULT_HYPHENATION_FREQUENCY;
|
|
@Nullable private StaticLayoutBuilderConfigurer staticLayoutBuilderConfigurer;
|
|
|
|
public CollapsingTextHelper(View view) {
|
|
this.view = view;
|
|
|
|
textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG);
|
|
tmpPaint = new TextPaint(textPaint);
|
|
|
|
collapsedBounds = new Rect();
|
|
expandedBounds = new Rect();
|
|
currentBounds = new RectF();
|
|
|
|
fadeModeThresholdFraction = calculateFadeModeThresholdFraction();
|
|
maybeUpdateFontWeightAdjustment(view.getContext().getResources().getConfiguration());
|
|
}
|
|
|
|
public void setTextSizeInterpolator(TimeInterpolator interpolator) {
|
|
textSizeInterpolator = interpolator;
|
|
recalculate();
|
|
}
|
|
|
|
public void setPositionInterpolator(TimeInterpolator interpolator) {
|
|
positionInterpolator = interpolator;
|
|
recalculate();
|
|
}
|
|
|
|
@Nullable
|
|
public TimeInterpolator getPositionInterpolator() {
|
|
return positionInterpolator;
|
|
}
|
|
|
|
public void setExpandedTextSize(float textSize) {
|
|
if (expandedTextSize != textSize) {
|
|
expandedTextSize = textSize;
|
|
recalculate();
|
|
}
|
|
}
|
|
|
|
public void setCollapsedTextSize(float textSize) {
|
|
if (collapsedTextSize != textSize) {
|
|
collapsedTextSize = textSize;
|
|
recalculate();
|
|
}
|
|
}
|
|
|
|
public void setCollapsedTextColor(ColorStateList textColor) {
|
|
if (collapsedTextColor != textColor) {
|
|
collapsedTextColor = textColor;
|
|
recalculate();
|
|
}
|
|
}
|
|
|
|
public void setExpandedTextColor(ColorStateList textColor) {
|
|
if (expandedTextColor != textColor) {
|
|
expandedTextColor = textColor;
|
|
recalculate();
|
|
}
|
|
}
|
|
|
|
public void setCollapsedAndExpandedTextColor(@Nullable ColorStateList textColor) {
|
|
if (collapsedTextColor != textColor || expandedTextColor != textColor) {
|
|
collapsedTextColor = textColor;
|
|
expandedTextColor = textColor;
|
|
recalculate();
|
|
}
|
|
}
|
|
|
|
public void setExpandedLetterSpacing(float letterSpacing) {
|
|
if (expandedLetterSpacing != letterSpacing) {
|
|
expandedLetterSpacing = letterSpacing;
|
|
recalculate();
|
|
}
|
|
}
|
|
|
|
public void setExpandedBounds(int left, int top, int right, int bottom) {
|
|
if (!rectEquals(expandedBounds, left, top, right, bottom)) {
|
|
expandedBounds.set(left, top, right, bottom);
|
|
boundsChanged = true;
|
|
}
|
|
}
|
|
|
|
public void setExpandedBounds(@NonNull Rect bounds) {
|
|
setExpandedBounds(bounds.left, bounds.top, bounds.right, bounds.bottom);
|
|
}
|
|
|
|
public void setCollapsedBounds(int left, int top, int right, int bottom) {
|
|
if (!rectEquals(collapsedBounds, left, top, right, bottom)) {
|
|
collapsedBounds.set(left, top, right, bottom);
|
|
boundsChanged = true;
|
|
}
|
|
}
|
|
|
|
public void setCollapsedBounds(@NonNull Rect bounds) {
|
|
setCollapsedBounds(bounds.left, bounds.top, bounds.right, bounds.bottom);
|
|
}
|
|
|
|
public void getCollapsedTextActualBounds(@NonNull RectF bounds, int labelWidth, int textGravity) {
|
|
isRtl = calculateIsRtl(text);
|
|
bounds.left = max(getCollapsedTextLeftBound(labelWidth, textGravity), collapsedBounds.left);
|
|
bounds.top = collapsedBounds.top;
|
|
bounds.right =
|
|
min(getCollapsedTextRightBound(bounds, labelWidth, textGravity), collapsedBounds.right);
|
|
bounds.bottom = collapsedBounds.top + getCollapsedTextHeight();
|
|
}
|
|
|
|
private float getCollapsedTextLeftBound(int width, int gravity) {
|
|
if (gravity == Gravity.CENTER
|
|
|| (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.CENTER_HORIZONTAL) {
|
|
return width / 2f - collapsedTextWidth / 2;
|
|
} else if ((gravity & Gravity.END) == Gravity.END
|
|
|| (gravity & Gravity.RIGHT) == Gravity.RIGHT) {
|
|
return isRtl ? collapsedBounds.left : (collapsedBounds.right - collapsedTextWidth);
|
|
} else {
|
|
return isRtl ? (collapsedBounds.right - collapsedTextWidth) : collapsedBounds.left;
|
|
}
|
|
}
|
|
|
|
private float getCollapsedTextRightBound(@NonNull RectF bounds, int width, int gravity) {
|
|
if (gravity == Gravity.CENTER
|
|
|| (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.CENTER_HORIZONTAL) {
|
|
return width / 2f + collapsedTextWidth / 2;
|
|
} else if ((gravity & Gravity.END) == Gravity.END
|
|
|| (gravity & Gravity.RIGHT) == Gravity.RIGHT) {
|
|
return isRtl ? (bounds.left + collapsedTextWidth) : collapsedBounds.right;
|
|
} else {
|
|
return isRtl ? collapsedBounds.right : (bounds.left + collapsedTextWidth);
|
|
}
|
|
}
|
|
|
|
public float getExpandedTextHeight() {
|
|
getTextPaintExpanded(tmpPaint);
|
|
// Return expanded height measured from the baseline.
|
|
return -tmpPaint.ascent();
|
|
}
|
|
|
|
public float getExpandedTextFullHeight() {
|
|
getTextPaintExpanded(tmpPaint);
|
|
// Return expanded height measured from the baseline.
|
|
return -tmpPaint.ascent() + tmpPaint.descent();
|
|
}
|
|
|
|
public float getCollapsedTextHeight() {
|
|
getTextPaintCollapsed(tmpPaint);
|
|
// Return collapsed height measured from the baseline.
|
|
return -tmpPaint.ascent();
|
|
}
|
|
|
|
public void setCurrentOffsetY(int currentOffsetY) {
|
|
this.currentOffsetY = currentOffsetY;
|
|
}
|
|
|
|
public void setFadeModeStartFraction(float fadeModeStartFraction) {
|
|
this.fadeModeStartFraction = fadeModeStartFraction;
|
|
fadeModeThresholdFraction = calculateFadeModeThresholdFraction();
|
|
}
|
|
|
|
private float calculateFadeModeThresholdFraction() {
|
|
return fadeModeStartFraction
|
|
+ (1 - fadeModeStartFraction) * FADE_MODE_THRESHOLD_FRACTION_RELATIVE;
|
|
}
|
|
|
|
public void setFadeModeEnabled(boolean fadeModeEnabled) {
|
|
this.fadeModeEnabled = fadeModeEnabled;
|
|
}
|
|
|
|
private void getTextPaintExpanded(@NonNull TextPaint textPaint) {
|
|
textPaint.setTextSize(expandedTextSize);
|
|
textPaint.setTypeface(expandedTypeface);
|
|
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
|
|
textPaint.setLetterSpacing(expandedLetterSpacing);
|
|
}
|
|
}
|
|
|
|
private void getTextPaintCollapsed(@NonNull TextPaint textPaint) {
|
|
textPaint.setTextSize(collapsedTextSize);
|
|
textPaint.setTypeface(collapsedTypeface);
|
|
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
|
|
textPaint.setLetterSpacing(collapsedLetterSpacing);
|
|
}
|
|
}
|
|
|
|
public void setExpandedTextGravity(int gravity) {
|
|
if (expandedTextGravity != gravity) {
|
|
expandedTextGravity = gravity;
|
|
recalculate();
|
|
}
|
|
}
|
|
|
|
public int getExpandedTextGravity() {
|
|
return expandedTextGravity;
|
|
}
|
|
|
|
public void setCollapsedTextGravity(int gravity) {
|
|
if (collapsedTextGravity != gravity) {
|
|
collapsedTextGravity = gravity;
|
|
recalculate();
|
|
}
|
|
}
|
|
|
|
public int getCollapsedTextGravity() {
|
|
return collapsedTextGravity;
|
|
}
|
|
|
|
public void setCollapsedTextAppearance(int resId) {
|
|
TextAppearance textAppearance = new TextAppearance(view.getContext(), resId);
|
|
|
|
if (textAppearance.getTextColor() != null) {
|
|
collapsedTextColor = textAppearance.getTextColor();
|
|
}
|
|
if (textAppearance.getTextSize() != 0) {
|
|
collapsedTextSize = textAppearance.getTextSize();
|
|
}
|
|
if (textAppearance.shadowColor != null) {
|
|
collapsedShadowColor = textAppearance.shadowColor;
|
|
}
|
|
collapsedShadowDx = textAppearance.shadowDx;
|
|
collapsedShadowDy = textAppearance.shadowDy;
|
|
collapsedShadowRadius = textAppearance.shadowRadius;
|
|
collapsedLetterSpacing = textAppearance.letterSpacing;
|
|
|
|
// Cancel pending async fetch, if any, and replace with a new one.
|
|
if (collapsedFontCallback != null) {
|
|
collapsedFontCallback.cancel();
|
|
}
|
|
collapsedFontCallback =
|
|
new CancelableFontCallback(
|
|
new ApplyFont() {
|
|
@Override
|
|
public void apply(Typeface font) {
|
|
setCollapsedTypeface(font);
|
|
}
|
|
},
|
|
textAppearance.getFallbackFont());
|
|
textAppearance.getFontAsync(view.getContext(), collapsedFontCallback);
|
|
|
|
recalculate();
|
|
}
|
|
|
|
public void setExpandedTextAppearance(int resId) {
|
|
TextAppearance textAppearance = new TextAppearance(view.getContext(), resId);
|
|
if (textAppearance.getTextColor() != null) {
|
|
expandedTextColor = textAppearance.getTextColor();
|
|
}
|
|
if (textAppearance.getTextSize() != 0) {
|
|
expandedTextSize = textAppearance.getTextSize();
|
|
}
|
|
if (textAppearance.shadowColor != null) {
|
|
expandedShadowColor = textAppearance.shadowColor;
|
|
}
|
|
expandedShadowDx = textAppearance.shadowDx;
|
|
expandedShadowDy = textAppearance.shadowDy;
|
|
expandedShadowRadius = textAppearance.shadowRadius;
|
|
expandedLetterSpacing = textAppearance.letterSpacing;
|
|
|
|
// Cancel pending async fetch, if any, and replace with a new one.
|
|
if (expandedFontCallback != null) {
|
|
expandedFontCallback.cancel();
|
|
}
|
|
expandedFontCallback =
|
|
new CancelableFontCallback(
|
|
new ApplyFont() {
|
|
@Override
|
|
public void apply(Typeface font) {
|
|
setExpandedTypeface(font);
|
|
}
|
|
},
|
|
textAppearance.getFallbackFont());
|
|
textAppearance.getFontAsync(view.getContext(), expandedFontCallback);
|
|
|
|
recalculate();
|
|
}
|
|
|
|
public void setTitleTextEllipsize(@NonNull TruncateAt ellipsize) {
|
|
titleTextEllipsize = ellipsize;
|
|
recalculate();
|
|
}
|
|
|
|
@NonNull
|
|
public TruncateAt getTitleTextEllipsize() {
|
|
return titleTextEllipsize;
|
|
}
|
|
|
|
public void setCollapsedTypeface(Typeface typeface) {
|
|
if (setCollapsedTypefaceInternal(typeface)) {
|
|
recalculate();
|
|
}
|
|
}
|
|
|
|
public void setExpandedTypeface(Typeface typeface) {
|
|
if (setExpandedTypefaceInternal(typeface)) {
|
|
recalculate();
|
|
}
|
|
}
|
|
|
|
public void setTypefaces(Typeface typeface) {
|
|
boolean collapsedFontChanged = setCollapsedTypefaceInternal(typeface);
|
|
boolean expandedFontChanged = setExpandedTypefaceInternal(typeface);
|
|
if (collapsedFontChanged || expandedFontChanged) {
|
|
recalculate();
|
|
}
|
|
}
|
|
|
|
@SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView
|
|
private boolean setCollapsedTypefaceInternal(Typeface typeface) {
|
|
// Explicit Typeface setting cancels pending async fetch, if any, to avoid old font overriding
|
|
// already updated one when async op comes back after a while.
|
|
if (collapsedFontCallback != null) {
|
|
collapsedFontCallback.cancel();
|
|
}
|
|
if (collapsedTypefaceDefault != typeface) {
|
|
collapsedTypefaceDefault = typeface;
|
|
collapsedTypefaceBold =
|
|
TypefaceUtils.maybeCopyWithFontWeightAdjustment(
|
|
view.getContext().getResources().getConfiguration(), typeface);
|
|
collapsedTypeface =
|
|
collapsedTypefaceBold == null ? collapsedTypefaceDefault : collapsedTypefaceBold;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView
|
|
private boolean setExpandedTypefaceInternal(Typeface typeface) {
|
|
// Explicit Typeface setting cancels pending async fetch, if any, to avoid old font overriding
|
|
// already updated one when async op comes back after a while.
|
|
if (expandedFontCallback != null) {
|
|
expandedFontCallback.cancel();
|
|
}
|
|
if (expandedTypefaceDefault != typeface) {
|
|
expandedTypefaceDefault = typeface;
|
|
expandedTypefaceBold =
|
|
TypefaceUtils.maybeCopyWithFontWeightAdjustment(
|
|
view.getContext().getResources().getConfiguration(), typeface);
|
|
expandedTypeface =
|
|
expandedTypefaceBold == null ? expandedTypefaceDefault : expandedTypefaceBold;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public Typeface getCollapsedTypeface() {
|
|
return collapsedTypeface != null ? collapsedTypeface : Typeface.DEFAULT;
|
|
}
|
|
|
|
public Typeface getExpandedTypeface() {
|
|
return expandedTypeface != null ? expandedTypeface : Typeface.DEFAULT;
|
|
}
|
|
|
|
public void maybeUpdateFontWeightAdjustment(@NonNull Configuration configuration) {
|
|
if (VERSION.SDK_INT >= VERSION_CODES.S) {
|
|
if (collapsedTypefaceDefault != null) {
|
|
collapsedTypefaceBold =
|
|
TypefaceUtils.maybeCopyWithFontWeightAdjustment(
|
|
configuration, collapsedTypefaceDefault);
|
|
}
|
|
if (expandedTypefaceDefault != null) {
|
|
expandedTypefaceBold =
|
|
TypefaceUtils.maybeCopyWithFontWeightAdjustment(configuration, expandedTypefaceDefault);
|
|
}
|
|
collapsedTypeface =
|
|
collapsedTypefaceBold != null ? collapsedTypefaceBold : collapsedTypefaceDefault;
|
|
expandedTypeface =
|
|
expandedTypefaceBold != null ? expandedTypefaceBold : expandedTypefaceDefault;
|
|
recalculate(/* forceRecalculate= */ true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the value indicating the current scroll value. This decides how much of the background will
|
|
* be displayed, as well as the title metrics/positioning.
|
|
*
|
|
* <p>A value of {@code 0.0} indicates that the layout is fully expanded. A value of {@code 1.0}
|
|
* indicates that the layout is fully collapsed.
|
|
*/
|
|
public void setExpansionFraction(float fraction) {
|
|
fraction = MathUtils.clamp(fraction, 0f, 1f);
|
|
|
|
if (fraction != expandedFraction) {
|
|
expandedFraction = fraction;
|
|
calculateCurrentOffsets();
|
|
}
|
|
}
|
|
|
|
public final boolean setState(final int[] state) {
|
|
this.state = state;
|
|
|
|
if (isStateful()) {
|
|
recalculate();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public final boolean isStateful() {
|
|
return (collapsedTextColor != null && collapsedTextColor.isStateful())
|
|
|| (expandedTextColor != null && expandedTextColor.isStateful());
|
|
}
|
|
|
|
public float getFadeModeThresholdFraction() {
|
|
return fadeModeThresholdFraction;
|
|
}
|
|
|
|
public float getExpansionFraction() {
|
|
return expandedFraction;
|
|
}
|
|
|
|
public float getCollapsedTextSize() {
|
|
return collapsedTextSize;
|
|
}
|
|
|
|
public float getExpandedTextSize() {
|
|
return expandedTextSize;
|
|
}
|
|
|
|
public void setRtlTextDirectionHeuristicsEnabled(boolean rtlTextDirectionHeuristicsEnabled) {
|
|
isRtlTextDirectionHeuristicsEnabled = rtlTextDirectionHeuristicsEnabled;
|
|
}
|
|
|
|
public boolean isRtlTextDirectionHeuristicsEnabled() {
|
|
return isRtlTextDirectionHeuristicsEnabled;
|
|
}
|
|
|
|
private void calculateCurrentOffsets() {
|
|
calculateOffsets(expandedFraction);
|
|
}
|
|
|
|
private void calculateOffsets(final float fraction) {
|
|
interpolateBounds(fraction);
|
|
float textBlendFraction;
|
|
if (fadeModeEnabled) {
|
|
if (fraction < fadeModeThresholdFraction) {
|
|
textBlendFraction = 0F;
|
|
currentDrawX = expandedDrawX;
|
|
currentDrawY = expandedDrawY;
|
|
|
|
setInterpolatedTextSize(/* fraction= */ 0);
|
|
} else {
|
|
textBlendFraction = 1F;
|
|
currentDrawX = collapsedDrawX;
|
|
currentDrawY = collapsedDrawY - max(0, currentOffsetY);
|
|
|
|
setInterpolatedTextSize(/* fraction= */ 1);
|
|
}
|
|
} else {
|
|
textBlendFraction = fraction;
|
|
currentDrawX = lerp(expandedDrawX, collapsedDrawX, fraction, positionInterpolator);
|
|
currentDrawY = lerp(expandedDrawY, collapsedDrawY, fraction, positionInterpolator);
|
|
|
|
setInterpolatedTextSize(fraction);
|
|
}
|
|
|
|
setCollapsedTextBlend(
|
|
1 - lerp(0, 1, 1 - fraction, AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR));
|
|
setExpandedTextBlend(lerp(1, 0, fraction, AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR));
|
|
|
|
if (collapsedTextColor != expandedTextColor) {
|
|
// If the collapsed and expanded text colors are different, blend them based on the
|
|
// fraction
|
|
textPaint.setColor(
|
|
blendARGB(
|
|
getCurrentExpandedTextColor(), getCurrentCollapsedTextColor(), textBlendFraction));
|
|
} else {
|
|
textPaint.setColor(getCurrentCollapsedTextColor());
|
|
}
|
|
|
|
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
|
|
if (collapsedLetterSpacing != expandedLetterSpacing) {
|
|
textPaint.setLetterSpacing(
|
|
lerp(
|
|
expandedLetterSpacing,
|
|
collapsedLetterSpacing,
|
|
fraction,
|
|
AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR));
|
|
} else {
|
|
textPaint.setLetterSpacing(collapsedLetterSpacing);
|
|
}
|
|
}
|
|
|
|
// Calculates paint parameters for shadow layer.
|
|
currentShadowRadius = lerp(expandedShadowRadius, collapsedShadowRadius, fraction, null);
|
|
currentShadowDx = lerp(expandedShadowDx, collapsedShadowDx, fraction, null);
|
|
currentShadowDy = lerp(expandedShadowDy, collapsedShadowDy, fraction, null);
|
|
currentShadowColor =
|
|
blendARGB(
|
|
getCurrentColor(expandedShadowColor), getCurrentColor(collapsedShadowColor), fraction);
|
|
textPaint.setShadowLayer(
|
|
currentShadowRadius, currentShadowDx, currentShadowDy, currentShadowColor);
|
|
|
|
if (fadeModeEnabled) {
|
|
int originalAlpha = textPaint.getAlpha();
|
|
|
|
// Calculates new alpha as a ratio of original alpha based on position.
|
|
int textAlpha = (int) (calculateFadeModeTextAlpha(fraction) * originalAlpha);
|
|
|
|
textPaint.setAlpha(textAlpha);
|
|
}
|
|
|
|
ViewCompat.postInvalidateOnAnimation(view);
|
|
}
|
|
|
|
private float calculateFadeModeTextAlpha(@FloatRange(from = 0.0, to = 1.0) float fraction) {
|
|
if (fraction <= fadeModeThresholdFraction) {
|
|
return AnimationUtils.lerp(
|
|
/* startValue= */ 1,
|
|
/* endValue= */ 0,
|
|
/* startFraction= */ fadeModeStartFraction,
|
|
/* endFraction= */ fadeModeThresholdFraction,
|
|
fraction);
|
|
} else {
|
|
return AnimationUtils.lerp(
|
|
/* startValue= */ 0,
|
|
/* endValue= */ 1,
|
|
/* startFraction= */ fadeModeThresholdFraction,
|
|
/* endFraction= */ 1,
|
|
fraction);
|
|
}
|
|
}
|
|
|
|
@ColorInt
|
|
private int getCurrentExpandedTextColor() {
|
|
return getCurrentColor(expandedTextColor);
|
|
}
|
|
|
|
@ColorInt
|
|
public int getCurrentCollapsedTextColor() {
|
|
return getCurrentColor(collapsedTextColor);
|
|
}
|
|
|
|
@ColorInt
|
|
private int getCurrentColor(@Nullable ColorStateList colorStateList) {
|
|
if (colorStateList == null) {
|
|
return 0;
|
|
}
|
|
if (state != null) {
|
|
return colorStateList.getColorForState(state, 0);
|
|
}
|
|
return colorStateList.getDefaultColor();
|
|
}
|
|
|
|
private void calculateBaseOffsets(boolean forceRecalculate) {
|
|
// We then calculate the collapsed text size, using the same logic
|
|
calculateUsingTextSize(/* fraction= */ 1, forceRecalculate);
|
|
if (textToDraw != null && textLayout != null) {
|
|
textToDrawCollapsed =
|
|
TextUtils.ellipsize(textToDraw, textPaint, textLayout.getWidth(), titleTextEllipsize);
|
|
}
|
|
if (textToDrawCollapsed != null) {
|
|
collapsedTextWidth = measureTextWidth(textPaint, textToDrawCollapsed);
|
|
} else {
|
|
collapsedTextWidth = 0;
|
|
}
|
|
final int collapsedAbsGravity =
|
|
GravityCompat.getAbsoluteGravity(
|
|
collapsedTextGravity,
|
|
isRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR);
|
|
|
|
switch (collapsedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) {
|
|
case Gravity.BOTTOM:
|
|
collapsedDrawY = collapsedBounds.bottom + textPaint.ascent();
|
|
break;
|
|
case Gravity.TOP:
|
|
collapsedDrawY = collapsedBounds.top;
|
|
break;
|
|
case Gravity.CENTER_VERTICAL:
|
|
default:
|
|
float textOffset = (textPaint.descent() - textPaint.ascent()) / 2;
|
|
collapsedDrawY = collapsedBounds.centerY() - textOffset;
|
|
break;
|
|
}
|
|
|
|
switch (collapsedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
|
|
case Gravity.CENTER_HORIZONTAL:
|
|
collapsedDrawX = collapsedBounds.centerX() - (collapsedTextWidth / 2);
|
|
break;
|
|
case Gravity.RIGHT:
|
|
collapsedDrawX = collapsedBounds.right - collapsedTextWidth;
|
|
break;
|
|
case Gravity.LEFT:
|
|
default:
|
|
collapsedDrawX = collapsedBounds.left;
|
|
break;
|
|
}
|
|
|
|
calculateUsingTextSize(/* fraction= */ 0, forceRecalculate);
|
|
float expandedTextHeight = textLayout != null ? textLayout.getHeight() : 0;
|
|
float expandedTextWidth = 0;
|
|
if (textLayout != null && maxLines > 1) {
|
|
expandedTextWidth = textLayout.getWidth();
|
|
} else if (textToDraw != null) {
|
|
expandedTextWidth = measureTextWidth(textPaint, textToDraw);
|
|
}
|
|
expandedLineCount = textLayout != null ? textLayout.getLineCount() : 0;
|
|
|
|
final int expandedAbsGravity =
|
|
GravityCompat.getAbsoluteGravity(
|
|
expandedTextGravity,
|
|
isRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR);
|
|
switch (expandedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) {
|
|
case Gravity.BOTTOM:
|
|
expandedDrawY = expandedBounds.bottom - expandedTextHeight + textPaint.descent();
|
|
break;
|
|
case Gravity.TOP:
|
|
expandedDrawY = expandedBounds.top;
|
|
break;
|
|
case Gravity.CENTER_VERTICAL:
|
|
default:
|
|
float textOffset = expandedTextHeight / 2;
|
|
expandedDrawY = expandedBounds.centerY() - textOffset;
|
|
break;
|
|
}
|
|
|
|
switch (expandedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
|
|
case Gravity.CENTER_HORIZONTAL:
|
|
expandedDrawX = expandedBounds.centerX() - (expandedTextWidth / 2);
|
|
break;
|
|
case Gravity.RIGHT:
|
|
expandedDrawX = expandedBounds.right - expandedTextWidth;
|
|
break;
|
|
case Gravity.LEFT:
|
|
default:
|
|
expandedDrawX = expandedBounds.left;
|
|
break;
|
|
}
|
|
|
|
// The bounds have changed so we need to clear the texture
|
|
clearTexture();
|
|
// Now reset the text size back to the original
|
|
setInterpolatedTextSize(expandedFraction);
|
|
}
|
|
|
|
private float measureTextWidth(TextPaint textPaint, CharSequence textToDraw) {
|
|
return textPaint.measureText(textToDraw, 0, textToDraw.length());
|
|
}
|
|
|
|
private void interpolateBounds(float fraction) {
|
|
if (fadeModeEnabled) {
|
|
currentBounds.set(fraction < fadeModeThresholdFraction ? expandedBounds : collapsedBounds);
|
|
} else {
|
|
currentBounds.left =
|
|
lerp(expandedBounds.left, collapsedBounds.left, fraction, positionInterpolator);
|
|
currentBounds.top = lerp(expandedDrawY, collapsedDrawY, fraction, positionInterpolator);
|
|
currentBounds.right =
|
|
lerp(expandedBounds.right, collapsedBounds.right, fraction, positionInterpolator);
|
|
currentBounds.bottom =
|
|
lerp(expandedBounds.bottom, collapsedBounds.bottom, fraction, positionInterpolator);
|
|
}
|
|
}
|
|
|
|
private void setCollapsedTextBlend(float blend) {
|
|
collapsedTextBlend = blend;
|
|
ViewCompat.postInvalidateOnAnimation(view);
|
|
}
|
|
|
|
private void setExpandedTextBlend(float blend) {
|
|
expandedTextBlend = blend;
|
|
ViewCompat.postInvalidateOnAnimation(view);
|
|
}
|
|
|
|
public void draw(@NonNull Canvas canvas) {
|
|
final int saveCount = canvas.save();
|
|
// Compute where to draw textLayout for this frame
|
|
if (textToDraw != null && currentBounds.width() > 0 && currentBounds.height() > 0) {
|
|
textPaint.setTextSize(currentTextSize);
|
|
float x = currentDrawX;
|
|
float y = currentDrawY;
|
|
final boolean drawTexture = useTexture && expandedTitleTexture != null;
|
|
|
|
if (DEBUG_DRAW) {
|
|
// Just a debug tool, which drawn a magenta rect in the text bounds
|
|
canvas.drawRect(
|
|
x,
|
|
y,
|
|
x + textLayout.getWidth() * scale,
|
|
y + textLayout.getHeight() * scale,
|
|
DEBUG_DRAW_PAINT);
|
|
}
|
|
|
|
if (scale != 1f && !fadeModeEnabled) {
|
|
canvas.scale(scale, scale, x, y);
|
|
}
|
|
|
|
if (drawTexture) {
|
|
// If we should use a texture, draw it instead of text
|
|
canvas.drawBitmap(expandedTitleTexture, x, y, texturePaint);
|
|
canvas.restoreToCount(saveCount);
|
|
return;
|
|
}
|
|
|
|
if (shouldDrawMultiline()
|
|
&& (!fadeModeEnabled || expandedFraction > fadeModeThresholdFraction)) {
|
|
drawMultilineTransition(canvas, currentDrawX - textLayout.getLineStart(0), y);
|
|
} else {
|
|
canvas.translate(x, y);
|
|
textLayout.draw(canvas);
|
|
}
|
|
|
|
canvas.restoreToCount(saveCount);
|
|
}
|
|
}
|
|
|
|
private boolean shouldDrawMultiline() {
|
|
return maxLines > 1 && (!isRtl || fadeModeEnabled) && !useTexture;
|
|
}
|
|
|
|
private void drawMultilineTransition(@NonNull Canvas canvas, float currentExpandedX, float y) {
|
|
int originalAlpha = textPaint.getAlpha();
|
|
// position text appropriately
|
|
canvas.translate(currentExpandedX, y);
|
|
|
|
if (!fadeModeEnabled) {
|
|
// Expanded text (when not in fade mode, because in fade mode at this point the expanded text
|
|
// has been fully faded out, so there's no need to try to draw it again)
|
|
textPaint.setAlpha((int) (expandedTextBlend * originalAlpha));
|
|
// Workaround for API 31(+). Paint applies an inverse alpha of Paint object on the shadow
|
|
// layer when collapsing mode is scale and shadow color is opaque. The workaround is to set
|
|
// the shadow not opaque. Then Paint will respect to the color's alpha. Applying the shadow
|
|
// color for expanded text.
|
|
if (VERSION.SDK_INT >= VERSION_CODES.S) {
|
|
textPaint.setShadowLayer(
|
|
currentShadowRadius,
|
|
currentShadowDx,
|
|
currentShadowDy,
|
|
MaterialColors.compositeARGBWithAlpha(currentShadowColor, textPaint.getAlpha()));
|
|
}
|
|
textLayout.draw(canvas);
|
|
}
|
|
|
|
// Collapsed text
|
|
if (!fadeModeEnabled) {
|
|
// Only change the collapsed text alpha when not in fade mode, because when in fade mode it
|
|
// will be precalculated based on the current fraction in calculateOffsets()
|
|
textPaint.setAlpha((int) (collapsedTextBlend * originalAlpha));
|
|
}
|
|
// Workaround for API 31(+). Applying the shadow color for collapsed text.
|
|
if (VERSION.SDK_INT >= VERSION_CODES.S) {
|
|
textPaint.setShadowLayer(
|
|
currentShadowRadius,
|
|
currentShadowDx,
|
|
currentShadowDy,
|
|
MaterialColors.compositeARGBWithAlpha(currentShadowColor, textPaint.getAlpha()));
|
|
}
|
|
int lineBaseline = textLayout.getLineBaseline(0);
|
|
canvas.drawText(
|
|
textToDrawCollapsed,
|
|
/* start = */ 0,
|
|
textToDrawCollapsed.length(),
|
|
/* x = */ 0,
|
|
lineBaseline,
|
|
textPaint);
|
|
// Reverse workaround for API 31(+). Applying opaque shadow color after the expanded text and
|
|
// the collapsed text are drawn.
|
|
if (VERSION.SDK_INT >= VERSION_CODES.S) {
|
|
textPaint.setShadowLayer(
|
|
currentShadowRadius, currentShadowDx, currentShadowDy, currentShadowColor);
|
|
}
|
|
|
|
if (!fadeModeEnabled) {
|
|
// Remove ellipsis for Cross-section animation
|
|
String tmp = textToDrawCollapsed.toString().trim();
|
|
if (tmp.endsWith(ELLIPSIS_NORMAL)) {
|
|
tmp = tmp.substring(0, tmp.length() - 1);
|
|
}
|
|
// Cross-section between both texts (should stay at original alpha)
|
|
textPaint.setAlpha(originalAlpha);
|
|
canvas.drawText(
|
|
tmp,
|
|
/* start = */ 0,
|
|
min(textLayout.getLineEnd(0), tmp.length()),
|
|
/* x = */ 0,
|
|
lineBaseline,
|
|
textPaint);
|
|
}
|
|
}
|
|
|
|
private boolean calculateIsRtl(@NonNull CharSequence text) {
|
|
final boolean defaultIsRtl = isDefaultIsRtl();
|
|
return isRtlTextDirectionHeuristicsEnabled
|
|
? isTextDirectionHeuristicsIsRtl(text, defaultIsRtl)
|
|
: defaultIsRtl;
|
|
}
|
|
|
|
private boolean isDefaultIsRtl() {
|
|
return ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_RTL;
|
|
}
|
|
|
|
private boolean isTextDirectionHeuristicsIsRtl(@NonNull CharSequence text, boolean defaultIsRtl) {
|
|
return (defaultIsRtl
|
|
? TextDirectionHeuristicsCompat.FIRSTSTRONG_RTL
|
|
: TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR)
|
|
.isRtl(text, 0, text.length());
|
|
}
|
|
|
|
private void setInterpolatedTextSize(float fraction) {
|
|
calculateUsingTextSize(fraction);
|
|
|
|
// Use our texture if the scale isn't 1.0
|
|
useTexture = USE_SCALING_TEXTURE && scale != 1f;
|
|
|
|
if (useTexture) {
|
|
// Make sure we have an expanded texture if needed
|
|
ensureExpandedTexture();
|
|
}
|
|
|
|
ViewCompat.postInvalidateOnAnimation(view);
|
|
}
|
|
|
|
private void calculateUsingTextSize(final float fraction) {
|
|
calculateUsingTextSize(fraction, /* forceRecalculate= */ false);
|
|
}
|
|
|
|
@SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView
|
|
private void calculateUsingTextSize(final float fraction, boolean forceRecalculate) {
|
|
if (text == null) {
|
|
return;
|
|
}
|
|
|
|
float collapsedWidth = collapsedBounds.width();
|
|
float expandedWidth = expandedBounds.width();
|
|
|
|
float availableWidth;
|
|
float newTextSize;
|
|
float newLetterSpacing;
|
|
Typeface newTypeface;
|
|
|
|
if (isClose(fraction, /* targetValue= */ 1)) {
|
|
newTextSize = collapsedTextSize;
|
|
newLetterSpacing = collapsedLetterSpacing;
|
|
scale = 1f;
|
|
newTypeface = collapsedTypeface;
|
|
availableWidth = collapsedWidth;
|
|
} else {
|
|
newTextSize = expandedTextSize;
|
|
newLetterSpacing = expandedLetterSpacing;
|
|
newTypeface = expandedTypeface;
|
|
if (isClose(fraction, /* targetValue= */ 0)) {
|
|
// If we're close to the expanded text size, snap to it and use a scale of 1
|
|
scale = 1f;
|
|
} else {
|
|
// Else, we'll scale down from the expanded text size
|
|
scale =
|
|
lerp(expandedTextSize, collapsedTextSize, fraction, textSizeInterpolator)
|
|
/ expandedTextSize;
|
|
}
|
|
|
|
float textSizeRatio = collapsedTextSize / expandedTextSize;
|
|
// This is the size of the expanded bounds when it is scaled to match the
|
|
// collapsed text size
|
|
float scaledDownWidth = expandedWidth * textSizeRatio;
|
|
|
|
if (forceRecalculate || fadeModeEnabled) {
|
|
// If we're forcing a recalculate during a measure pass, use the expanded width since the
|
|
// collapsed width might not be ready yet
|
|
// Or if the fade mode is enabled, we can also just use the expanded width because when
|
|
// fading out/in there is not a continuous scale transition between expanded/collapsed text
|
|
availableWidth = expandedWidth;
|
|
} else {
|
|
// If the scaled down size is larger than the actual collapsed width, we need to
|
|
// cap the available width so that when the expanded text scales down, it matches
|
|
// the collapsed width
|
|
// Otherwise we'll just use the expanded width
|
|
|
|
availableWidth =
|
|
scaledDownWidth > collapsedWidth
|
|
? min(collapsedWidth / textSizeRatio, expandedWidth)
|
|
: expandedWidth;
|
|
}
|
|
}
|
|
|
|
boolean updateDrawText;
|
|
if (availableWidth > 0) {
|
|
boolean textSizeChanged = currentTextSize != newTextSize;
|
|
boolean letterSpacingChanged = currentLetterSpacing != newLetterSpacing;
|
|
boolean typefaceChanged = currentTypeface != newTypeface;
|
|
boolean availableWidthChanged = textLayout != null && availableWidth != textLayout.getWidth();
|
|
updateDrawText =
|
|
textSizeChanged
|
|
|| letterSpacingChanged
|
|
|| availableWidthChanged
|
|
|| typefaceChanged
|
|
|| boundsChanged;
|
|
currentTextSize = newTextSize;
|
|
currentLetterSpacing = newLetterSpacing;
|
|
currentTypeface = newTypeface;
|
|
boundsChanged = false;
|
|
// Use linear text scaling if we're scaling the canvas
|
|
textPaint.setLinearText(scale != 1f);
|
|
} else {
|
|
updateDrawText = false;
|
|
}
|
|
|
|
if (textToDraw == null || updateDrawText) {
|
|
textPaint.setTextSize(currentTextSize);
|
|
textPaint.setTypeface(currentTypeface);
|
|
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
|
|
textPaint.setLetterSpacing(currentLetterSpacing);
|
|
}
|
|
|
|
isRtl = calculateIsRtl(text);
|
|
textLayout = createStaticLayout(shouldDrawMultiline() ? maxLines : 1, availableWidth, isRtl);
|
|
textToDraw = textLayout.getText();
|
|
}
|
|
}
|
|
|
|
private StaticLayout createStaticLayout(int maxLines, float availableWidth, boolean isRtl) {
|
|
StaticLayout textLayout = null;
|
|
try {
|
|
// In multiline mode, the text alignment should be controlled by the static layout.
|
|
Alignment textAlignment = maxLines == 1 ? ALIGN_NORMAL : getMultilineTextLayoutAlignment();
|
|
textLayout =
|
|
StaticLayoutBuilderCompat.obtain(text, textPaint, (int) availableWidth)
|
|
.setEllipsize(titleTextEllipsize)
|
|
.setIsRtl(isRtl)
|
|
.setAlignment(textAlignment)
|
|
.setIncludePad(false)
|
|
.setMaxLines(maxLines)
|
|
.setLineSpacing(lineSpacingAdd, lineSpacingMultiplier)
|
|
.setHyphenationFrequency(hyphenationFrequency)
|
|
.setStaticLayoutBuilderConfigurer(staticLayoutBuilderConfigurer)
|
|
.build();
|
|
} catch (StaticLayoutBuilderCompatException e) {
|
|
Log.e(TAG, e.getCause().getMessage(), e);
|
|
}
|
|
|
|
return checkNotNull(textLayout);
|
|
}
|
|
|
|
private Alignment getMultilineTextLayoutAlignment() {
|
|
int absoluteGravity =
|
|
GravityCompat.getAbsoluteGravity(
|
|
expandedTextGravity,
|
|
isRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR);
|
|
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
|
|
case Gravity.CENTER_HORIZONTAL:
|
|
return ALIGN_CENTER;
|
|
case Gravity.RIGHT:
|
|
return isRtl ? ALIGN_NORMAL : ALIGN_OPPOSITE;
|
|
default:
|
|
return isRtl ? ALIGN_OPPOSITE : ALIGN_NORMAL;
|
|
}
|
|
}
|
|
|
|
private void ensureExpandedTexture() {
|
|
if (expandedTitleTexture != null || expandedBounds.isEmpty() || TextUtils.isEmpty(textToDraw)) {
|
|
return;
|
|
}
|
|
|
|
calculateOffsets(0f);
|
|
int width = textLayout.getWidth();
|
|
int height = textLayout.getHeight();
|
|
|
|
if (width <= 0 || height <= 0) {
|
|
return;
|
|
}
|
|
|
|
expandedTitleTexture = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
|
Canvas c = new Canvas(expandedTitleTexture);
|
|
textLayout.draw(c);
|
|
|
|
if (texturePaint == null) {
|
|
// Make sure we have a paint
|
|
texturePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
|
|
}
|
|
}
|
|
|
|
public void recalculate() {
|
|
recalculate(/* forceRecalculate= */ false);
|
|
}
|
|
|
|
public void recalculate(boolean forceRecalculate) {
|
|
if ((view.getHeight() > 0 && view.getWidth() > 0) || forceRecalculate) {
|
|
// If we've already been laid out, calculate everything now otherwise we'll wait
|
|
// until a layout
|
|
calculateBaseOffsets(forceRecalculate);
|
|
calculateCurrentOffsets();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the title to display
|
|
*
|
|
* @param text
|
|
*/
|
|
public void setText(@Nullable CharSequence text) {
|
|
if (text == null || !TextUtils.equals(this.text, text)) {
|
|
this.text = text;
|
|
textToDraw = null;
|
|
clearTexture();
|
|
recalculate();
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
public CharSequence getText() {
|
|
return text;
|
|
}
|
|
|
|
private void clearTexture() {
|
|
if (expandedTitleTexture != null) {
|
|
expandedTitleTexture.recycle();
|
|
expandedTitleTexture = null;
|
|
}
|
|
}
|
|
|
|
public void setMaxLines(int maxLines) {
|
|
if (maxLines != this.maxLines) {
|
|
this.maxLines = maxLines;
|
|
clearTexture();
|
|
recalculate();
|
|
}
|
|
}
|
|
|
|
public int getMaxLines() {
|
|
return maxLines;
|
|
}
|
|
|
|
/**
|
|
* Returns the current text line count.
|
|
*
|
|
* @return The current text line count.
|
|
*/
|
|
public int getLineCount() {
|
|
return textLayout != null ? textLayout.getLineCount() : 0;
|
|
}
|
|
|
|
/**
|
|
* Returns the expanded text line count.
|
|
*
|
|
* @return The expanded text line count.
|
|
*/
|
|
public int getExpandedLineCount() {
|
|
return expandedLineCount;
|
|
}
|
|
|
|
@RequiresApi(VERSION_CODES.M)
|
|
public void setLineSpacingAdd(float spacingAdd) {
|
|
this.lineSpacingAdd = spacingAdd;
|
|
}
|
|
|
|
@RequiresApi(VERSION_CODES.M)
|
|
public float getLineSpacingAdd() {
|
|
return textLayout.getSpacingAdd();
|
|
}
|
|
|
|
@RequiresApi(VERSION_CODES.M)
|
|
public void setLineSpacingMultiplier(@FloatRange(from = 0.0) float spacingMultiplier) {
|
|
this.lineSpacingMultiplier = spacingMultiplier;
|
|
}
|
|
|
|
@RequiresApi(VERSION_CODES.M)
|
|
public float getLineSpacingMultiplier() {
|
|
return textLayout.getSpacingMultiplier();
|
|
}
|
|
|
|
@RequiresApi(VERSION_CODES.M)
|
|
public void setHyphenationFrequency(int hyphenationFrequency) {
|
|
this.hyphenationFrequency = hyphenationFrequency;
|
|
}
|
|
|
|
@RequiresApi(VERSION_CODES.M)
|
|
public int getHyphenationFrequency() {
|
|
return hyphenationFrequency;
|
|
}
|
|
|
|
@RequiresApi(VERSION_CODES.M)
|
|
public void setStaticLayoutBuilderConfigurer(
|
|
@Nullable StaticLayoutBuilderConfigurer staticLayoutBuilderConfigurer) {
|
|
if (this.staticLayoutBuilderConfigurer != staticLayoutBuilderConfigurer) {
|
|
this.staticLayoutBuilderConfigurer = staticLayoutBuilderConfigurer;
|
|
recalculate(/* forceRecalculate= */ true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if {@code value} is 'close' to it's closest decimal value. Close is currently
|
|
* defined as it's difference being < 0.00001.
|
|
*/
|
|
private static boolean isClose(float value, float targetValue) {
|
|
return Math.abs(value - targetValue) < 0.00001f;
|
|
}
|
|
|
|
public ColorStateList getExpandedTextColor() {
|
|
return expandedTextColor;
|
|
}
|
|
|
|
public ColorStateList getCollapsedTextColor() {
|
|
return collapsedTextColor;
|
|
}
|
|
|
|
/**
|
|
* Blend between two ARGB colors using the given ratio.
|
|
*
|
|
* <p>A blend ratio of 0.0 will result in {@code color1}, 0.5 will give an even blend, 1.0 will
|
|
* result in {@code color2}.
|
|
*
|
|
* <p>This is different from the AndroidX implementation by rounding the blended channel values
|
|
* with {@link Math#round(float)}.
|
|
*
|
|
* @param color1 the first ARGB color
|
|
* @param color2 the second ARGB color
|
|
* @param ratio the blend ratio of {@code color1} to {@code color2}
|
|
*/
|
|
@ColorInt
|
|
private static int blendARGB(
|
|
@ColorInt int color1, @ColorInt int color2, @FloatRange(from = 0.0, to = 1.0) float ratio) {
|
|
final float inverseRatio = 1 - ratio;
|
|
float a = Color.alpha(color1) * inverseRatio + Color.alpha(color2) * ratio;
|
|
float r = Color.red(color1) * inverseRatio + Color.red(color2) * ratio;
|
|
float g = Color.green(color1) * inverseRatio + Color.green(color2) * ratio;
|
|
float b = Color.blue(color1) * inverseRatio + Color.blue(color2) * ratio;
|
|
return Color.argb(Math.round(a), Math.round(r), Math.round(g), Math.round(b));
|
|
}
|
|
|
|
private static float lerp(
|
|
float startValue, float endValue, float fraction, @Nullable TimeInterpolator interpolator) {
|
|
if (interpolator != null) {
|
|
fraction = interpolator.getInterpolation(fraction);
|
|
}
|
|
return AnimationUtils.lerp(startValue, endValue, fraction);
|
|
}
|
|
|
|
private static boolean rectEquals(@NonNull Rect r, int left, int top, int right, int bottom) {
|
|
return !(r.left != left || r.top != top || r.right != right || r.bottom != bottom);
|
|
}
|
|
}
|