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.Trace;
22 import android.text.BoringLayout;
23 import android.text.Layout;
24 import android.text.StaticLayout;
25 import android.text.TextUtils;
26 import android.text.method.TransformationMethod;
27 import android.util.AttributeSet;
28 import android.view.RemotableViewMethod;
29 import android.widget.RemoteViews;
30 import android.widget.TextView;
31 
32 /**
33  * A TextView that can float around an image on the end.
34  *
35  * @hide
36  */
37 @RemoteViews.RemoteView
38 public class ImageFloatingTextView extends TextView {
39 
40     /** Number of lines from the top to indent. */
41     private int mIndentLines = 0;
42     /** Whether or not there is an image to indent for. */
43     private boolean mHasImage = false;
44 
45     /** Resolved layout direction */
46     private int mResolvedDirection = LAYOUT_DIRECTION_UNDEFINED;
47     private int mMaxLinesForHeight = -1;
48     private int mLayoutMaxLines = -1;
49     private int mImageEndMargin;
50 
ImageFloatingTextView(Context context)51     public ImageFloatingTextView(Context context) {
52         this(context, null);
53     }
54 
ImageFloatingTextView(Context context, @Nullable AttributeSet attrs)55     public ImageFloatingTextView(Context context, @Nullable AttributeSet attrs) {
56         this(context, attrs, 0);
57     }
58 
ImageFloatingTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)59     public ImageFloatingTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
60         this(context, attrs, defStyleAttr, 0);
61     }
62 
ImageFloatingTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)63     public ImageFloatingTextView(Context context, AttributeSet attrs, int defStyleAttr,
64             int defStyleRes) {
65         super(context, attrs, defStyleAttr, defStyleRes);
66         setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL_FAST);
67         setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY);
68     }
69 
70     @Override
makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth, Layout.Alignment alignment, boolean shouldEllipsize, TextUtils.TruncateAt effectiveEllipsize, boolean useSaved)71     protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
72             Layout.Alignment alignment, boolean shouldEllipsize,
73             TextUtils.TruncateAt effectiveEllipsize, boolean useSaved) {
74         Trace.beginSection("ImageFloatingTextView#makeSingleLayout");
75         TransformationMethod transformationMethod = getTransformationMethod();
76         CharSequence text = getText();
77         if (transformationMethod != null) {
78             text = transformationMethod.getTransformation(text, this);
79         }
80         text = text == null ? "" : text;
81         StaticLayout.Builder builder = StaticLayout.Builder.obtain(text, 0, text.length(),
82                 getPaint(), wantWidth)
83                 .setAlignment(alignment)
84                 .setTextDirection(getTextDirectionHeuristic())
85                 .setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier())
86                 .setIncludePad(getIncludeFontPadding())
87                 .setUseLineSpacingFromFallbacks(true)
88                 .setBreakStrategy(getBreakStrategy())
89                 .setHyphenationFrequency(getHyphenationFrequency());
90         int maxLines;
91         if (mMaxLinesForHeight > 0) {
92             maxLines = mMaxLinesForHeight;
93         } else {
94             maxLines = getMaxLines() >= 0 ? getMaxLines() : Integer.MAX_VALUE;
95         }
96         builder.setMaxLines(maxLines);
97         mLayoutMaxLines = maxLines;
98         if (shouldEllipsize) {
99             builder.setEllipsize(effectiveEllipsize)
100                     .setEllipsizedWidth(ellipsisWidth);
101         }
102 
103         // we set the endmargin on the requested number of lines.
104         int[] margins = null;
105         if (mHasImage && mIndentLines > 0) {
106             margins = new int[mIndentLines + 1];
107             for (int i = 0; i < mIndentLines; i++) {
108                 margins[i] = mImageEndMargin;
109             }
110         }
111         if (mResolvedDirection == LAYOUT_DIRECTION_RTL) {
112             builder.setIndents(margins, null);
113         } else {
114             builder.setIndents(null, margins);
115         }
116 
117         final StaticLayout result = builder.build();
118         Trace.endSection();
119         return result;
120     }
121 
122     /**
123      * @param imageEndMargin the end margin (in pixels) to indent the first few lines of the text
124      */
125     @RemotableViewMethod
setImageEndMargin(int imageEndMargin)126     public void setImageEndMargin(int imageEndMargin) {
127         if (mImageEndMargin != imageEndMargin) {
128             mImageEndMargin = imageEndMargin;
129             invalidateTextIfIndenting();
130         }
131     }
132 
133     /**
134      * @param imageEndMarginDp the end margin (in dp) to indent the first few lines of the text
135      */
136     @RemotableViewMethod
setImageEndMarginDp(float imageEndMarginDp)137     public void setImageEndMarginDp(float imageEndMarginDp) {
138         setImageEndMargin(
139                 (int) (imageEndMarginDp * getResources().getDisplayMetrics().density));
140     }
141 
142     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)143     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
144         Trace.beginSection("ImageFloatingTextView#onMeasure");
145         int availableHeight = MeasureSpec.getSize(heightMeasureSpec) - mPaddingTop - mPaddingBottom;
146         if (getLayout() != null && getLayout().getHeight() != availableHeight) {
147             // We've been measured before and the new size is different than before, lets make sure
148             // we reset the maximum lines, otherwise the last line of text may be partially cut off
149             mMaxLinesForHeight = -1;
150             nullLayouts();
151         }
152         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
153         Layout layout = getLayout();
154         if (layout.getHeight() > availableHeight) {
155             // With the existing layout, not all of our lines fit on the screen, let's find the
156             // first one that fits and ellipsize at that one.
157             int maxLines = layout.getLineCount();
158             while (maxLines > 1 && layout.getLineBottom(maxLines - 1) > availableHeight) {
159                 maxLines--;
160             }
161             if (getMaxLines() > 0) {
162                 maxLines = Math.min(getMaxLines(), maxLines);
163             }
164             // Only if the number of lines is different from the current layout, we recreate it.
165             if (maxLines != mLayoutMaxLines) {
166                 mMaxLinesForHeight = maxLines;
167                 nullLayouts();
168                 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
169             }
170         }
171         Trace.endSection();
172     }
173 
174     @Override
onRtlPropertiesChanged(int layoutDirection)175     public void onRtlPropertiesChanged(int layoutDirection) {
176         super.onRtlPropertiesChanged(layoutDirection);
177 
178         if (layoutDirection != mResolvedDirection && isLayoutDirectionResolved()) {
179             mResolvedDirection = layoutDirection;
180             invalidateTextIfIndenting();
181         }
182     }
183 
invalidateTextIfIndenting()184     private void invalidateTextIfIndenting() {
185         if (mHasImage && mIndentLines > 0) {
186             // Invalidate layout.
187             nullLayouts();
188             requestLayout();
189         }
190     }
191 
192     /**
193      * @param hasImage whether there is an image to wrap text around.
194      */
195     @RemotableViewMethod
setHasImage(boolean hasImage)196     public void setHasImage(boolean hasImage) {
197         setHasImageAndNumIndentLines(hasImage, mIndentLines);
198     }
199 
200     /**
201      * @param lines the number of lines at the top that should be indented by indentEnd
202      */
203     @RemotableViewMethod
setNumIndentLines(int lines)204     public void setNumIndentLines(int lines) {
205         setHasImageAndNumIndentLines(mHasImage, lines);
206     }
207 
setHasImageAndNumIndentLines(boolean hasImage, int lines)208     private void setHasImageAndNumIndentLines(boolean hasImage, int lines) {
209         int oldEffectiveLines = mHasImage ? mIndentLines : 0;
210         int newEffectiveLines = hasImage ? lines : 0;
211         mIndentLines = lines;
212         mHasImage = hasImage;
213         if (oldEffectiveLines != newEffectiveLines) {
214             // always invalidate layout.
215             nullLayouts();
216             requestLayout();
217         }
218     }
219 }
220