I need to reach the following result, with dynamic titles that are coming from the server:
I tried to reach this result using the answer from the following question:
How to make the first character much larger than other in a TextView
But the result I reached was not good enough for the design team.
There are several requirements that are requested by the design:
Big first latterv (B).
the text continues a little bit above the middle of the big letter (the smaller red letters).
The second row of text continue aligned with the base of the big letter ("and star in film").
finally if there is more text it continue under the big letter.
What would be the best way to reach this result in Android?
The solution would be to have 3 views, one for the first letter, another for the (maximum) two lines, and another for the remainder of the text.
When measuring the view, you can determine if you've passed the 2 lines limit you wish to impose, and if so break the text and set the remainder of the text in the 3rd view.
You would also need to overcome the internal font padding of the first letter in order to achieve your desired result, hence the BottomAlignedTextView view.
Here is the code:
BottomAlignedTextView.java
public class BottomAlignedTextView extends TextView {
public BottomAlignedTextView(Context context) {
super(context);
}
#Override
protected void onDraw(Canvas canvas) {
float offset = getTextSize() - getLineHeight();
canvas.translate(0, -offset);
super.onDraw(canvas);
}
}
view_reader_title.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.shellanoo.newsbot.ui.views.BottomAlignedTextView
android:id="#+id/first_letter_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="serif"
android:background="#null"
android:gravity="bottom"
android:includeFontPadding="false"
android:textSize="92dp"
tools:text="B"/>
<TextView
android:id="#+id/two_lines_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignBottom="#+id/first_letter_tv"
android:layout_toEndOf="#+id/first_letter_tv"
android:layout_toRightOf="#+id/first_letter_tv"
android:gravity="bottom"
android:includeFontPadding="false"
android:lineSpacingMultiplier="0.9"
android:textColor="#color/black"
android:textSize="30dp"
tools:text="eyonce to write and star in a film"/>
</RelativeLayout>
<TextView
android:id="#+id/remainder_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="2dp"
android:layout_marginStart="2dp"
android:includeFontPadding="false"
android:textColor="#color/black"
android:textSize="30dp"
tools:text="about Saartjie Baartman"/>
</LinearLayout>
ReaderTitleView.java
public class ReaderTitleView extends FrameLayout {
#BindView(R.id.first_letter_tv)
TextView firstLetterTv;
#BindView(R.id.two_lines_tv)
TextView twoLinesTv;
#BindView(R.id.remainder_tv)
TextView remainderTv;
#ColorInt
private int mFirstWordColor;
private String mText;
public ReaderTitleView(Context context) {
this(context, null);
}
public ReaderTitleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ReaderTitleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
private void init(AttributeSet attrs) {
View view = inflate(getContext(), R.layout.view_reader_title, this);
ButterKnife.bind(this, view);
TypedArray a = getContext().getTheme().obtainStyledAttributes(
attrs,
R.styleable.ReaderTitleView,
0, 0);
mText = a.getString(R.styleable.ReaderTitleView_rtv_text);
if (mText == null) {
mText = "";
}
mFirstWordColor = a.getColor(R.styleable.ReaderTitleView_rtv_first_word_color, -1);
updateTextViews();
}
private void updateTextViews() {
if (!TextUtils.isEmpty(mText)) {
String firstLetter = mText.substring(0, 1);
firstLetter = firstLetter.toUpperCase();
String restText = mText.substring(1, mText.length());
firstLetterTv.setText(firstLetter);
twoLinesTv.setText(restText);
colorifyFirstWord();
} else {
firstLetterTv.setText("");
twoLinesTv.setText("");
remainderTv.setText("");
}
}
private void colorifyFirstWord() {
if (mFirstWordColor != -1) {
CharSequence text = twoLinesTv.getText();
Spannable s;
if (text instanceof Spannable) {
s = (Spannable) text;
} else {
s = new SpannableString(text);
}
String[] split = s.toString().split(" ", 2);
int start = 0;
int end = start + split[0].length();
s.setSpan(new ForegroundColorSpan(mFirstWordColor), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
twoLinesTv.setText(s, TextView.BufferType.SPANNABLE);
firstLetterTv.setTextColor(mFirstWordColor);
}
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (twoLinesTv.getLineCount() > 2) {
String text = twoLinesTv.getText().toString();
int secondLineEnd = twoLinesTv.getLayout().getLineEnd(1);
String twoLines = text.substring(0, secondLineEnd);
String remainder = text.substring(secondLineEnd, text.length());
twoLinesTv.setText(twoLines);
remainderTv.setText(remainder);
colorifyFirstWord();
}
}
public void setText(String text) {
mText = text;
updateTextViews();
}
public String getText() {
return mText;
}
}
have a look at http://uncodin.github.io/bypass/. https://github.com/Uncodin/bypass. eg:
Bypass markdown = new Bypass(context, new Bypass.Options());
CharSequence about0 = markdown.markdownToSpannable(parent.getResources()
.getString(R.string.string_0), yourTextView, null);
SpannableString about1 = new SpannableString(
parent.getResources().getString(R.string.string_1));
about1.setSpan(new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER),
0, about1.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableString about2 = new SpannableString(markdown.markdownToSpannable
(parent.getResources().getString(R.string.string2),
plaidDescription, null));
about2.setSpan(new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER),
0, about2.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableString about3 = new SpannableString(markdown.markdownToSpannable
(parent.getResources().getString(R.string.string_3),
yourTextView, null));
about3.setSpan(new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER),
0, about3.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
CharSequence desc = TextUtils.concat(about0, "\n\n", about1, "\n", about2,
"\n\n", about3);
HtmlUtils.setTextWithNiceLinks(yourTextView, desc);
Related
I'm trying to make a custom EditText for currency which means I need to have a prefix of it for the currency and I have to limit users' input to numbers only.
This is my custom EditText code
public OpenSansEditText(Context context, AttributeSet attrs) {
super(context, attrs);
paint = getPaint();
applyCustomFont(context, attrs);
}
public OpenSansEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
paint = getPaint();
applyCustomFont(context, attrs);
}
private void applyCustomFont(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.OpenSansET);
...
// Prefix
String prefix = a.getString(R.styleable.OpenSansET_prefix);
if (prefix != null) {
mPrefix = prefix;
} else {
mPrefix = "";
}
// Prefix Color
int prefixColor = a.getColor(R.styleable.OpenSansET_prefixColor, 0);
if (prefix != null) {
mPrefixColor = prefixColor;
} else {
mPrefixColor = ContextCompat.getColor(context, R.color.miBlack);
}
a.recycle();
}
...
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!mPrefix.equals("")) {
getPaint().getTextBounds(mPrefix, 0, mPrefix.length(), mPrefixRect);
mPrefixRect.right += getPaint().measureText(" "); // add some offset
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!mPrefix.equals("")) {
paint.setColor(mPrefixColor);
canvas.drawText(mPrefix, super.getCompoundPaddingLeft(), getBaseline(), paint);
}
}
#Override
public int getCompoundPaddingLeft() {
return mPrefix.equals("") ? super.getCompoundPaddingLeft()
: super.getCompoundPaddingLeft() + mPrefixRect.width();
}
This is how I use it in xml :
<com.asta.www.classes.OpenSansEditText
android:id="#+id/shopping_filter_priceMinRange"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.4"
android:gravity="center"
android:hint="#string/min"
android:textColor="#color/miBlack"
android:textColorHint="#color/miGrey"
app:prefix="$"
app:prefixColor="#color/miBlack" />
<com.asta.www.classes.OpenSansEditText
android:id="#+id/shopping_filter_priceMaxRange"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.4"
android:gravity="center"
android:hint="#string/max"
android:inputType="number"
android:textColorHint="#color/miGrey"
app:prefix="$"
app:prefixColor="#color/miBlack" />
Which yields :
Only the first one without inputType as number has the currency sign shown, whereas the second ET doesn't have its currency sign shown.
How to achieve currency prefix as text and still keeping inputType to numbers only for user? And I don't want to use two views, namely EditText and TextView to left of it, both inside a ViewGroup to achieve that.
For this type of scenarios I use Compound views. Please see below code for more information.
First create a layout for your custom view like below.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/txt_prefix"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$" />
<EditText
android:id="#+id/et_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:inputType="number" />
</LinearLayout>
Now create a new class which should extends the LinearLayout. See below code.
public class OpenSansEditText extends LinearLayout {
private TextView txtPrefix;
private EditText etValue;
private String prefix = "$";
private int prefixColor = Color.BLACK;
public OpenSansEditText(Context context) {
super(context);
initializeViews(context, null);
}
public OpenSansEditText(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
initializeViews(context, attrs);
}
public OpenSansEditText(Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initializeViews(context, attrs);
}
private void initializeViews(Context context, AttributeSet attrs) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.opensansedittext_view, this,true);
if (attrs != null) {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.OpenSansEditText);
prefix = a.getString(R.styleable.OpenSansEditText_prefix);
prefixColor = a.getColor(R.styleable.OpenSansEditText_prefixColor, Color.BLACK);
}
}
public CharSequence getValue(){
return etValue.getText();
}
public CharSequence getPrefix(){
return txtPrefix.getText();
}
#Override
protected void onFinishInflate() {
super.onFinishInflate();
txtPrefix = (TextView) findViewById(R.id.txt_prefix);
etValue = (EditText) findViewById(R.id.et_value);
txtPrefix.setText(prefix);
txtPrefix.setTextColor(prefixColor);
}
}
And then add your attributes to attribute xml file Ex: (attrs.xml in my case)
<resources>
<declare-styleable name="OpenSansEditText">
<attr name="prefix" format="string"/>
<attr name="prefixColor" format="color"/>
</declare-styleable>
</resources>
Now you can use it anywhere in the project as below
<com.asta.www.classes.OpenSansEditText
android:layout_width="match_parent"
android:layout_height="match_parent"
app:prefix="$"
app:prefixColor="#f00"/>
Hope this will help you to solve your problem. Thanks...
In the end I found this link https://gist.github.com/kennydude/5407963 which helps me in the right direction. So what it does is I think making the prefix as Drawable using this class :
private class TagDrawable extends Drawable {
public String text = "";
public void setText(String s){
text = s;
// Tell it we need to be as big as we want to be!
setBounds(0,0,getIntrinsicWidth(),getIntrinsicHeight());
invalidateSelf();
}
#Override
public void draw(#NonNull Canvas canvas) {
// I don't know why this y works here, but it does :)
// (aka if you are from Google/are Jake Wharton and I have done it wrong, please tell me!)
canvas.drawText( text, 0, mLine0Baseline + canvas.getClipBounds().top, mTextPaint );
}
#Override public void setAlpha(int i) {}
#Override public void setColorFilter(ColorFilter colorFilter) {}
#Override public int getOpacity() {return PixelFormat.UNKNOWN;}
#Override public int getIntrinsicHeight (){
return (int)mFontHeight;
}
#Override public int getIntrinsicWidth(){
return (int)mTextPaint.measureText( text );
}
}
And draw it to the left of the TextView like
TagDrawable left = new TagDrawable();
left.setText("$");
setCompoundDrawablesRelative(left, null, null, null);
The link I supplied even has suffix support which I haven't tried.
I have a next problem, which happens in 2 cases:
First case.
1). I have some custom veiw which draw photos on it with different opacity. here is method MyView.onDraw:
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.getClipBounds(clipRect);
int i1 = Math.min(testColors.length-1, (int)Math.floor(posX/PHOTO_DISTANCE));
int c1 = testColors[Math.max(0, i1)];
int i2 = Math.min(testColors.length-1, (int)Math.ceil(posX/PHOTO_DISTANCE));
int c2 = testColors[Math.max(0, i2)];
paint.setColor(c1);
float r = (255f/PHOTO_DISTANCE*posX)%255;
paint.setAlpha(255);
if(photoA != null){//bitmap != null
bitmapRect.set(0, 0, photoA.getWidth(), photoA.getHeight());
canvas.drawBitmap(photoA, bitmapRect, clipRect, paint);
}
paint.setAlpha((int)(r));
if(photoB != null){//bitmap != null
bitmapRect.set(0, 0, photoB.getWidth(), photoB.getHeight());
canvas.drawBitmap(photoB, bitmapRect, clipRect, paint);
}
}
testColors - array of colors(int);
photoA, photoB - bitmaps;
i1, i2 - image indexes;
c1, c2 - colors. they are not importatant.
I added this view to FrameView:
viewHolder.myFrame.addView(viewHolder.myView, 0);
And in this FrameView I have some clickable RelativeLayout's:
<com.app.custom.view.ClickableRelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/share_action_container"
app1:pressedStateColor="#color/app_pressed_default"
app1:unpressedStateColor="#color/color_white"
android:background="#color/transparent"
android:layout_centerHorizontal="true">
<RelativeLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#drawable/circle_background"
android:id="#+id/share_icon"
android:layout_centerHorizontal="true">
<ImageView
android:id="#+id/img_share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:tag="icon"
android:src="#drawable/ic_share"/>
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_below="#id/share_icon"
android:text="Share"
android:layout_marginTop="3dp"
android:shadowColor="#color/text_shadow"
android:shadowDx="#integer/shadowDX"
android:shadowDy="#integer/shadowDY"
android:shadowRadius="#integer/shadowRadius"
android:background="#color/transparent"
android:layout_centerHorizontal="true"
android:textSize="#dimen/icon_text_size"
android:tag="text"
android:textColor="#android:color/white"
android:id="#+id/txt_share_action"/>
</com.app.custom.view.ClickableRelativeLayout>
Here is a Java code of ClickableRelativeLayout:
public class ClickableRelativeLayout extends RelativeLayout implements View.OnTouchListener {
private ViewHolder viewHolder;
private int pressedStateColor;
private int unpressedStateColor;
private final int DEFAULT_PRESSED_STATE_COLOR;
private final int DEFAULT_UNPRESSED_STATE_COLOR;
public ClickableRelativeLayout(Context context) {
super(context);
DEFAULT_PRESSED_STATE_COLOR = context.getResources().getColor(R.color.app_pressed_default);
DEFAULT_UNPRESSED_STATE_COLOR = context.getResources().getColor(R.color.app_blue_without_transparent);
setup();
initColors(context, null);
}
public ClickableRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
DEFAULT_PRESSED_STATE_COLOR = context.getResources().getColor(R.color.app_pressed_default);
DEFAULT_UNPRESSED_STATE_COLOR = context.getResources().getColor(R.color.app_blue_without_transparent);
setup();
initColors(context, attrs);
}
public ClickableRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
DEFAULT_PRESSED_STATE_COLOR = context.getResources().getColor(R.color.app_pressed_default);
DEFAULT_UNPRESSED_STATE_COLOR = context.getResources().getColor(R.color.app_blue_without_transparent);
setup();
initColors(context, attrs);
}
private void setup(){
setOnTouchListener(this);
}
private void initColors(Context context, AttributeSet attrs){
if(attrs != null) {
TypedArray styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.ClickableRelativeLayout);
pressedStateColor = styledAttributes.getColor(R.styleable.ClickableRelativeLayout_pressedStateColor,
DEFAULT_PRESSED_STATE_COLOR);
unpressedStateColor = styledAttributes.getColor(R.styleable.RowLayout_android_verticalSpacing,
DEFAULT_UNPRESSED_STATE_COLOR);
styledAttributes.recycle();
}else{
pressedStateColor = DEFAULT_PRESSED_STATE_COLOR;
unpressedStateColor = DEFAULT_UNPRESSED_STATE_COLOR;
}
}
#Override
protected void onFinishInflate() {
super.onFinishInflate();
viewHolder = new ViewHolder(
(TextView) findViewWithTag("text"),
(ImageView) findViewWithTag("icon")
);
}
#Override
public boolean onTouch(View v, MotionEvent event) {
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
if (hasOnClickListeners()) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
select();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
unSelect();
break;
}
}
}
return false;
}
private void select(){
if(isInitializedCorrect()){
final int color = pressedStateColor;
viewHolder.text.setTextColor(color);
ImageHelper.INSTANCE.applyColorFilterToImage(viewHolder.icon.getDrawable(), color);
}
}
private void unSelect(){
if(isInitializedCorrect()){
final int color = unpressedStateColor;
viewHolder.text.setTextColor(color);
viewHolder.icon.setColorFilter(color);
}
}
private boolean isInitializedCorrect(){
return viewHolder != null && viewHolder.icon != null && viewHolder.text != null;
}
private class ViewHolder{
ImageView icon;
TextView text;
public ViewHolder(TextView text, ImageView icon) {
this.text = text;
this.icon = icon;
}
}
}
And when I clicked on this layout, background of MyView shrinks, and sets to this ClickableRelativeLayout:
And Second case.
I have some text view on same frame, it is invisible by default, and when you scrolled 10 photos, I apply AlphaAnimation for this TextView and it draws slowly. Here is a code of alpha animation:
AlphaAnimation animation1 = new AlphaAnimation(0.0f, 1.0f);
animation1.setDuration(1300);
animation1.setFillAfter(true);
//here is my TextView
viewHolder.gotItView.setVisibility(View.VISIBLE);
viewHolder.gotItView.startAnimation(animation1);
And happens the same this. On background of this TextView appears content of MyView
I have a weird problem, for some reason the android:ellipsize="end" works, but added the point in the middle of the text == centered vertically instead of being aligned to baseline:
I checked for any "center" properties, but there is none of those:
Update:
This is the XML part:
<com.citylifeapps.cups.customviews.CarmelaTextView
android:id="#+id/venue_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="#+id/venue_distance"
android:layout_toRightOf="#+id/venue_name"
android:gravity="left"
android:text="#string/placeholder_venue_address"
android:textColor="#color/cups_white"
android:textSize="20sp"
android:textStyle="bold"
android:ellipsize="end"
android:singleLine="true"
android:scrollHorizontally="true"
android:layout_alignBaseline="#+id/venue_name" />
And the custom TextView class:
public class CarmelaTextView extends TextView {
public CarmelaTextView(Context context, AttributeSet attrs) {
super(context, attrs);
setCarmelaTypeface(context);
}
public CarmelaTextView(Context context) {
super(context);
setCarmelaTypeface(context);
}
private void setCarmelaTypeface(Context context) {
if (this.isInEditMode()) return;
Typeface typeface = Typeface.createFromAsset(context.getAssets(), "carmela.ttf");
this.setTypeface(typeface);
}
}
further check shows that if I use a simple TextView the problem disappears,
but there is nothing in the custom TextView that will cause such a behavior.
Does anyone know why this might happen?
Thanks.
It looks like the problem lies within my custom font I'm using for this custom TextView, from the accepted answer here:
Why does TextView in single line elipsized with "end" show boxes?
I'm guessing that I'm facing the same problem but with a different result because the 3 dots (...) U+FEFF glyph for my font is different.
But still if some one found a solution that works for this issue I would be glad if he could share it.
I used this class to resolve this issue
public class EllipsizingTextView extends TextView {
private static final String ELLIPSIS = "...";
public interface EllipsizeListener {
void ellipsizeStateChanged(boolean ellipsized);
}
private final List<EllipsizeListener> ellipsizeListeners = new ArrayList<EllipsizeListener>();
private boolean isEllipsized;
private boolean isStale;
private boolean programmaticChange;
private String fullText;
private int maxLines = -1;
private float lineSpacingMultiplier = 1.0f;
private float lineAdditionalVerticalPadding = 0.0f;
public EllipsizingTextView(Context context) {
super(context);
}
public EllipsizingTextView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, new int[] { android.R.attr.maxLines });
setMaxLines(a.getInt(0, 1));
}
public EllipsizingTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray a = context.obtainStyledAttributes(attrs, new int[] { android.R.attr.maxLines });
setMaxLines(a.getInt(0, 1));
}
public void addEllipsizeListener(EllipsizeListener listener) {
if (listener == null) {
throw new NullPointerException();
}
ellipsizeListeners.add(listener);
}
public void removeEllipsizeListener(EllipsizeListener listener) {
ellipsizeListeners.remove(listener);
}
public boolean isEllipsized() {
return isEllipsized;
}
#Override
public void setMaxLines(int maxLines) {
super.setMaxLines(maxLines);
this.maxLines = maxLines;
isStale = true;
}
public int getMaxLines() {
return maxLines;
}
#Override
public void setLineSpacing(float add, float mult) {
this.lineAdditionalVerticalPadding = add;
this.lineSpacingMultiplier = mult;
super.setLineSpacing(add, mult);
}
#Override
protected void onTextChanged(CharSequence text, int start, int before,
int after) {
super.onTextChanged(text, start, before, after);
if (!programmaticChange) {
fullText = text.toString();
isStale = true;
}
}
#Override
protected void onDraw(Canvas canvas) {
if (isStale) {
super.setEllipsize(null);
resetText();
}
super.onDraw(canvas);
}
private void resetText() {
int maxLines = getMaxLines();
String workingText = fullText;
boolean ellipsized = false;
if (maxLines != -1) {
Layout layout = createWorkingLayout(workingText);
if (layout.getLineCount() > maxLines) {
workingText = fullText.substring(0,
layout.getLineEnd(maxLines - 1)).trim();
while (createWorkingLayout(workingText + ELLIPSIS)
.getLineCount() > maxLines) {
workingText = workingText.substring(0,
workingText.length() - 1 - 1);
}
workingText = workingText + ELLIPSIS;
ellipsized = true;
}
}
if (!workingText.equals(getText())) {
programmaticChange = true;
try {
setText(workingText);
} finally {
programmaticChange = false;
}
}
isStale = false;
if (ellipsized != isEllipsized) {
isEllipsized = ellipsized;
for (EllipsizeListener listener : ellipsizeListeners) {
listener.ellipsizeStateChanged(ellipsized);
}
}
}
private Layout createWorkingLayout(String workingText) {
return new StaticLayout(workingText, getPaint(), getWidth()
- getPaddingLeft() - getPaddingRight(), Alignment.ALIGN_NORMAL,
lineSpacingMultiplier, lineAdditionalVerticalPadding, false);
}
#Override
public void setEllipsize(TruncateAt where) {
// Ellipsize settings are not respected
}
}
I am trying to set ellipsize of text view. using the following code. I want to add "view more" at the end of truncated string after 3 dots. If this would be possible with same text view that would be great, or "view more" in seperate text view will also work. Max lines allow are 4. I tried to set width of first text view but it left the empty space at end of first 3 lines. Please see the image below.
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="#+id/tvReviewDescription"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:maxLines="4"
android:text="I tend to shy away from restaurant chains, but wherever I go, PF Chang's has solidly good food and, like Starbucks, they're reliable. We were staying in Boston for a week and after a long day and blah blah blah blah... "
android:textColor="#color/black"
android:textSize="13dp"
android:maxLength="280"
android:ellipsize="end"/>
<TextView
android:id="#+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="#+id/tvReviewDescription"
android:layout_alignParentRight="true"
android:text="#string/label_view_more"
android:textColor="#color/yellow" />
</RelativeLayout>
Find my answer
public static void makeTextViewResizable(final TextView tv, final int maxLine, final String expandText, final boolean viewMore) {
if (tv.getTag() == null) {
tv.setTag(tv.getText());
}
ViewTreeObserver vto = tv.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#SuppressWarnings("deprecation")
#Override
public void onGlobalLayout() {
ViewTreeObserver obs = tv.getViewTreeObserver();
obs.removeGlobalOnLayoutListener(this);
if (maxLine == 0) {
int lineEndIndex = tv.getLayout().getLineEnd(0);
String text = tv.getText().subSequence(0, lineEndIndex - expandText.length() + 1) + " " + expandText;
tv.setText(text);
tv.setMovementMethod(LinkMovementMethod.getInstance());
tv.setText(
addClickablePartTextViewResizable(Html.fromHtml(tv.getText().toString()), tv, maxLine, expandText,
viewMore), TextView.BufferType.SPANNABLE);
} else if (maxLine > 0 && tv.getLineCount() >= maxLine) {
int lineEndIndex = tv.getLayout().getLineEnd(maxLine - 1);
String text = tv.getText().subSequence(0, lineEndIndex - expandText.length() + 1) + " " + expandText;
tv.setText(text);
tv.setMovementMethod(LinkMovementMethod.getInstance());
tv.setText(
addClickablePartTextViewResizable(Html.fromHtml(tv.getText().toString()), tv, maxLine, expandText,
viewMore), TextView.BufferType.SPANNABLE);
} else {
int lineEndIndex = tv.getLayout().getLineEnd(tv.getLayout().getLineCount() - 1);
String text = tv.getText().subSequence(0, lineEndIndex) + " " + expandText;
tv.setText(text);
tv.setMovementMethod(LinkMovementMethod.getInstance());
tv.setText(
addClickablePartTextViewResizable(Html.fromHtml(tv.getText().toString()), tv, lineEndIndex, expandText,
viewMore), TextView.BufferType.SPANNABLE);
}
}
});
}
private static SpannableStringBuilder addClickablePartTextViewResizable(final Spanned strSpanned, final TextView tv,
final int maxLine, final String spanableText, final boolean viewMore) {
String str = strSpanned.toString();
SpannableStringBuilder ssb = new SpannableStringBuilder(strSpanned);
if (str.contains(spanableText)) {
ssb.setSpan(new MySpannable(false){
#Override
public void onClick(View widget) {
if (viewMore) {
tv.setLayoutParams(tv.getLayoutParams());
tv.setText(tv.getTag().toString(), TextView.BufferType.SPANNABLE);
tv.invalidate();
makeTextViewResizable(tv, -1, "See Less", false);
} else {
tv.setLayoutParams(tv.getLayoutParams());
tv.setText(tv.getTag().toString(), TextView.BufferType.SPANNABLE);
tv.invalidate();
makeTextViewResizable(tv, 3, ".. See More", true);
}
}
}, str.indexOf(spanableText), str.indexOf(spanableText) + spanableText.length(), 0);
}
return ssb;
}
Another class:-
import android.graphics.Color;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.view.View;
public class MySpannable extends ClickableSpan {
private boolean isUnderline = true;
/**
* Constructor
*/
public MySpannable(boolean isUnderline) {
this.isUnderline = isUnderline;
}
#Override
public void updateDrawState(TextPaint ds) {
ds.setUnderlineText(isUnderline);
ds.setColor(Color.parseColor("#1b76d3"));
}
#Override
public void onClick(View widget) {
}
}
Last step to call it:
DetailTv.setText(discription);
makeTextViewResizable(DetailTv, 3, "See More", true);
Simpler than the accepted answer:
public static final int MAX_LINES = 3;
public static final String TWO_SPACES = " ";
String myReallyLongText = "Bacon ipsum dolor amet porchetta venison ham fatback alcatra tri-tip, turducken strip steak sausage rump burgdoggen pork loin. Spare ribs filet mignon salami, strip steak ball tip shank frankfurter corned beef venison. Pig pork belly pork chop andouille. Porchetta pork belly ground round, filet mignon bresaola chuck swine shoulder leberkas jerky boudin. Landjaeger pork chop corned beef, tri-tip brisket rump pastrami flank."
textView.setText(myReallyLongText);
textView.post(new Runnable() {
#Override
public void run() {
// Past the maximum number of lines we want to display.
if (textView.getLineCount() > MAX_LINES) {
int lastCharShown = textView.getLayout().getLineVisibleEnd(MAX_LINES - 1);
textView.setMaxLines(MAX_LINES);
String moreString = context.getString(R.string.more);
String suffix = TWO_SPACES + moreString;
// 3 is a "magic number" but it's just basically the length of the ellipsis we're going to insert
String actionDisplayText = myReallyLongText.substring(0, lastCharShown - suffix.length() - 3) + "..." + suffix;
SpannableString truncatedSpannableString = new SpannableString(actionDisplayText);
int startIndex = actionDisplayText.indexOf(moreString);
truncatedSpannableString.setSpan(new ForegroundColorSpan(context.getColor(android.R.color.blue)), startIndex, startIndex + moreString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(truncatedSpannableString);
}
}
});
If you want the "View More" part of your Text to be clickable (but not the entire TextView), utilize ClickableSpan as outlined here in this StackOverflow for
How to set the part of the text view is clickable. I would caution you to be aware of the UX implications of this, as normally you truncate your text because you have a lot of it and you don't have much space, so your font size is already probably small. Having a tiny target for users to click to navigate to the full text might not be the best or most accessible user experience, especially if your users are elderly or have mobility issues that make hitting a small part of the screen difficult. Generally I would suggest making your entire TextView clickable rather than a small portion of it for this reason.
As an alternative, you can do as I did and turn this into a custom view. Here's the class; you can modify as you desire using the ClickableSpan code, but since I have not compiled this project in a long, long time I don't wish to make changes that I then need to verify are safe to publish. I welcome an edit if someone wants to tackle that.
public class TruncatingTextView extends AppCompatTextView {
private static final String TWO_SPACES = " ";
private int truncateAfter = Integer.MAX_VALUE;
private String suffix;
private final RelativeSizeSpan truncateTextSpan = new RelativeSizeSpan(0.75f);
private ForegroundColorSpan viewMoreTextSpan;
private final String moreString = getContext().getString(R.string.more);
private final String ellipsis = getContext().getString(R.string.ellipsis);
public TruncatingTextView(Context context) {
super(context);
init();
}
public TruncatingTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public TruncatingTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
viewMoreTextSpan = new ForegroundColorSpan(ContextCompat.getColor(getContext(), R.color.upsell_blue));
}
public void setText(CharSequence fullText, #Nullable CharSequence afterTruncation, int truncateAfterLineCount) {
this.suffix = TWO_SPACES + moreString;
if (!TextUtils.isEmpty(afterTruncation)) {
suffix += TWO_SPACES + afterTruncation;
}
if (this.truncateAfter != truncateAfterLineCount) {
this.truncateAfter = truncateAfterLineCount;
setMaxLines(truncateAfter);
}
setText(fullText);
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (getLayout() != null && getLayout().getLineCount() > truncateAfter) {
int lastCharToShowOfFullTextAfterTruncation = getLayout().getLineVisibleEnd(truncateAfter - 1) - suffix.length() - ellipsis.length();
int startIndexOfMoreString = lastCharToShowOfFullTextAfterTruncation + TWO_SPACES.length() + 1;
SpannableString truncatedSpannableString = new SpannableString(getText().subSequence(0, lastCharToShowOfFullTextAfterTruncation) + ellipsis + suffix);
truncatedSpannableString.setSpan(truncateTextSpan, startIndexOfMoreString, truncatedSpannableString.length(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
truncatedSpannableString.setSpan(viewMoreTextSpan, startIndexOfMoreString, startIndexOfMoreString + moreString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
setText(truncatedSpannableString);
}
}
}
This can be achieved during Runtime , all you need to do is check the length of string and add Underlined View More at the end of string like this.
I have used length '20' as an example , you can change according to your requirement.
final TextView result = (TextView) findViewById(R.id.textview);
String text = "I tend to shy away from restaurant chains, but wherever I go, PF Chang's has solidly good food and, like Starbucks, they're reliable. We were staying in Boston for a week and after a long day and blah blah blah blah...";
if (text.length()>20) {
text=text.substring(0,20)+"...";
result.setText(Html.fromHtml(text+"<font color='red'> <u>View More</u></font>"));
}
This will have ellipsize effect.
set Boolean isCheck= true;
put this in the xml:
<TextView
android:id="#+id/txt_id"
android:maxLines="2"
android:ellipsize="end"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
and the code:
txt_id= (TextView)findViewById(R.id.txt_id);
txt_id.setText("data");
txt_id.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if (isCheck) {
txt_id.setMaxLines(10);
isCheck = false;
} else {
txt_id.setMaxLines(2);
isCheck = true;
}
}
}
Check out my library: https://github.com/AhmMhd/SeeMoreTextView-Android
<com.abdulhakeem.seemoretextview.SeeMoreTextView
android:id="#+id/textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
usage:
TextView seemoreTv = (TextView) findViewById(R.id.textview)
seemoreTv.setContent("some really long text here.")
it also works great on RecyclerView.
In Kotlin adapter you can write like this to "View More" on TextView
if (News[position].description.length > 150) {
holder.desc.text = Html.fromHtml(News[position].description.substring(0, 150) + "..." + "<font color='blue'> <u>View More</u></font>")
} else {
holder.desc.text = News[position].description
}
holder.desc.setOnClickListener {
if (holder.desc.text.toString().endsWith("View More")) {
holder.desc.text = News[position].description
} else {
if (News[position].description.length > 150) {
holder.desc.text = Html.fromHtml(News[position].description.substring(0, 150) + "..." + "<font color='blue'> <u>View More</u></font>")
} else holder.desc.text = News[position].description
}
}
This solution is a bit easier to implement in code. It doesn't support on-the-fly changes well, but can easily be modified to do so.
public class ExpandableTextView extends TextView {
private final String readMoreText = "...read more";
private final int readMoreColor = Color.parseColor("#4A0281");
private int _maxLines = 4;
private CharSequence originalText;
public ExpandableTextView(Context context) {
super(context);
init(context);
}
public ExpandableTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ExpandableTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
ViewTreeObserver vto = getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#SuppressWarnings("deprecation")
#Override
public void onGlobalLayout() {
ViewTreeObserver obs = getViewTreeObserver();
obs.removeGlobalOnLayoutListener(this);
truncateText();
}
});
}
#Override
public void setText(CharSequence text, BufferType type) {
super.setText(text, type);
if (originalText == null) {
originalText = text;
}
}
#Override
public int getMaxLines() {
return _maxLines;
}
#Override
public void setMaxLines(int maxLines) {
_maxLines = maxLines;
}
public void truncateText() {
int maxLines = _maxLines;
String text = getText().toString();
if (getLineCount() >= maxLines) {
int lineEndIndex = getLayout().getLineEnd(maxLines - 1);
String truncatedText = getText().subSequence(0, lineEndIndex - readMoreText.length() + 1) + readMoreText;
Spannable spannable = new SpannableString(truncatedText);
spannable.setSpan(new ForegroundColorSpan(readMoreColor), truncatedText.length() - readMoreText.length(), truncatedText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
setText(spannable, TextView.BufferType.SPANNABLE);
super.setMaxLines(_maxLines);
}
}
public void expandText() {
setText(originalText);
super.setMaxLines(1000);
}
public void reset() {
originalText = null;
}
}
In the above solution, Expanded text (see more/less)
showing, if the length of the text is less than max lines. In this class, I remove this error. You just need to put this class into your code and use it in XML. You can easily modify it according to your requirements (color of expanded text, font style, etc.)
public class ExpandableTextView extends AppCompatTextView {
private static Context context;
private TextView textView;
private int maxLine = 3;
private boolean isViewMore = true;
public ExpandableTextView(Context context) {
super(context);
ExpandableTextView.context = context;
textView = this;
initViews();
}
public ExpandableTextView(Context context, AttributeSet attrs) {
super(context, attrs);
ExpandableTextView.context = context;
textView = this;
initViews();
}
public ExpandableTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ExpandableTextView.context = context;
textView = this;
initViews();
}
public void initViews() {
if (textView.getText().toString().isEmpty()) {
return;
}
if (textView.getTag() == null) {
textView.setTag(textView.getText());
}
textView.setTypeface(Typeface.createFromAsset(context.getAssets(), "GothamBook.ttf"));
ViewTreeObserver vto = textView.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#SuppressWarnings("deprecation")
#Override
public void onGlobalLayout() {
String text, expandText = "See ";
int lineEndIndex;
ViewTreeObserver obs = textView.getViewTreeObserver();
obs.removeGlobalOnLayoutListener(this);
int lineCount = textView.getLayout().getLineCount();
expandText += isViewMore ? "More" : "Less";
if (lineCount <= maxLine) {
lineEndIndex = textView.getLayout().getLineEnd(textView.getLayout().getLineCount() - 1);
text = textView.getText().subSequence(0, lineEndIndex).toString();
} else if (isViewMore && maxLine > 0 && textView.getLineCount() >= maxLine) {
lineEndIndex = textView.getLayout().getLineEnd(maxLine - 1);
text = textView.getText().subSequence(0, lineEndIndex - expandText.length() + 1) + " " + expandText;
} else {
lineEndIndex = textView.getLayout().getLineEnd(textView.getLayout().getLineCount() - 1);
text = textView.getText().subSequence(0, lineEndIndex) + " " + expandText;
}
textView.setText(text);
textView.setMovementMethod(LinkMovementMethod.getInstance());
if (lineCount > maxLine)
textView.setText(addClickablePartTextViewResizable(expandText),
BufferType.SPANNABLE);
textView.setSelected(true);
}
});
}
private SpannableStringBuilder addClickablePartTextViewResizable(final String expandText) {
String string = textView.getText().toString();
SpannableStringBuilder expandedStringBuilder = new SpannableStringBuilder(string);
if (string.contains(expandText)) {
expandedStringBuilder.setSpan(new ClickableSpan() {
#Override
public void onClick(View widget) {
textView.setLayoutParams(textView.getLayoutParams());
textView.setText(textView.getTag().toString(), BufferType.SPANNABLE);
textView.invalidate();
maxLine = isViewMore ? -1 : 3;
isViewMore = !isViewMore;
initViews();
}
#Override
public void updateDrawState(#NonNull TextPaint ds) {
ds.setUnderlineText(true);
ds.setColor(context.getResources().getColor(R.color.red));
ds.setTypeface(Typeface.createFromAsset(context.getAssets(), "GothamMedium.ttf"));
}
}, string.indexOf(expandText), string.length(), 0);
}
return expandedStringBuilder;
}
}
If you set dynamic data you need to call initViews() after setting the text into the text view.
tvDescription.setText(sessionModel.getDescription());
tvDescription.initViews();
Thanks to Jitender's answer.Improving on it I have done the below implementation based on length of text.This might not be ideal solution if you want View More option exactly after specified number of lines but assuming that there are around 50 characters in single line the solution will work well if you are adjustable with number of lines.Below solution will add View More option if text length is greater than 150 and will ellipsize the text to 150 characters.On clicking on View More it will show complete text with Show Less option and on clicking on Show Less again it will ellipsize the text to 150 characters.No separate view is required.Also it works well with recyclerview item's textview.
if(inputText.length()>150)
{
String text=inputText.substring(0,150)+"...";
final String fulltext=inputText;
final SpannableString ss = new SpannableString(text+"View More");
ClickableSpan span1 = new ClickableSpan() {
#Override
public void onClick(View textView) {
// do some thing
SpannableString ss1 = new SpannableString(fulltext+"Show Less");
ClickableSpan span2 = new ClickableSpan() {
#Override
public void onClick(View textView) {
// do some thing
textView.setText(ss);
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
};
ss1.setSpan(span2, fulltext.length(), ss1.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ss1.setSpan(new ForegroundColorSpan(Color.BLUE), fulltext.length(), ss1.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(ss1);
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
};
ss.setSpan(span1, 153, 162, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ss.setSpan(new ForegroundColorSpan(Color.BLUE), 153,162,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(ss);
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
else
{
textView.setText(inputText);
}
Try using this library :)
//Add this dependency into App Gradle
implementation 'com.borjabravo:readmoretextview:2.1.0'
Usage:
<com.borjabravo.readmoretextview.ReadMoreTextView
android:id="#+id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/activity_vertical_margin"
android:text="#string/DemoText"
app:colorClickableText="#3F51B5"/>
Check This Link: https://github.com/bravoborja/ReadMoreTextView
You can use below code for this;
holder.tvMoreInfo.setText(horizontalList.get(position));
holder.tvMoreInfo.post(new Runnable() {
#Override
public void run() {
int lineCount = holder.tvMoreInfo.getLineCount();
if (lineCount<3)
{
}else
{
makeTextViewResizable(holder.tvMoreInfo, 3, "...More", true);
}
}
});
public static void makeTextViewResizable(final TextView tv, final int maxLine, final String expandText, final boolean viewMore) {
if (tv.getTag() == null) {
tv.setTag(tv.getText());
}
ViewTreeObserver vto = tv.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#SuppressWarnings("deprecation")
#Override
public void onGlobalLayout() {
String text;
int lineEndIndex;
ViewTreeObserver obs = tv.getViewTreeObserver();
obs.removeGlobalOnLayoutListener(this);
if (maxLine == 0) {
lineEndIndex = tv.getLayout().getLineEnd(0);
text = tv.getText().subSequence(0, lineEndIndex - expandText.length() + 1) + " " + "<font color=\"#F15d36\">" + expandText + "</font>";
} else if (maxLine > 0 && tv.getLineCount() >= maxLine) {
lineEndIndex = tv.getLayout().getLineEnd(maxLine - 1);
text = tv.getText().subSequence(0, lineEndIndex - expandText.length() + 1) + " " + "<font color=\"#F15d36\">" + expandText + "</font>";
} else {
lineEndIndex = tv.getLayout().getLineEnd(tv.getLayout().getLineCount() - 1);
text = tv.getText().subSequence(0, lineEndIndex) + " " + "<font color=\"#F15d36\">" + expandText + "</font>";
}
tv.setText(Html.fromHtml(text));
tv.setMovementMethod(LinkMovementMethod.getInstance());
tv.setText(
addClickablePartTextViewResizable(Html.fromHtml(tv.getText().toString()), tv, lineEndIndex, expandText,
viewMore), TextView.BufferType.SPANNABLE);
}
});
}
private static SpannableStringBuilder addClickablePartTextViewResizable(final Spanned strSpanned, final TextView tv,
final int maxLine, final String spanableText, final boolean viewMore) {
String str = strSpanned.toString();
SpannableStringBuilder ssb = new SpannableStringBuilder(strSpanned);
if (str.contains(spanableText)) {
ssb.setSpan(new MySpannable(false) {
#Override
public void onClick(View widget) {
tv.setLayoutParams(tv.getLayoutParams());
tv.setText(tv.getTag().toString(), TextView.BufferType.SPANNABLE);
tv.invalidate();
if (viewMore) {
makeTextViewResizable(tv, -1, "...Less", false);
} else {
makeTextViewResizable(tv, 3, "...More", true);
}
}
}, str.indexOf(spanableText), str.indexOf(spanableText) + spanableText.length(), 0);
}
return ssb;
}
It might be late but its the easiest and tested way to handle this issue in recyclerview .
first check the length of textview and set view more if require
if (inventory.getDescription().length()>90) {
inventoryDescription.setText(Html.fromHtml(inventory.getDescription().substring(0,90)+"..."+"<font color='blue'> <u>View More</u></font>"));
}
else inventoryDescription.setText(inventory.getDescription());
and in textview click listener
inventoryDescription.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (inventoryDescription.getText().toString().endsWith("View More")) {
inventoryDescription.setText(inventory.getDescription());
}
else {
if (inventory.getDescription().length()>90) {
inventoryDescription.setText(Html.fromHtml(inventory.getDescription().substring(0,90)+"..."+"<font color='blue'> <u>View More</u></font>"));
}
else inventoryDescription.setText(inventory.getDescription());
}
}
});
I have written a blog post about how I did it in our app. It is based on some of the other solutions here, it can display read more... / read less, and it also works great in RecyclerViews, because the text is calculated on the main thread immediately.
Here's the blog post.
And here's the code, which is also linked in the post.
I faced some problems with specified solutions, but this library working perfectly with recycler view and solved my problems.
Sample code:
<io.github.giangpham96.expandabletextview.ExpandableTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/purple_100"
android:padding="16dp"
android:maxLines="10"
app:expandAction="More"
app:limitedMaxLines="2"
app:expandActionColor="#color/blue_500"
app:originalText="#string/long_text" />
Kotlin Extension method based on Abdul Hakim's Library
fun TextView.setSeeMoreOrLessView(msg: String, maxLengthToShowSeeMore: Int){
if (msg.length <= maxLengthToShowSeeMore) {
text = msg
return
}
val seeMoreText = "...See more"
val seeLessText = "...See less"
val spannableTextSeeMore = SpannableString("${msg.take(maxLengthToShowSeeMore)}$seeMoreText")
val spannableTextSeeLess = SpannableString("$msg$seeLessText")
val clickableSpan = object : ClickableSpan(){
override fun onClick(widget: View) {
//change spannable string
val currentTag = tag as? String?
if (currentTag?.equals(seeMoreText) == true){
text = spannableTextSeeLess
tag = seeLessText
} else {
text = spannableTextSeeMore
tag = seeMoreText
}
}
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.isUnderlineText = false
}
}
spannableTextSeeMore.setSpan(
clickableSpan,
maxLengthToShowSeeMore,
maxLengthToShowSeeMore+seeMoreText.length,
0
)
spannableTextSeeLess.setSpan(
clickableSpan,
msg.length,
msg.length+seeLessText.length,
0
)
text = spannableTextSeeMore // default
tag = seeMoreText
movementMethod = LinkMovementMethod() }
Instead of using
android:layout_alignParentLeft="true" in first textview use
android:layout_toLeftOf="#+id/textView1"
This should take care of the overlapping text
The issue I'm trying to fix is the following: I'm having a TextView and I'm using a Spannable to set some characters bold.
The text needs to have a maxim of 2 lines ( android:maxLines="2") and I want the text to be ellipsized, but for some reason I cannot make the text ellipsized.
Here is the simple code:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView android:id="#+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:maxLines="2"
android:ellipsize="end"
android:bufferType="spannable"
android:text="#string/app_name"
android:textSize="15dp"/>
</LinearLayout>
and the activity:
public class MyActivity extends Activity {
private TextView name;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
name= (TextView) findViewById(R.id.name);
name.setText("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy ");
Spannable spannable = (Spannable)name.getText();
StyleSpan boldSpan = new StyleSpan( Typeface.BOLD );
spannable.setSpan( boldSpan, 10, 15, Spannable.SPAN_INCLUSIVE_INCLUSIVE );
}
}
The text is truncated, no "..." are displayed.
I realise this is a very old post, but seeing as it's still unanswered and I also ran into this issue today I thought I would post a solution to this. Hopefully it helps someone in the future.
ViewTreeObserver viewTreeObserver = textView.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener()
{
#Override
public void onGlobalLayout()
{
ViewTreeObserver viewTreeObserver = textView.getViewTreeObserver();
viewTreeObserver.removeOnGlobalLayoutListener(this);
if (textView.getLineCount() > 5)
{
int endOfLastLine = textView.getLayout().getLineEnd(4);
String newVal = textView.getText().subSequence(0, endOfLastLine - 3) + "...";
textView.setText(newVal);
}
}
});
Having same problem and seems the following works for me:
Spannable wordtoSpan = new SpannableString(lorem);
wordtoSpan.setSpan(new ForegroundColorSpan(0xffff0000), 0, 10, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
wordtoSpan.setSpan(new ForegroundColorSpan(0xff00ffff), 20, 35, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(wordtoSpan);
in xml the textView has android:multiLine set, and android:ellipsize="end", and android:singleLine="false;
You're right in that ellipsize, declared either in xml or in code won't work on spannable text.
However, with a little bit of investigation you can actually do the ellipsizing yourself:
private TextView name;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
name= (TextView) findViewById(R.id.name);
String lorem = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy "
name.setText(lorem);
Spannable spannable = (Spannable)name.getText();
StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
spannable.setSpan( boldSpan, 10, 15, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
int maxLines = 2;
// in my experience, this needs to be called in code, your mileage may vary.
name.setMaxLines(maxLines);
// check line count.. this will actually be > than the # of visible lines
// if it is long enough to be truncated
if (name.getLineCount() > maxLines){
// this returns _1 past_ the index of the last character shown
// on the indicated line. the lines are zero indexed, so the last
// valid line is maxLines -1;
int lastCharShown = name.getLayout().getLineVisibleEnd(maxLines - 1);
// chop off some characters. this value is arbitrary, i chose 3 just
// to be conservative.
int numCharsToChop = 3;
String truncatedText = lorem.substring(0, lastCharShown - numCharsToChop);
// ellipsize! note ellipsis character.
name.setText(truncatedText+"…");
// reapply the span, since the text has been changed.
spannable.setSpan(boldSpan, 10, 15, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
}
}
This may a little trick by using reflection to solve this problem. After reading the source code of AOSP, in TextView.java, DynamicLayout only contains a static field member named sStaticLayout and it's constructed by new StaticLayout(null) without any params including maxLines.
Therefore, doEllipsis will alway be false as mMaximumVisibleLineCount is set Integer.MAX_VALUE by default.
boolean firstLine = (j == 0);
boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
boolean lastLine = currentLineIsTheLastVisibleOne || (end == bufEnd);
......
if (ellipsize != null) {
// If there is only one line, then do any type of ellipsis except when it is MARQUEE
// if there are multiple lines, just allow END ellipsis on the last line
boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
boolean doEllipsis =
(((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) &&
ellipsize != TextUtils.TruncateAt.MARQUEE) ||
(!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
ellipsize == TextUtils.TruncateAt.END);
if (doEllipsis) {
calculateEllipsis(start, end, widths, widthStart,
ellipsisWidth, ellipsize, j,
textWidth, paint, forceEllipsis);
}
}
So I extends the TextView and make a View named EllipsizeTextView
public class EllipsizeTextView extends TextView {
public EllipsizeTextView(Context context) {
super(context);
}
public EllipsizeTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public EllipsizeTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
}
public EllipsizeTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
StaticLayout layout = null;
Field field = null;
try {
Field staticField = DynamicLayout.class.getDeclaredField("sStaticLayout");
staticField.setAccessible(true);
layout = (StaticLayout) staticField.get(DynamicLayout.class);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
if (layout != null) {
try {
field = StaticLayout.class.getDeclaredField("mMaximumVisibleLineCount");
field.setAccessible(true);
field.setInt(layout, getMaxLines());
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (layout != null && field != null) {
try {
field.setInt(layout, Integer.MAX_VALUE);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
problem solved!
Another solution is to overwrite onDraw of TextView. The following suggested solution, doesn't make use any reflection technique. Hence, shouldn't break in the future, in case any member variable naming changes.
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.os.Build;
import android.text.Layout;
import android.text.SpannableStringBuilder;
import android.util.AttributeSet;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
public class EllipsizeTextView extends TextView {
private static final String THREE_DOTS = "...";
private static final int THREE_DOTS_LENGTH = THREE_DOTS.length();
private volatile boolean enableEllipsizeWorkaround = false;
private SpannableStringBuilder spannableStringBuilder;
public EllipsizeTextView(Context context) {
super(context);
}
public EllipsizeTextView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public EllipsizeTextView(Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public EllipsizeTextView(Context context, #Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public void setEnableEllipsizeWorkaround(boolean enableEllipsizeWorkaround) {
this.enableEllipsizeWorkaround = enableEllipsizeWorkaround;
}
// https://stackoverflow.com/questions/14691511/textview-using-spannable-ellipsize-doesnt-work
// https://blog.csdn.net/htyxz8802/article/details/50387950
#Override
protected void onDraw(Canvas canvas) {
if (enableEllipsizeWorkaround && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
final Layout layout = getLayout();
if (layout.getLineCount() >= getMaxLines()) {
CharSequence charSequence = getText();
int lastCharDown = layout.getLineVisibleEnd(getMaxLines()-1);
if (lastCharDown >= THREE_DOTS_LENGTH && charSequence.length() > lastCharDown) {
if (spannableStringBuilder == null) {
spannableStringBuilder = new SpannableStringBuilder();
} else {
spannableStringBuilder.clear();
}
spannableStringBuilder.append(charSequence.subSequence(0, lastCharDown - THREE_DOTS_LENGTH)).append(THREE_DOTS);
setText(spannableStringBuilder);
}
}
}
super.onDraw(canvas);
}
}
This is a known issue in the Android framework: https://code.google.com/p/android/issues/detail?id=67186
A simple and working solution
This is my code ->
<TextView
android:id="#+id/textViewProfileContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="false"
android:ellipsize="end"
android:maxLines="3"
android:textSize="14sp"
android:textColor="#000000" />
SpannableStringBuilder sb = new SpannableStringBuilder();
SpannableString attrAdditional = new SpannableString(additionalText);
attrAdditional.SetSpan(new StyleSpan(TypefaceStyle.Bold), 0, additionalText.Length, 0);...
sb.Append(attrAdditional);...
ProfileContent.SetText(sb, **TextView.BufferType.Normal**);
Result
As for single line case, android:maxLines is not working,but android:singleLine is ok.
Similar to #Dallas187 but doesn't break link formatting
fun TextView.addEllipsizeToSpannedOnLayout() {
doOnNextLayout {
if (maxLines != -1 && lineCount > maxLines) {
val endOfLastLine = layout.getLineEnd(maxLines - 1)
val spannedDropLast3Chars = text.subSequence(0, endOfLastLine - 3) as? Spanned
if (spannedDropLast3Chars != null) {
val spannableBuilder = SpannableStringBuilder()
.append(spannedDropLast3Chars)
.append("…")
text = spannableBuilder
}
}
}
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (maxLength < 0) {
Layout layout = getLayout();
if (layout.getLineCount() > getMaxLines()) {
maxLength = layout.getLineVisibleEnd(getMaxLines() - 1) - 1;
setSpannableString();
}
}
}
The "setSpannableString()" shows below:
private void setSpannableString() {
// ShowString is real visible string. FullString is original string which
// is useful when caching and calculating.
String showString = fullString;
if (maxLength > 0) {
showString = fullString.substring(0, maxLength) + THREE_DOTS;
}
SpannableStringBuilder builder = new SpannableStringBuilder(showString);
for (int i = 0; i < mHighLightColor.size(); i++) {
String highLightString = mHighLightString.get(i);
int color = mHighLightColor.get(i);
int start = fullString.indexOf(highLightString);
int end = Math.min(start + highLightString.length(), showString.length());
if (mClickableSpanList.get(i) != null) {
builder.setSpan(mClickableSpanList.get(i), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
setMovementMethod(LinkMovementMethod.getInstance());
}
builder.setSpan(new ColorBoldSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
setText(builder, BufferType.SPANNABLE);
}
Use Kotlin's CharSequence instead of using SpannableString directly.
for eg:
finalString = "your very long string"
var spannable :CharSequence? = ""
spannable = SpannableStringBuilder(finalString).apply{
setSpan(.....)
setSpan(.....)
setSpan(.....)
}
textView.text = spannable
Kotlin's CharSequence works perfectly with ellipsize for more than one line.
Try This Its Work for me.
This is my text view
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:ellipsize="end"
android:text="Type here maximum characters."
android:textSize="16sp" />