您的位置:首页 > 其它

适用于长文本的TextView

2015-10-14 16:55 375 查看
UI绘制操作分为三步来走,分别是测量、布局和绘制

1.测量

为了能告诉父视图自己的所占据的空间的大小,所有控件都必须要重写父类View的成员函数onMeasure。

public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
......

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

int width;
int height;

//计算TextView控件的宽度和高度
......

setMeasuredDimension(width, height);
}

......
}


参数widthMeasureSpec和heightMeasureSpec分别用来描述宽度测量规范和高度测量规范。测量规范使用一个int值来表法,这个int值包含了两个分量。

第一个是mode分量,使用最高2位来表示。测量模式有三种,分别是MeasureSpec.UNSPECIFIED(0)、MeasureSpec.EXACTLY(1)、和MeasureSpec.AT_MOST(2)。

第二个是size分量,使用低30位来表示。当mode分量等于MeasureSpec.EXACTLY时,size分量的值就是父视图要求当前控件要设置的宽度或者高度;当mode分量等于MeasureSpec.AT_MOST时,size分量的值就是父视图限定当前控件可以设置的最大宽度或者高度;当mode分量等于MeasureSpec.UNSPECIFIED时,父视图不限定当前控件所设置的宽度或者高度,这时候当前控件一般就按照实际需求来设置自己的宽度和高度。

2.布局

前面的测量工作实际上是确定了控件的大小,但是控件的位置还未确定。控件的位置是通过布局这个操作来完成的。

我们知道,控件是按照树形结构组织在一起的,其中,子控件的位置由父控件来设置,也就是说,只有容器类控件才需要执行布局操作,这是通过重写父类View的成员函数onLayout来实现的。

我们常见的FrameLayout、LinearLayout、RelativeLayout、TableLayout和AbsoluteLayout,都是属于容器类控件,因此,它们都需要重写父类View的成员函数onLayout。由于TextView控件不是容器类控件,因此,它可以不重写父类View的成员函数onLayout。

3.绘制

有了前面两个操作之后,控件的位置的大小就确定下来了,接下来就可以对它们的UI进行绘制了。控件为了能够绘制自己的UI,必须要重写父类View的成员函数onDraw。

自定义控件TextViewMultilineEllipse

/**
* 支持多行结尾省略号的TextView,改自http://code.google.com/p/android-textview-multiline-ellipse/
*/
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<
4000
/span> 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)
*/

//    UI绘制操作分为三步来走,分别是测量、布局和绘制
@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,

10f25
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();
}
}

}


布局

<?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:textSize="15sp"
/>

<LinearLayout
android:id="@+id/linearLayout1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
</LinearLayout>

</LinearLayout>


主程序代码

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.MATCH_PARENT,LayoutParams.WRAP_CONTENT);
//----用TextView来显示换行长文本----//
tvNormal=(TextView)this.findViewById(R.id.tvNormal);
tvNormal.setLayoutParams(lp); //限制TextView的宽高
tvNormal.setEllipsize(TextUtils.TruncateAt.END);
tvNormal.setSingleLine(false);
tvNormal.setMaxLines(10);
tvNormal.setText(text);

//----用TextViewMultilineEllipse来显示换行长文本----//
linearLayout1=(LinearLayout) this.findViewById(R.id.linearLayout1);

tvMultilineEllipse = new TextViewMultilineEllipse(this);
tvMultilineEllipse.setLayoutParams(lp);//限制TextView的宽高
tvMultilineEllipse.setEllipsis("...");//...替换剩余字符串
tvMultilineEllipse.setMaxLines(10);
tvMultilineEllipse.setTextSize((int)tvNormal.getTextSize());//设置文字大小
tvMultilineEllipse.setTextColor(Color.WHITE);
tvMultilineEllipse.setText(text);//设置文本

//在代码里添加tvMultilineEllipse,暂时不支持Layout里直接添加
linearLayout1.addView(tvMultilineEllipse);

}
}


Demo下载地址:

http://download.csdn.net/detail/yy471101598/9180779
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  textview