├── .gitignore ├── AndroidManifest.xml ├── README.md ├── Untitled Diagram.drawio ├── ellipsize-textview-android.apk ├── project.properties ├── res ├── drawable-xhdpi │ ├── book_detail_arrow_ltr.png │ └── ic_launcher.png ├── drawable │ ├── book_detail_arrow_down.xml │ └── book_detail_arrow_up.xml ├── layout │ └── main.xml └── values │ ├── strings.xml │ └── styleable.xml └── src └── com └── vincestyling └── android ├── MainActivity.java └── ui └── EllipsizeEndTextView.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | *.idb 4 | out 5 | 6 | .DS_Store 7 | 8 | target/ 9 | 10 | *~ 11 | *.swp 12 | *.orig 13 | 14 | bin/ 15 | gen/ 16 | .settings/ 17 | .DS_Store 18 | proguard/ 19 | build.xml 20 | build.properties 21 | ant.properties 22 | local.properties 23 | pkgs/ 24 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ellipsize-textview-android 2 | ========================== 3 | 4 | This project provides a Textview-like control which allows you to set max number 5 | of lines to wrap an input string, then ellipsizes the last line if there's not 6 | enough room to handle the entire input string. 7 | 8 | As you know, the Android UI TextView also allow we to set MaxLines and EllipsizeMode, 9 | but we can't find out how to know if TextView ellipsized, we want to know it because 10 | we can do something for it such as show an expand/collapse Button by the ellipsize 11 | state, that was why we wrote this widget. 12 | 13 | For more Detail, take a look at this [Article](http://vincestyling.com/posts/2013/easily-to-know-and-switch-the-ellipsize-mode-of-textview-in-android.html). 14 | 15 | ## How to use? 16 | 17 | in layout file, define it: 18 | 19 | ```xml 20 | 23 | 24 | 32 | 33 | 34 | ``` 35 | 36 | in Activity, just set Text to the widget: 37 | 38 | ```java 39 | mTxvEllipsize.setText(getString(R.string.ellipsize_txt_chn)); 40 | ``` 41 | 42 | License 43 | ======= 44 | 45 | ``` 46 | Copyright 2013 Vince Styling 47 | 48 | Licensed under the Apache License, Version 2.0 (the "License"); 49 | you may not use this file except in compliance with the License. 50 | You may obtain a copy of the License at 51 | 52 | http://www.apache.org/licenses/LICENSE-2.0 53 | 54 | Unless required by applicable law or agreed to in writing, software 55 | distributed under the License is distributed on an "AS IS" BASIS, 56 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 57 | See the License for the specific language governing permissions and 58 | limitations under the License. 59 | ``` -------------------------------------------------------------------------------- /Untitled Diagram.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /ellipsize-textview-android.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vince-styling/ellipsize-textview-android/77e761879ef3754952cdb0b79887a0e1efcac25f/ellipsize-textview-android.apk -------------------------------------------------------------------------------- /project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | target=android-19 15 | -------------------------------------------------------------------------------- /res/drawable-xhdpi/book_detail_arrow_ltr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vince-styling/ellipsize-textview-android/77e761879ef3754952cdb0b79887a0e1efcac25f/res/drawable-xhdpi/book_detail_arrow_ltr.png -------------------------------------------------------------------------------- /res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vince-styling/ellipsize-textview-android/77e761879ef3754952cdb0b79887a0e1efcac25f/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable/book_detail_arrow_down.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /res/drawable/book_detail_arrow_up.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 18 | 26 | 27 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ellipsize-textview 5 | 6 | 收起 7 | 展开 8 | 9 | 10 | 大白鲨以其好奇心而闻名 ——它经常从水中抬起它的头,并且更令在水中的人担心的是,它经常通过啃咬的方式去探索不熟悉的目标, 还会将一切它们感兴趣的东西吞下去:肉、骨头、木块,甚至钢笔、玻璃瓶什么的(它们的胃内有一层坚韧的壁,这样吞入的东西不会弄伤它们)。许多鲨鱼生物学家认为对人类的进攻是这种探索行为的结果,由于大白鲨令人难以置信的锋利牙齿和上下颚的力量,很可能会轻易地导致人的死亡。\n 11 | 即便如此,现今仍然有很多的冒险者愿意以生命作为代价去揭示大白鲨不为人知的一面。在他们眼中,大白鲨智力高,好奇心强,最重要的是它们乐于与人接触。当你亲切的去对待它时,它亦会亲切地去对待你。 12 | 13 | 14 | 15 | 16 | Android powers hundreds of millions of mobile devices in more than 190 countries around the world. It\'s the largest installed base of any mobile platform and growing fast—every day another million users power up their Android devices for the first time and start looking for apps, games, and other digital content.\n 17 | Android gives you a world-class platform for creating apps and games for Android users everywhere, as well as an open marketplace for distributing to them instantly. 18 | 19 | 20 | -------------------------------------------------------------------------------- /res/values/styleable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/com/vincestyling/android/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.vincestyling.android; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | import android.widget.TextView; 7 | import com.vincestyling.android.ui.EllipsizeEndTextView; 8 | 9 | public class MainActivity extends Activity implements View.OnClickListener { 10 | private EllipsizeEndTextView mTxvEllipsize; 11 | private TextView mBtnExpand; 12 | private View mLotEllipsize; 13 | 14 | @Override 15 | public void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.main); 18 | 19 | mTxvEllipsize = (EllipsizeEndTextView) findViewById(R.id.txvEllipsize); 20 | 21 | mLotEllipsize = findViewById(R.id.lotEllipsize); 22 | 23 | mBtnExpand = (TextView) findViewById(R.id.btnExpand); 24 | 25 | mTxvEllipsize.setOnMeasureDoneListener(new EllipsizeEndTextView.OnMeasureDoneListener() { 26 | @Override 27 | public void onMeasureDone(View v) { 28 | if (mTxvEllipsize.isExpanded()) { 29 | mBtnExpand.setVisibility(View.GONE); 30 | } else { 31 | mLotEllipsize.setOnClickListener(MainActivity.this); 32 | } 33 | mTxvEllipsize.setOnMeasureDoneListener(null); 34 | } 35 | }); 36 | mTxvEllipsize.setText(getString(R.string.ellipsize_txt_chn)); 37 | } 38 | 39 | @Override 40 | public void onClick(View v) { 41 | mTxvEllipsize.setOnMeasureDoneListener(new EllipsizeEndTextView.OnMeasureDoneListener() { 42 | @Override 43 | public void onMeasureDone(View v) { 44 | int resId = mTxvEllipsize.isExpanded() ? R.string.book_detail_summary_collapse : R.string.book_detail_summary_expand; 45 | mBtnExpand.setText(resId); 46 | resId = mTxvEllipsize.isExpanded() ? R.drawable.book_detail_arrow_up : R.drawable.book_detail_arrow_down; 47 | mBtnExpand.setCompoundDrawablesWithIntrinsicBounds(0, 0, resId, 0); 48 | } 49 | }); 50 | mTxvEllipsize.elipsizeSwitch(); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/com/vincestyling/android/ui/EllipsizeEndTextView.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Vince Styling 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 | package com.vincestyling.android.ui; 17 | 18 | import android.content.Context; 19 | import android.content.res.TypedArray; 20 | import android.graphics.Canvas; 21 | import android.graphics.Color; 22 | import android.graphics.Paint; 23 | import android.util.AttributeSet; 24 | import android.view.View; 25 | import com.vincestyling.android.R; 26 | 27 | import java.util.ArrayList; 28 | 29 | /** 30 | * This class provides a Textview-like control which allows you to set max number 31 | * of lines to wrap an input string, then ellipsizes the last line if there's not 32 | * enough room to handle the entire input string. 33 | *

34 | * As you know, the Android UI TextView also allow we to set MaxLines and EllipsizeMode, 35 | * but we can't find out how to know if TextView ellipsized, we want to know it because 36 | * we can do something for it such as show an expand/collapse Button by the ellipsize 37 | * state, that was why we wrote this widget. 38 | *

39 | * We implement this class Like TextView, also offer LineSpacing, TextSize, TextColor 40 | * to let you customize like you do with TextView via StyledAttributes, of course you 41 | * can declare more attributes as you wanted. 42 | *

43 | * When onMeasure calling, we use Paint.breakText() to calculate the input string then store 44 | * every line's start&end index. after that, we got the actually line count(EntireLineCount) 45 | * by the entire input string, according to that we can measure width simply done by 46 | * Paint.measureText() method. Use EntireLineCount we can decide should expand or not 47 | * and measure height. when measure is done, we will inform the ellipsize mode via 48 | * OnMeasureDoneListener who want to know it was changed. you can toggle the mode to 49 | * expand/collapse via click-handler if there's not enough space for the 50 | * input string(when EntireLineCount > MaxLineCount). 51 | *

52 | * Notes : This widget is pretty basic, it just support left-to-right text and 53 | * ellipsize text end. it handled Chinese or some languages like Chinese, 54 | * you can modify the breakText logic code to implement yourself. 55 | * 56 | * @author Vince 57 | */ 58 | public class EllipsizeEndTextView extends View { 59 | public final static String NEW_LINE_STR = "\n"; 60 | 61 | private int mLineSpacing; 62 | 63 | // the inform listener when measure is done 64 | private OnMeasureDoneListener mOnMeasureDoneListener; 65 | 66 | public EllipsizeEndTextView(Context context, AttributeSet attrs) { 67 | super(context, attrs); 68 | 69 | TypedArray typeArray = context.obtainStyledAttributes(attrs, R.styleable.EllipsizeEndTextView); 70 | 71 | mStrEllipsis = "..."; 72 | mMaxLineCount = typeArray.getInteger(R.styleable.EllipsizeEndTextView_maxLines, 5); 73 | mLineSpacing = typeArray.getDimensionPixelSize(R.styleable.EllipsizeEndTextView_lineSpacing, 0); 74 | 75 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 76 | mPaint.setColor(typeArray.getColor(R.styleable.EllipsizeEndTextView_textColor, Color.BLACK)); 77 | mPaint.setTextSize(typeArray.getDimensionPixelSize(R.styleable.EllipsizeEndTextView_textSize, 0)); 78 | 79 | typeArray.recycle(); 80 | } 81 | 82 | // the text ascent by Paint, use to compute the text height 83 | private int mAscent; 84 | 85 | private int mMaxLineCount; 86 | 87 | // when measure's done, drawLineCount will decide by expand mode 88 | private int mDrawLineCount; 89 | 90 | // the input string 91 | private String mText; 92 | 93 | private Paint mPaint; 94 | 95 | private boolean mExpanded = false; 96 | 97 | private String mStrEllipsis; 98 | 99 | // Beginning and end indices for the input string 100 | private ArrayList mLines; 101 | 102 | /** 103 | * Sets the text to display in this widget. 104 | * 105 | * @param text The text to display. 106 | */ 107 | public void setText(String text) { 108 | mText = text; 109 | requestLayout(); 110 | invalidate(); 111 | } 112 | 113 | /** 114 | * @see android.view.View#measure(int, int) 115 | */ 116 | @Override 117 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 118 | setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); 119 | if (mOnMeasureDoneListener != null) mOnMeasureDoneListener.onMeasureDone(this); 120 | } 121 | 122 | /** 123 | * Determines the width of this view 124 | * 125 | * @param measureSpec A measureSpec packed into an int 126 | * @return The width of the view, honoring constraints from measureSpec 127 | */ 128 | private int measureWidth(int measureSpec) { 129 | int specMode = MeasureSpec.getMode(measureSpec); 130 | int specSize = MeasureSpec.getSize(measureSpec); 131 | 132 | if (specMode == MeasureSpec.EXACTLY) { 133 | // Format the text using this exact width, and the current mode. 134 | breakWidth(specSize); 135 | // We were told how big to be. 136 | return specSize; 137 | } else if (specMode == MeasureSpec.AT_MOST) { 138 | // Use the AT_MOST size - if we had very short text, we may need even less 139 | // than the AT_MOST value, so return the minimum. 140 | return Math.min(breakWidth(specSize), specSize); 141 | } else { 142 | // We're not given any width - so in this case we assume we have an unlimited width? 143 | return breakWidth(specSize); 144 | } 145 | } 146 | 147 | /** 148 | * use Paint.breakText() to calculate widget entire line count 149 | * and measure first line width 150 | * 151 | * @param availableWidth The available width 152 | * @return The width of the view 153 | */ 154 | private int breakWidth(int availableWidth) { 155 | int maxWidth = availableWidth - getPaddingLeft() - getPaddingRight(); 156 | 157 | // we assume the width always equals first measure, so we just measure once 158 | if (mLines == null) { 159 | // If maxWidth is -1, interpret that as meaning to render the string on a single 160 | // line. Skip everything. 161 | if (maxWidth == -1) { 162 | mLines = new ArrayList(1); 163 | mLines.add(new int[]{0, mText.length()}); 164 | } else { 165 | int index = 0; 166 | int newlineIndex = 0; 167 | int endCharIndex = 0; 168 | mLines = new ArrayList(mMaxLineCount * 2); 169 | 170 | // breakText line by line and store line's indices 171 | while (index < mText.length()) { 172 | if (index == newlineIndex) { 173 | newlineIndex = mText.indexOf(NEW_LINE_STR, newlineIndex); 174 | endCharIndex = (newlineIndex != -1) ? newlineIndex : mText.length(); 175 | } 176 | 177 | int charCount = mPaint.breakText(mText, index, endCharIndex, true, maxWidth, null); 178 | if (charCount > 0) { 179 | mLines.add(new int[]{index, index + charCount}); 180 | index += charCount; 181 | } 182 | 183 | if (index == newlineIndex) { 184 | newlineIndex++; 185 | index++; 186 | } 187 | } 188 | } 189 | } 190 | 191 | int widthUsed; 192 | // If we required only one line, return its length, otherwise we used 193 | // whatever the maxWidth supplied was. 194 | switch (mLines.size()) { 195 | case 1: 196 | widthUsed = (int) (mPaint.measureText(mText) + 0.5f); 197 | break; 198 | case 0: 199 | widthUsed = 0; 200 | break; 201 | default: 202 | widthUsed = maxWidth; 203 | break; 204 | } 205 | 206 | return widthUsed + getPaddingLeft() + getPaddingRight(); 207 | } 208 | 209 | /** 210 | * Determines the height of this view 211 | * 212 | * @param measureSpec A measureSpec packed into an int 213 | * @return The height of the view, honoring constraints from measureSpec 214 | */ 215 | private int measureHeight(int measureSpec) { 216 | int result; 217 | int specMode = MeasureSpec.getMode(measureSpec); 218 | int specSize = MeasureSpec.getSize(measureSpec); 219 | 220 | mAscent = (int) mPaint.ascent(); 221 | if (specMode == MeasureSpec.EXACTLY) { 222 | // We were told how big to be, so nothing to do. 223 | result = specSize; 224 | } else { 225 | // The lines should already be broken up. Calculate our max desired height 226 | // for our current mode. 227 | if (mExpanded) { 228 | mDrawLineCount = mLines.size(); 229 | } else if (mLines.size() > mMaxLineCount) { 230 | mDrawLineCount = mMaxLineCount; 231 | } else { 232 | // when collapse mode on, but entire line count less then or equals 233 | // max line count, we should turn expand mode on 234 | mDrawLineCount = mLines.size(); 235 | mExpanded = true; 236 | } 237 | 238 | int textHeight = (int) (-mAscent + mPaint.descent()); 239 | result = getPaddingTop() + getPaddingBottom(); 240 | if (mDrawLineCount > 0) { 241 | result += mDrawLineCount * textHeight + (mDrawLineCount - 1) * mLineSpacing; 242 | } else { 243 | result += textHeight; 244 | } 245 | 246 | // Respect AT_MOST value if that was what is called for by measureSpec. 247 | if (specMode == MeasureSpec.AT_MOST) result = Math.min(result, specSize); 248 | } 249 | return result; 250 | } 251 | 252 | @Override 253 | protected void onDraw(Canvas canvas) { 254 | int renderWidth = canvas.getWidth() - getPaddingLeft() - getPaddingRight(); 255 | float x = getPaddingLeft(); 256 | float y = getPaddingTop() - mAscent; 257 | 258 | StringBuilder sb = new StringBuilder(); 259 | for (int i = 0; i < mDrawLineCount; i++) { 260 | // obtain current line to draw 261 | sb.append(mText, mLines.get(i)[0], mLines.get(i)[1]); 262 | 263 | // draw the ellipsis if necessary 264 | if (!mExpanded && mDrawLineCount - i == 1) { 265 | float lineDrawWidth = mPaint.measureText(sb, 0, sb.length()); 266 | float ellipsisWidth = mPaint.measureText(mStrEllipsis); 267 | 268 | // delete one char then measure until enough to draw ellipsize text 269 | while (lineDrawWidth + ellipsisWidth > renderWidth) { 270 | sb.deleteCharAt(sb.length() - 1); 271 | lineDrawWidth = mPaint.measureText(sb, 0, sb.length()); 272 | } 273 | sb.append(mStrEllipsis); 274 | } 275 | 276 | // draw the current line. 277 | canvas.drawText(sb, 0, sb.length(), x, y, mPaint); 278 | 279 | y += (-mAscent + mPaint.descent()) + mLineSpacing; 280 | // stop if canvas not enough space to draw next line 281 | if (y > canvas.getHeight()) break; 282 | 283 | // clean the line buffer 284 | sb.delete(0, sb.length()); 285 | } 286 | } 287 | 288 | public void elipsizeSwitch() { 289 | if (mExpanded) collapse(); 290 | else expand(); 291 | } 292 | 293 | public boolean isExpanded() { 294 | return mExpanded; 295 | } 296 | 297 | public void expand() { 298 | mExpanded = true; 299 | requestLayout(); 300 | invalidate(); 301 | } 302 | 303 | public void collapse() { 304 | mExpanded = false; 305 | requestLayout(); 306 | invalidate(); 307 | } 308 | 309 | public void setOnMeasureDoneListener(OnMeasureDoneListener onMeasureDoneListener) { 310 | mOnMeasureDoneListener = onMeasureDoneListener; 311 | } 312 | 313 | public interface OnMeasureDoneListener { 314 | void onMeasureDone(View v); 315 | } 316 | 317 | } 318 | --------------------------------------------------------------------------------