283 lines
9.6 KiB
Java
283 lines
9.6 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.android.internal.widget;
|
|
|
|
import android.annotation.Nullable;
|
|
import android.content.Context;
|
|
import android.os.Build;
|
|
import android.os.Trace;
|
|
import android.text.BoringLayout;
|
|
import android.text.Layout;
|
|
import android.text.PrecomputedText;
|
|
import android.text.StaticLayout;
|
|
import android.text.TextUtils;
|
|
import android.text.method.TransformationMethod;
|
|
import android.util.AttributeSet;
|
|
import android.view.RemotableViewMethod;
|
|
import android.widget.RemoteViews;
|
|
import android.widget.TextView;
|
|
|
|
import com.android.internal.R;
|
|
|
|
/**
|
|
* A TextView that can float around an image on the end.
|
|
*
|
|
* @hide
|
|
*/
|
|
@RemoteViews.RemoteView
|
|
public class ImageFloatingTextView extends TextView {
|
|
|
|
/** Number of lines from the top to indent. */
|
|
private int mIndentLines = 0;
|
|
/** Whether or not there is an image to indent for. */
|
|
private boolean mHasImage = false;
|
|
|
|
/** Resolved layout direction */
|
|
private int mResolvedDirection = LAYOUT_DIRECTION_UNDEFINED;
|
|
private int mMaxLinesForHeight = -1;
|
|
private int mLayoutMaxLines = -1;
|
|
private int mImageEndMargin;
|
|
private final int mMaxLineUpperLimit;
|
|
|
|
private int mStaticLayoutCreationCountInOnMeasure = 0;
|
|
|
|
private static final boolean TRACE_ONMEASURE = Build.isDebuggable();
|
|
|
|
public ImageFloatingTextView(Context context) {
|
|
this(context, null);
|
|
}
|
|
|
|
public ImageFloatingTextView(Context context, @Nullable AttributeSet attrs) {
|
|
this(context, attrs, 0);
|
|
}
|
|
|
|
public ImageFloatingTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
|
this(context, attrs, defStyleAttr, 0);
|
|
}
|
|
|
|
public ImageFloatingTextView(Context context, AttributeSet attrs, int defStyleAttr,
|
|
int defStyleRes) {
|
|
super(context, attrs, defStyleAttr, defStyleRes);
|
|
setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL_FAST);
|
|
setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY);
|
|
mMaxLineUpperLimit =
|
|
getResources().getInteger(R.integer.config_notificationLongTextMaxLineCount);
|
|
}
|
|
|
|
@Override
|
|
protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
|
|
Layout.Alignment alignment, boolean shouldEllipsize,
|
|
TextUtils.TruncateAt effectiveEllipsize, boolean useSaved) {
|
|
if (TRACE_ONMEASURE) {
|
|
Trace.beginSection("ImageFloatingTextView#makeSingleLayout");
|
|
mStaticLayoutCreationCountInOnMeasure++;
|
|
}
|
|
TransformationMethod transformationMethod = getTransformationMethod();
|
|
CharSequence text = getText();
|
|
if (transformationMethod != null) {
|
|
text = transformationMethod.getTransformation(text, this);
|
|
}
|
|
text = text == null ? "" : text;
|
|
StaticLayout.Builder builder = StaticLayout.Builder.obtain(text, 0, text.length(),
|
|
getPaint(), wantWidth)
|
|
.setAlignment(alignment)
|
|
.setTextDirection(getTextDirectionHeuristic())
|
|
.setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier())
|
|
.setIncludePad(getIncludeFontPadding())
|
|
.setUseLineSpacingFromFallbacks(true)
|
|
.setBreakStrategy(getBreakStrategy())
|
|
.setHyphenationFrequency(getHyphenationFrequency());
|
|
int maxLines;
|
|
if (mMaxLinesForHeight > 0) {
|
|
maxLines = mMaxLinesForHeight;
|
|
} else {
|
|
maxLines = getMaxLines() >= 0 ? getMaxLines() : Integer.MAX_VALUE;
|
|
}
|
|
|
|
if (mMaxLineUpperLimit > 0) {
|
|
maxLines = Math.min(maxLines, mMaxLineUpperLimit);
|
|
}
|
|
|
|
builder.setMaxLines(maxLines);
|
|
mLayoutMaxLines = maxLines;
|
|
if (shouldEllipsize) {
|
|
builder.setEllipsize(effectiveEllipsize)
|
|
.setEllipsizedWidth(ellipsisWidth);
|
|
}
|
|
|
|
// we set the endmargin on the requested number of lines.
|
|
int[] margins = null;
|
|
if (mHasImage && mIndentLines > 0) {
|
|
margins = new int[mIndentLines + 1];
|
|
for (int i = 0; i < mIndentLines; i++) {
|
|
margins[i] = mImageEndMargin;
|
|
}
|
|
}
|
|
if (mResolvedDirection == LAYOUT_DIRECTION_RTL) {
|
|
builder.setIndents(margins, null);
|
|
} else {
|
|
builder.setIndents(null, margins);
|
|
}
|
|
|
|
final StaticLayout result = builder.build();
|
|
if (TRACE_ONMEASURE) {
|
|
trackMaxLines();
|
|
Trace.endSection();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @param imageEndMargin the end margin (in pixels) to indent the first few lines of the text
|
|
*/
|
|
@RemotableViewMethod
|
|
public void setImageEndMargin(int imageEndMargin) {
|
|
if (mImageEndMargin != imageEndMargin) {
|
|
mImageEndMargin = imageEndMargin;
|
|
invalidateTextIfIndenting();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param imageEndMarginDp the end margin (in dp) to indent the first few lines of the text
|
|
*/
|
|
@RemotableViewMethod
|
|
public void setImageEndMarginDp(float imageEndMarginDp) {
|
|
setImageEndMargin(
|
|
(int) (imageEndMarginDp * getResources().getDisplayMetrics().density));
|
|
}
|
|
|
|
@Override
|
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
if (TRACE_ONMEASURE) {
|
|
Trace.beginSection("ImageFloatingTextView#onMeasure");
|
|
}
|
|
mStaticLayoutCreationCountInOnMeasure = 0;
|
|
int availableHeight = MeasureSpec.getSize(heightMeasureSpec) - mPaddingTop - mPaddingBottom;
|
|
if (getLayout() != null && getLayout().getHeight() != availableHeight) {
|
|
// We've been measured before and the new size is different than before, lets make sure
|
|
// we reset the maximum lines, otherwise the last line of text may be partially cut off
|
|
mMaxLinesForHeight = -1;
|
|
nullLayouts();
|
|
}
|
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
Layout layout = getLayout();
|
|
if (layout.getHeight() > availableHeight) {
|
|
// With the existing layout, not all of our lines fit on the screen, let's find the
|
|
// first one that fits and ellipsize at that one.
|
|
int maxLines = layout.getLineCount();
|
|
while (maxLines > 1 && layout.getLineBottom(maxLines - 1) > availableHeight) {
|
|
maxLines--;
|
|
}
|
|
if (getMaxLines() > 0) {
|
|
maxLines = Math.min(getMaxLines(), maxLines);
|
|
}
|
|
// Only if the number of lines is different from the current layout, we recreate it.
|
|
if (maxLines != mLayoutMaxLines) {
|
|
mMaxLinesForHeight = maxLines;
|
|
nullLayouts();
|
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
}
|
|
}
|
|
|
|
|
|
if (TRACE_ONMEASURE) {
|
|
trackParameters();
|
|
Trace.endSection();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onRtlPropertiesChanged(int layoutDirection) {
|
|
super.onRtlPropertiesChanged(layoutDirection);
|
|
|
|
if (layoutDirection != mResolvedDirection && isLayoutDirectionResolved()) {
|
|
mResolvedDirection = layoutDirection;
|
|
invalidateTextIfIndenting();
|
|
}
|
|
}
|
|
|
|
private void invalidateTextIfIndenting() {
|
|
if (mHasImage && mIndentLines > 0) {
|
|
// Invalidate layout.
|
|
nullLayouts();
|
|
requestLayout();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param hasImage whether there is an image to wrap text around.
|
|
*/
|
|
@RemotableViewMethod
|
|
public void setHasImage(boolean hasImage) {
|
|
setHasImageAndNumIndentLines(hasImage, mIndentLines);
|
|
}
|
|
|
|
/**
|
|
* @param lines the number of lines at the top that should be indented by indentEnd
|
|
*/
|
|
@RemotableViewMethod
|
|
public void setNumIndentLines(int lines) {
|
|
setHasImageAndNumIndentLines(mHasImage, lines);
|
|
}
|
|
|
|
private void setHasImageAndNumIndentLines(boolean hasImage, int lines) {
|
|
int oldEffectiveLines = mHasImage ? mIndentLines : 0;
|
|
int newEffectiveLines = hasImage ? lines : 0;
|
|
mIndentLines = lines;
|
|
mHasImage = hasImage;
|
|
if (oldEffectiveLines != newEffectiveLines) {
|
|
// always invalidate layout.
|
|
nullLayouts();
|
|
requestLayout();
|
|
}
|
|
}
|
|
|
|
private void trackParameters() {
|
|
if (!TRACE_ONMEASURE) {
|
|
return;
|
|
}
|
|
Trace.setCounter("ImageFloatingView#staticLayoutCreationCount",
|
|
mStaticLayoutCreationCountInOnMeasure);
|
|
Trace.setCounter("ImageFloatingView#isPrecomputedText",
|
|
isTextAPrecomputedText());
|
|
}
|
|
/**
|
|
* @return 1 if {@link TextView#getText()} is PrecomputedText, else 0
|
|
*/
|
|
private int isTextAPrecomputedText() {
|
|
final CharSequence text = getText();
|
|
if (text == null) {
|
|
return 0;
|
|
}
|
|
|
|
if (text instanceof PrecomputedText) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private void trackMaxLines() {
|
|
if (!TRACE_ONMEASURE) {
|
|
return;
|
|
}
|
|
|
|
Trace.setCounter("ImageFloatingView#layoutMaxLines", mLayoutMaxLines);
|
|
}
|
|
}
|