Home | History | Annotate | Download | only in widget
      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;
     41 
     42     /** Resolved layout direction */
     43     private int mResolvedDirection = LAYOUT_DIRECTION_UNDEFINED;
     44     private int mMaxLinesForHeight = -1;
     45     private int mLayoutMaxLines = -1;
     46     private int mImageEndMargin;
     47 
     48     public ImageFloatingTextView(Context context) {
     49         this(context, null);
     50     }
     51 
     52     public ImageFloatingTextView(Context context, @Nullable AttributeSet attrs) {
     53         this(context, attrs, 0);
     54     }
     55 
     56     public ImageFloatingTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
     57         this(context, attrs, defStyleAttr, 0);
     58     }
     59 
     60     public ImageFloatingTextView(Context context, AttributeSet attrs, int defStyleAttr,
     61             int defStyleRes) {
     62         super(context, attrs, defStyleAttr, defStyleRes);
     63     }
     64 
     65     @Override
     66     protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
     67             Layout.Alignment alignment, boolean shouldEllipsize,
     68             TextUtils.TruncateAt effectiveEllipsize, boolean useSaved) {
     69         TransformationMethod transformationMethod = getTransformationMethod();
     70         CharSequence text = getText();
     71         if (transformationMethod != null) {
     72             text = transformationMethod.getTransformation(text, this);
     73         }
     74         text = text == null ? "" : text;
     75         StaticLayout.Builder builder = StaticLayout.Builder.obtain(text, 0, text.length(),
     76                 getPaint(), wantWidth)
     77                 .setAlignment(alignment)
     78                 .setTextDirection(getTextDirectionHeuristic())
     79                 .setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier())
     80                 .setIncludePad(getIncludeFontPadding())
     81                 .setUseLineSpacingFromFallbacks(true)
     82                 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
     83                 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL);
     84         int maxLines;
     85         if (mMaxLinesForHeight > 0) {
     86             maxLines = mMaxLinesForHeight;
     87         } else {
     88             maxLines = getMaxLines() >= 0 ? getMaxLines() : Integer.MAX_VALUE;
     89         }
     90         builder.setMaxLines(maxLines);
     91         mLayoutMaxLines = maxLines;
     92         if (shouldEllipsize) {
     93             builder.setEllipsize(effectiveEllipsize)
     94                     .setEllipsizedWidth(ellipsisWidth);
     95         }
     96 
     97         // we set the endmargin on the requested number of lines.
     98         int[] margins = null;
     99         if (mIndentLines > 0) {
    100             margins = new int[mIndentLines + 1];
    101             for (int i = 0; i < mIndentLines; i++) {
    102                 margins[i] = mImageEndMargin;
    103             }
    104         }
    105         if (mResolvedDirection == LAYOUT_DIRECTION_RTL) {
    106             builder.setIndents(margins, null);
    107         } else {
    108             builder.setIndents(null, margins);
    109         }
    110 
    111         return builder.build();
    112     }
    113 
    114     @RemotableViewMethod
    115     public void setImageEndMargin(int imageEndMargin) {
    116         mImageEndMargin = imageEndMargin;
    117     }
    118 
    119     @Override
    120     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    121         int availableHeight = MeasureSpec.getSize(heightMeasureSpec) - mPaddingTop - mPaddingBottom;
    122         if (getLayout() != null && getLayout().getHeight() != availableHeight) {
    123             // We've been measured before and the new size is different than before, lets make sure
    124             // we reset the maximum lines, otherwise we may be cut short
    125             mMaxLinesForHeight = -1;
    126             nullLayouts();
    127         }
    128         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    129         Layout layout = getLayout();
    130         if (layout.getHeight() > availableHeight) {
    131             // With the existing layout, not all of our lines fit on the screen, let's find the
    132             // first one that fits and ellipsize at that one.
    133             int maxLines = layout.getLineCount() - 1;
    134             while (maxLines > 1 && layout.getLineBottom(maxLines - 1) > availableHeight) {
    135                 maxLines--;
    136             }
    137             if (getMaxLines() > 0) {
    138                 maxLines = Math.min(getMaxLines(), maxLines);
    139             }
    140             // Only if the number of lines is different from the current layout, we recreate it.
    141             if (maxLines != mLayoutMaxLines) {
    142                 mMaxLinesForHeight = maxLines;
    143                 nullLayouts();
    144                 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    145             }
    146         }
    147     }
    148 
    149     @Override
    150     public void onRtlPropertiesChanged(int layoutDirection) {
    151         super.onRtlPropertiesChanged(layoutDirection);
    152 
    153         if (layoutDirection != mResolvedDirection && isLayoutDirectionResolved()) {
    154             mResolvedDirection = layoutDirection;
    155             if (mIndentLines > 0) {
    156                 // Invalidate layout.
    157                 nullLayouts();
    158                 requestLayout();
    159             }
    160         }
    161     }
    162 
    163     @RemotableViewMethod
    164     public void setHasImage(boolean hasImage) {
    165         setNumIndentLines(hasImage ? 2 : 0);
    166     }
    167 
    168     /**
    169      * @param lines the number of lines at the top that should be indented by indentEnd
    170      * @return whether a change was made
    171      */
    172     public boolean setNumIndentLines(int lines) {
    173         if (mIndentLines != lines) {
    174             mIndentLines = lines;
    175             // Invalidate layout.
    176             nullLayouts();
    177             requestLayout();
    178             return true;
    179         }
    180         return false;
    181     }
    182 }
    183