适应多行长文本的Android TextView

来源:互联网 发布:手机图文编辑软件 编辑:程序博客网 时间:2024/05/01 03:23

本文来自http://blog.csdn.net/hellogv/ ,引用必须注明出处!

       大家经常会用到系统默认的TextView,TextView可以很好地适应单行长文本(尾部自动打上省略号),以及可以完整显示多行文本(TextView的宽高足够大)。但如果是很多行的文本而TextView又足够大的时候,则会出现以下这种情况.......超出的文本受TextView大小限制,不能完全显示。

      本文主要实现一个能够适应多行长文本的TextView,自动缩减长文本并在结尾补上省略号。如下图:红色部分为普通的TextView,绿色部分为本文实现的TextView。

 
       本文的源码可以到 http://download.csdn.net/detail/hellogv/4298631 下载,本文的TextViewMultilineEllipse.java改自http://code.google.com/p/android-textview-multiline-ellipse/以及http://code.google.com/p/android/的MyClipTextView.java,相对于前面2者,本文使用哈希表来保存每次onMeasure()计算所得的宽高,大幅减少重复计算宽高。

 

本文的主Activity的源码如下:

public class AutoFixTextViewActivity extends Activity {private LinearLayout linearLayout1;private TextViewMultilineEllipse tvMultilineEllipse;private TextView tvNormal;//水调歌头,大家懂的private final String text="明月几时有?把酒问青天。不知天上宫阙,今夕是何年。\n"+"我欲乘风归去,又恐琼楼玉宇,高处不胜寒。\n"+"起舞弄清影,何似在人间。\n"+"转朱阁,低绮户,照无眠。不应有恨,何事长向别时圆?\n"+"人有悲欢离合,月有阴晴圆缺,此事古难全。\n"+"但愿人长久,千里共婵娟。";    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.main);        this.setTitle("适应多行文本的Android TextView---hellogv");                //共同的宽高        LayoutParams lp=new LayoutParams(LayoutParams.FILL_PARENT,100);        //----用TextView来显示换行长文本----//        tvNormal=(TextView)this.findViewById(R.id.tvNormal);        tvNormal.setLayoutParams(lp); //限制TextView的宽高        tvNormal.setEllipsize(TextUtils.TruncateAt.END);        tvNormal.setSingleLine(false);        tvNormal.setMaxLines(5);        tvNormal.setText(text);                        //----用TextViewMultilineEllipse来显示换行长文本----//        linearLayout1=(LinearLayout) this.findViewById(R.id.linearLayout1);                tvMultilineEllipse = new TextViewMultilineEllipse(this);tvMultilineEllipse.setLayoutParams(lp);//限制TextView的宽高tvMultilineEllipse.setEllipsis("...");//...替换剩余字符串tvMultilineEllipse.setMaxLines(5);tvMultilineEllipse.setTextSize((int)tvNormal.getTextSize());//设置文字大小tvMultilineEllipse.setTextColor(Color.WHITE);tvMultilineEllipse.setText(text);//设置文本//在代码里添加tvMultilineEllipse,暂时不支持Layout里直接添加linearLayout1.addView(tvMultilineEllipse);    }     }


PS:TextViewMultilineEllipse是在代码里动态构建和使用,而不能直接在Layout.xml里使用。

main.xml的源码如下:

 

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="fill_parent"    android:layout_height="fill_parent"    android:orientation="vertical" >    <TextView        android:id="@+id/tvNormal"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="Medium Text"        android:textAppearance="?android:attr/textAppearanceMedium" />    <LinearLayout        android:id="@+id/linearLayout1"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:layout_marginTop="20dip" >    </LinearLayout></LinearLayout>


TextViewMultilineEllipse.java源码如下,有点多,读者可以直接忽略:

public class TextViewMultilineEllipse extends TextView{    private TextPaint mTextPaint;    private String mText;    private int mAscent;    private String mStrEllipsis;    private String mStrEllipsisMore;    private int mMaxLines;    private boolean mDrawEllipsizeMoreString;    private int mColorEllipsizeMore;    private boolean mRightAlignEllipsizeMoreString;    private boolean mExpanded;    private LineBreaker mBreakerExpanded;    private LineBreaker mBreakerCollapsed;    /**hashMapW是优化的关键点,通过哈希表来减少计算次数*/    private HashMap<Integer,Integer> hashMapW=new HashMap<Integer,Integer>();    public TextViewMultilineEllipse(Context context) {        super(context);     // TODO Auto-generated constructor stub        mExpanded = false;        mDrawEllipsizeMoreString = true;        mRightAlignEllipsizeMoreString = false;        mMaxLines = -1;        mStrEllipsis = "...";        mStrEllipsisMore = "";        mColorEllipsizeMore = 0xFF0000FF;                mBreakerExpanded = new LineBreaker();                mBreakerCollapsed = new LineBreaker();                // Default font size and color.        mTextPaint = new TextPaint();        mTextPaint.setAntiAlias(true);        mTextPaint.setTextSize(13);        mTextPaint.setColor(0xFF000000);        mTextPaint.setTextAlign(Align.LEFT);        setDrawingCacheEnabled(true);    }        /**     * Sets the text to display in this widget.     * @param text The text to display.     */    public void setText(String text) {        mText = text;        requestLayout();        invalidate();    }    /**     * Sets the text size for this widget.     * @param size Font size.     */    public void setTextSize(int size) {        mTextPaint.setTextSize(size);        requestLayout();        invalidate();    }    /**     * Sets the text color for this widget.     * @param color ARGB value for the text.     */    public void setTextColor(int color) {        mTextPaint.setColor(color);        invalidate();    }    /**     * The string to append when ellipsizing. Must be shorter than the available     * width for a single line!     * @param ellipsis The ellipsis string to use, like "...", or "-----".     */    public void setEllipsis(String ellipsis) {        mStrEllipsis = ellipsis;    }        /**     * Optional extra ellipsize string. This     * @param ellipsisMore     */    public void setEllipsisMore(String ellipsisMore) {        mStrEllipsisMore = ellipsisMore;    }        /**     * The maximum number of lines to allow, height-wise.     * @param maxLines     */    public void setMaxLines(int maxLines) {        mMaxLines = maxLines;    }        /**     * Turn drawing of the optional ellipsizeMore string on or off.     * @param drawEllipsizeMoreString Yes or no.     */    public void setDrawEllipsizeMoreString(boolean drawEllipsizeMoreString) {        mDrawEllipsizeMoreString = drawEllipsizeMoreString;    }        /**     * Font color to use for the optional ellipsizeMore string.     * @param color ARGB color.     */    public void setColorEllpsizeMore(int color) {        mColorEllipsizeMore = color;    }        /**     * When drawing the ellipsizeMore string, either draw it wherever ellipsizing on the last     * line occurs, or always right align it. On by default.     * @param rightAlignEllipsizeMoreString Yes or no.     */    public void setRightAlignEllipsizeMoreString(boolean rightAlignEllipsizeMoreString) {        mRightAlignEllipsizeMoreString = rightAlignEllipsizeMoreString;    }        /**     * @see android.view.View#measure(int, int)     */    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        setMeasuredDimension(            measureWidth(widthMeasureSpec),            measureHeight(heightMeasureSpec));    }    /**     * Determines the width of this view     * @param measureSpec A measureSpec packed into an int     * @return The width of the view, honoring constraints from measureSpec     */    private int measureWidth(int measureSpec) {        int result = 0;        int specMode = MeasureSpec.getMode(measureSpec);        int specSize = MeasureSpec.getSize(measureSpec);        if (specMode == MeasureSpec.EXACTLY) {            // We were told how big to be.            result = specSize;                        // Format the text using this exact width, and the current mode.            breakWidth(specSize);        }         else {            if (specMode == MeasureSpec.AT_MOST) {                // Use the AT_MOST size - if we had very short text, we may need even less                // than the AT_MOST value, so return the minimum.                result = breakWidth(specSize);                result = Math.min(result, specSize);            }            else {                // We're not given any width - so in this case we assume we have an unlimited                // width?                breakWidth(specSize);            }        }        return result;    }    /**     * Determines the height of this view     * @param measureSpec A measureSpec packed into an int     * @return The height of the view, honoring constraints from measureSpec     */    private int measureHeight(int measureSpec) {        int result = 0;        int specMode = MeasureSpec.getMode(measureSpec);        int specSize = MeasureSpec.getSize(measureSpec);        mAscent = (int) mTextPaint.ascent();        if (specMode == MeasureSpec.EXACTLY) {            // We were told how big to be, so nothing to do.            result = specSize;        }         else {            // The lines should already be broken up. Calculate our max desired height            // for our current mode.            int numLines;            if (mExpanded) {                numLines = mBreakerExpanded.getLines().size();            }            else {                numLines = mBreakerCollapsed.getLines().size();            }            result = numLines * (int) (-mAscent + mTextPaint.descent())                   + getPaddingTop()                   + getPaddingBottom();            // Respect AT_MOST value if that was what is called for by measureSpec.            if (specMode == MeasureSpec.AT_MOST) {                result = Math.min(result, specSize);            }        }        return result;    }    /**     * Render the text     *      * @see android.view.View#onDraw(android.graphics.Canvas)     */    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        List<int[]> lines;        LineBreaker breaker;        if (mExpanded) {            breaker = mBreakerExpanded;            lines = mBreakerExpanded.getLines();        }        else {            breaker = mBreakerCollapsed;            lines = mBreakerCollapsed.getLines();        }                float x = getPaddingLeft();        float y = getPaddingTop() + (-mAscent);        for (int i = 0; i < lines.size(); i++) {            // Draw the current line.            int[] pair = lines.get(i);            canvas.drawText(mText, pair[0], pair[1]+1, x, y, mTextPaint);                        // Draw the ellipsis if necessary.            if (i == lines.size() - 1) {                if (breaker.getRequiredEllipsis()) {                    canvas.drawText(mStrEllipsis, x + breaker.getLengthLastEllipsizedLine(), y, mTextPaint);                    if (mDrawEllipsizeMoreString) {                        int lastColor = mTextPaint.getColor();                        mTextPaint.setColor(mColorEllipsizeMore);                        if (mRightAlignEllipsizeMoreString) {                            // Seems to not be right...                            canvas.drawText(mStrEllipsisMore, canvas.getWidth()-(breaker.getLengthEllipsisMore()+getPaddingRight()+getPaddingLeft()), y, mTextPaint);                        }                        else {                            canvas.drawText(mStrEllipsisMore, x + breaker.getLengthLastEllipsizedLinePlusEllipsis(), y, mTextPaint);                        }                        mTextPaint.setColor(lastColor);                    }                }            }                        y += (-mAscent + mTextPaint.descent());            if (y > canvas.getHeight()) {                break;            }        }    }        public boolean getIsExpanded() {        return mExpanded;    }        public void expand() {        mExpanded = true;        requestLayout();        invalidate();    }        public void collapse() {        mExpanded = false;        requestLayout();        invalidate();    }        private int breakWidth(int availableWidth) {    if(hashMapW.containsKey(availableWidth))    return hashMapW.get(availableWidth);            int widthUsed = 0;        if (mExpanded) {            widthUsed =              mBreakerExpanded.breakText(                 mText,                 availableWidth - getPaddingLeft() - getPaddingRight(),                 mTextPaint);        }        else {            widthUsed =              mBreakerCollapsed.breakText(                mText,                 mStrEllipsis,                 mStrEllipsisMore,                 mMaxLines,                 availableWidth - getPaddingLeft() - getPaddingRight(),                 mTextPaint);        }        hashMapW.put(availableWidth, widthUsed + getPaddingLeft() + getPaddingRight());        return widthUsed + getPaddingLeft() + getPaddingRight();    }            /**     * Used internally to break a string into a list of integer pairs. The pairs are     * start and end locations for lines given the current available layout width.     */    private static class LineBreaker    {        /** Was the input text long enough to need an ellipsis? */        private boolean mRequiredEllipsis;                /** Beginning and end indices for the input string. */        private ArrayList<int[]> mLines;                /** The width in pixels of the last line, used to draw the ellipsis if necessary. */        private float mLengthLastLine;                /** The width of the ellipsis string, so we know where to draw the ellipsisMore string         *  if necessary.         */        private float mLengthEllipsis;                /** The width of the ellipsizeMore string, same use as above. */        private float mLengthEllipsisMore;                        public LineBreaker() {            mRequiredEllipsis = false;            mLines = new ArrayList<int[]>();        }        /**         * Used for breaking text in 'expanded' mode, which needs no ellipse.         * Uses as many lines as is necessary to accomodate the entire input         * string.         * @param input String to be broken.         * @param maxWidth Available layout width.         * @param tp Current paint object with styles applied to it.         */        public int breakText(String input,                                 int maxWidth,                              TextPaint tp)         {            return breakText(input, null, null, -1, maxWidth, tp);        }        /**         * Used for breaking text, honors ellipsizing. The string will be broken into lines using         * the available width. The last line will subtract the physical width of the ellipsis         * string from maxWidth to reserve room for the ellipsis. If the ellpsisMore string is set,         * then space will also be reserved for its length as well.         * @param input String to be broken.         * @param ellipsis Ellipsis string, like "..."         * @param ellipsisMore Optional space reservation after the ellipsis, like " Read More!"         * @param maxLines Max number of lines to allow before ellipsizing.         * @param maxWidth Available layout width.         * @param tp Current paint object with styles applied to it.         */        public int breakText(String input,                                 String ellipsis,                                String ellipsisMore,                             int maxLines,                              int maxWidth,                              TextPaint tp)         {            mLines.clear();            mRequiredEllipsis = false;            mLengthLastLine = 0.0f;            mLengthEllipsis = 0.0f;            mLengthEllipsisMore = 0.0f;                        // If maxWidth is -1, interpret that as meaning to render the string on a single            // line. Skip everything.            if (maxWidth == -1) {                mLines.add(new int[]{ 0, input.length() });                return (int)(tp.measureText(input) + 0.5f);            }            // Measure the ellipsis string, and the ellipsisMore string if valid.            if (ellipsis != null) {                mLengthEllipsis = tp.measureText(ellipsis);            }            if (ellipsisMore != null) {                mLengthEllipsisMore = tp.measureText(ellipsisMore);            }            // Start breaking.            int posStartThisLine = -1;            float lengthThisLine = 0.0f;            boolean breakWords = true;            int pos = 0;            while (pos < input.length()) {                                if (posStartThisLine == -1) {                    posStartThisLine = pos;                }                                if (mLines.size() == maxLines) {                    mRequiredEllipsis = true;                    break;                }                                float widthOfChar = tp.measureText(input.charAt(pos) + "");                boolean newLineRequired = false;                                if(!hasChinese(input)){/**english*/                    // Check for a new line character or if we've run over max width.                    if (input.charAt(pos) == '\n') {                        newLineRequired = true;                                                // We want the current line to go up to the character right before the                        // new line char, and we want the next line to start at the char after                        // this new line char.                        mLines.add(new int[] { posStartThisLine, pos-1 });                    }else if (lengthThisLine + widthOfChar >= maxWidth) {                        newLineRequired = true;                        // We need to backup if we are in the middle of a word.                        if (input.charAt(pos) == ' ' || breakWords == false) {                            // Backup one character, because it doesn't fit on this line.                            pos--;                                                        // So this line includes up to the character before the space.                            mLines.add(new int[] { posStartThisLine, pos });                        }else {                            // Backup until we are at a space.                            Log.v("*******", "*********************************now char = " + input.charAt(pos));                            while (input.charAt(pos) != ' ') {                                pos--;                            }                                                        // This line includes up to the space.                            mLines.add(new int[] { posStartThisLine, pos });                        }                    }                }else{/**chinese*/                    // Check for a new line character or if we've run over max width.                    if (input.charAt(pos) == '\n') {                        newLineRequired = true;                                                // We want the current line to go up to the character right before the                        // new line char, and we want the next line to start at the char after                        // this new line char.                        mLines.add(new int[] { posStartThisLine, pos-1 });                    }else if (lengthThisLine + widthOfChar >= maxWidth) {                        newLineRequired = true;                            // This line includes up to the space.                            mLines.add(new int[] { posStartThisLine, pos });                    }                }                                                if (newLineRequired) {                    // The next cycle should reset the position if it sees it's -1 (to whatever i is).                    posStartThisLine = -1;                                        // Reset line length for next iteration.                    lengthThisLine = 0.0f;                                        // When we get to the last line, subtract the width of the ellipsis.                    if (mLines.size() == maxLines - 1) {                        maxWidth -= (mLengthEllipsis + mLengthEllipsisMore);                        // We also don't need to break on a full word, it'll look a little                        // cleaner if all breaks on the final lines break in the middle of                        // the last word.                        breakWords = false;                    }                }else {                    if(!hasChinese(input)){/**english*/                        lengthThisLine += widthOfChar;                    }else{/**chinese*/                        lengthThisLine += (widthOfChar + 0.5f);                    }                                        // If we're on the last character of the input string, add on whatever we have leftover.                    if (pos == input.length() - 1) {                        mLines.add(new int[] { posStartThisLine, pos });                    }                }                                pos++;            }                        // If we ellipsized, then add the ellipsis string to the end.            if (mRequiredEllipsis) {                int[] pairLast = mLines.get(mLines.size()-1);                mLengthLastLine = tp.measureText(input.substring(pairLast[0], pairLast[1] + 1));            }                        // If we required only one line, return its length, otherwise we used            // whatever the maxWidth supplied was.            if (mLines.size() == 0) {                return 0;            }            else if (mLines.size() == 1) {                return (int)(tp.measureText(input) + 0.5f);            }            else {                return maxWidth;            }        }                public boolean getRequiredEllipsis() {            return mRequiredEllipsis;        }                public List<int[]> getLines() {            return mLines;        }                public float getLengthLastEllipsizedLine() {            return mLengthLastLine;        }                public float getLengthLastEllipsizedLinePlusEllipsis() {            return mLengthLastLine + mLengthEllipsis;        }                public float getLengthEllipsis() {            return mLengthEllipsis;        }                public float getLengthEllipsisMore() {            return mLengthEllipsisMore;        }                /**         * 判断文本中是否含有中文         */        private boolean hasChinese(String input){            return input.getBytes().length != input.length();        }    }}


 

 

 

原创粉丝点击