1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.internal.widget;
18 
19 import android.annotation.Nullable;
20 import android.content.Context;
21 import android.os.Build;
22 import android.os.Trace;
23 import android.text.BoringLayout;
24 import android.text.Layout;
25 import android.text.PrecomputedText;
26 import android.text.StaticLayout;
27 import android.text.TextUtils;
28 import android.text.method.TransformationMethod;
29 import android.util.AttributeSet;
30 import android.view.RemotableViewMethod;
31 import android.widget.RemoteViews;
32 import android.widget.TextView;
33 
34 import com.android.internal.R;
35 
36 /**
37  * A TextView that can float around an image on the end.
38  *
39  * @hide
40  */
41 @RemoteViews.RemoteView
42 public class ImageFloatingTextView extends TextView {
43 
44     /** Number of lines from the top to indent. */
45     private int mIndentLines = 0;
46     /** Whether or not there is an image to indent for. */
47     private boolean mHasImage = false;
48 
49     /** Resolved layout direction */
50     private int mResolvedDirection = LAYOUT_DIRECTION_UNDEFINED;
51     private int mMaxLinesForHeight = -1;
52     private int mLayoutMaxLines = -1;
53     private int mImageEndMargin;
54     private final int mMaxLineUpperLimit;
55 
56     private int mStaticLayoutCreationCountInOnMeasure = 0;
57 
58     private static final boolean TRACE_ONMEASURE = Build.isDebuggable();
59 
ImageFloatingTextView(Context context)60     public ImageFloatingTextView(Context context) {
61         this(context, null);
62     }
63 
ImageFloatingTextView(Context context, @Nullable AttributeSet attrs)64     public ImageFloatingTextView(Context context, @Nullable AttributeSet attrs) {
65         this(context, attrs, 0);
66     }
67 
ImageFloatingTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)68     public ImageFloatingTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
69         this(context, attrs, defStyleAttr, 0);
70     }
71 
ImageFloatingTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)72     public ImageFloatingTextView(Context context, AttributeSet attrs, int defStyleAttr,
73             int defStyleRes) {
74         super(context, attrs, defStyleAttr, defStyleRes);
75         setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL_FAST);
76         setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY);
77         mMaxLineUpperLimit =
78                 getResources().getInteger(R.integer.config_notificationLongTextMaxLineCount);
79     }
80 
81     @Override
makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth, Layout.Alignment alignment, boolean shouldEllipsize, TextUtils.TruncateAt effectiveEllipsize, boolean useSaved)82     protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
83             Layout.Alignment alignment, boolean shouldEllipsize,
84             TextUtils.TruncateAt effectiveEllipsize, boolean useSaved) {
85         if (TRACE_ONMEASURE) {
86             Trace.beginSection("ImageFloatingTextView#makeSingleLayout");
87             mStaticLayoutCreationCountInOnMeasure++;
88         }
89         TransformationMethod transformationMethod = getTransformationMethod();
90         CharSequence text = getText();
91         if (transformationMethod != null) {
92             text = transformationMethod.getTransformation(text, this);
93         }
94         text = text == null ? "" : text;
95         StaticLayout.Builder builder = StaticLayout.Builder.obtain(text, 0, text.length(),
96                         getPaint(), wantWidth)
97                 .setAlignment(alignment)
98                 .setTextDirection(getTextDirectionHeuristic())
99                 .setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier())
100                 .setIncludePad(getIncludeFontPadding())
101                 .setUseLineSpacingFromFallbacks(true)
102                 .setBreakStrategy(getBreakStrategy())
103                 .setHyphenationFrequency(getHyphenationFrequency());
104         int maxLines;
105         if (mMaxLinesForHeight > 0) {
106             maxLines = mMaxLinesForHeight;
107         } else {
108             maxLines = getMaxLines() >= 0 ? getMaxLines() : Integer.MAX_VALUE;
109         }
110 
111         if (mMaxLineUpperLimit > 0) {
112             maxLines = Math.min(maxLines, mMaxLineUpperLimit);
113         }
114 
115         builder.setMaxLines(maxLines);
116         mLayoutMaxLines = maxLines;
117         if (shouldEllipsize) {
118             builder.setEllipsize(effectiveEllipsize)
119                     .setEllipsizedWidth(ellipsisWidth);
120         }
121 
122         // we set the endmargin on the requested number of lines.
123         int[] margins = null;
124         if (mHasImage && mIndentLines > 0) {
125             margins = new int[mIndentLines + 1];
126             for (int i = 0; i < mIndentLines; i++) {
127                 margins[i] = mImageEndMargin;
128             }
129         }
130         if (mResolvedDirection == LAYOUT_DIRECTION_RTL) {
131             builder.setIndents(margins, null);
132         } else {
133             builder.setIndents(null, margins);
134         }
135 
136         final StaticLayout result = builder.build();
137         if (TRACE_ONMEASURE) {
138             trackMaxLines();
139             Trace.endSection();
140         }
141         return result;
142     }
143 
144     /**
145      * @param imageEndMargin the end margin (in pixels) to indent the first few lines of the text
146      */
147     @RemotableViewMethod
setImageEndMargin(int imageEndMargin)148     public void setImageEndMargin(int imageEndMargin) {
149         if (mImageEndMargin != imageEndMargin) {
150             mImageEndMargin = imageEndMargin;
151             invalidateTextIfIndenting();
152         }
153     }
154 
155     /**
156      * @param imageEndMarginDp the end margin (in dp) to indent the first few lines of the text
157      */
158     @RemotableViewMethod
setImageEndMarginDp(float imageEndMarginDp)159     public void setImageEndMarginDp(float imageEndMarginDp) {
160         setImageEndMargin(
161                 (int) (imageEndMarginDp * getResources().getDisplayMetrics().density));
162     }
163 
164     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)165     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
166         if (TRACE_ONMEASURE) {
167             Trace.beginSection("ImageFloatingTextView#onMeasure");
168         }
169         mStaticLayoutCreationCountInOnMeasure = 0;
170         int availableHeight = MeasureSpec.getSize(heightMeasureSpec) - mPaddingTop - mPaddingBottom;
171         if (getLayout() != null && getLayout().getHeight() != availableHeight) {
172             // We've been measured before and the new size is different than before, lets make sure
173             // we reset the maximum lines, otherwise the last line of text may be partially cut off
174             mMaxLinesForHeight = -1;
175             nullLayouts();
176         }
177         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
178         Layout layout = getLayout();
179         if (layout.getHeight() > availableHeight) {
180             // With the existing layout, not all of our lines fit on the screen, let's find the
181             // first one that fits and ellipsize at that one.
182             int maxLines = layout.getLineCount();
183             while (maxLines > 1 && layout.getLineBottom(maxLines - 1) > availableHeight) {
184                 maxLines--;
185             }
186             if (getMaxLines() > 0) {
187                 maxLines = Math.min(getMaxLines(), maxLines);
188             }
189             // Only if the number of lines is different from the current layout, we recreate it.
190             if (maxLines != mLayoutMaxLines) {
191                 mMaxLinesForHeight = maxLines;
192                 nullLayouts();
193                 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
194             }
195         }
196 
197 
198         if (TRACE_ONMEASURE) {
199             trackParameters();
200             Trace.endSection();
201         }
202     }
203 
204     @Override
onRtlPropertiesChanged(int layoutDirection)205     public void onRtlPropertiesChanged(int layoutDirection) {
206         super.onRtlPropertiesChanged(layoutDirection);
207 
208         if (layoutDirection != mResolvedDirection && isLayoutDirectionResolved()) {
209             mResolvedDirection = layoutDirection;
210             invalidateTextIfIndenting();
211         }
212     }
213 
invalidateTextIfIndenting()214     private void invalidateTextIfIndenting() {
215         if (mHasImage && mIndentLines > 0) {
216             // Invalidate layout.
217             nullLayouts();
218             requestLayout();
219         }
220     }
221 
222     /**
223      * @param hasImage whether there is an image to wrap text around.
224      */
225     @RemotableViewMethod
setHasImage(boolean hasImage)226     public void setHasImage(boolean hasImage) {
227         setHasImageAndNumIndentLines(hasImage, mIndentLines);
228     }
229 
230     /**
231      * @param lines the number of lines at the top that should be indented by indentEnd
232      */
233     @RemotableViewMethod
setNumIndentLines(int lines)234     public void setNumIndentLines(int lines) {
235         setHasImageAndNumIndentLines(mHasImage, lines);
236     }
237 
setHasImageAndNumIndentLines(boolean hasImage, int lines)238     private void setHasImageAndNumIndentLines(boolean hasImage, int lines) {
239         int oldEffectiveLines = mHasImage ? mIndentLines : 0;
240         int newEffectiveLines = hasImage ? lines : 0;
241         mIndentLines = lines;
242         mHasImage = hasImage;
243         if (oldEffectiveLines != newEffectiveLines) {
244             // always invalidate layout.
245             nullLayouts();
246             requestLayout();
247         }
248     }
249 
trackParameters()250     private void trackParameters() {
251         if (!TRACE_ONMEASURE) {
252             return;
253         }
254         Trace.setCounter("ImageFloatingView#staticLayoutCreationCount",
255                 mStaticLayoutCreationCountInOnMeasure);
256         Trace.setCounter("ImageFloatingView#isPrecomputedText",
257                 isTextAPrecomputedText());
258     }
259     /**
260      * @return 1 if {@link TextView#getText()} is PrecomputedText, else 0
261      */
isTextAPrecomputedText()262     private int isTextAPrecomputedText() {
263         final CharSequence text = getText();
264         if (text == null) {
265             return 0;
266         }
267 
268         if (text instanceof PrecomputedText) {
269             return 1;
270         }
271 
272         return 0;
273     }
274 
trackMaxLines()275     private void trackMaxLines() {
276         if (!TRACE_ONMEASURE) {
277             return;
278         }
279 
280         Trace.setCounter("ImageFloatingView#layoutMaxLines", mLayoutMaxLines);
281     }
282 }
283