blob: 0704cb8094d73b41663f004ca13fce58bd70e752 [file] [log] [blame]
/*
* 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.Trace;
import android.text.BoringLayout;
import android.text.Layout;
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;
/**
* 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;
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);
}
@Override
protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
Layout.Alignment alignment, boolean shouldEllipsize,
TextUtils.TruncateAt effectiveEllipsize, boolean useSaved) {
Trace.beginSection("ImageFloatingTextView#makeSingleLayout");
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;
}
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();
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) {
Trace.beginSection("ImageFloatingTextView#onMeasure");
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);
}
}
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();
}
}
}