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