Android - Expandable TextView with Animation
Smooth expanding (using heigh & ObjectAnimator)
FYI: requires API 11
public static void expandCollapsedByMaxLines(@NonNull final TextView text) {
final int height = text.getMeasuredHeight();
text.setHeight(height);
text.setMaxLines(Integer.MAX_VALUE); //expand fully
text.measure(View.MeasureSpec.makeMeasureSpec(text.getMeasuredWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(ViewGroup.LayoutParams.WRAP_CONTENT, View.MeasureSpec.UNSPECIFIED));
final int newHeight = text.getMeasuredHeight();
ObjectAnimator animation = ObjectAnimator.ofInt(text, "height", height, newHeight);
animation.setDuration(250).start();
}
P.S. I assume TextView limited by maxLines.
P.S.S. Thanks Amagi82 for ObjectAnimator example
You can check my blog post on ExpandableTexTView:
The idea is, initially the TextView will show a small portion of a long text and when it is clicked, it will show the rest of the text.
So here is the code that how I solved it.
package com.rokonoid.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.text.SpannableStringBuilder;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
* User: Bazlur Rahman Rokon
* Date: 9/7/13 - 3:33 AM
*/
public class ExpandableTextView extends TextView {
private static final int DEFAULT_TRIM_LENGTH = 200;
private static final String ELLIPSIS = ".....";
private CharSequence originalText;
private CharSequence trimmedText;
private BufferType bufferType;
private boolean trim = true;
private int trimLength;
public ExpandableTextView(Context context) {
this(context, null);
}
public ExpandableTextView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ExpandableTextView);
this.trimLength = typedArray.getInt(R.styleable.ExpandableTextView_trimLength, DEFAULT_TRIM_LENGTH);
typedArray.recycle();
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
trim = !trim;
setText();
requestFocusFromTouch();
}
});
}
private void setText() {
super.setText(getDisplayableText(), bufferType);
}
private CharSequence getDisplayableText() {
return trim ? trimmedText : originalText;
}
@Override
public void setText(CharSequence text, BufferType type) {
originalText = text;
trimmedText = getTrimmedText(text);
bufferType = type;
setText();
}
private CharSequence getTrimmedText(CharSequence text) {
if (originalText != null && originalText.length() > trimLength) {
return new SpannableStringBuilder(originalText, 0, trimLength + 1).append(ELLIPSIS);
} else {
return originalText;
}
}
public CharSequence getOriginalText() {
return originalText;
}
public void setTrimLength(int trimLength) {
this.trimLength = trimLength;
trimmedText = getTrimmedText(originalText);
setText();
}
public int getTrimLength() {
return trimLength;
}
}
And add the following line in your attr.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ExpandableTextView">
<attr name="trimLength" format="integer"/>
</declare-styleable>
</resources>
Put the following in your main.xml
<?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">
<com.rokonoid.widget.ExpandableTextView
android:id="@+id/lorem_ipsum"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
And test your activity
package com.rokonoid.widget;
import android.app.Activity;
import android.os.Bundle;
public class MyActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
String yourText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
"Ut volutpat interdum interdum. Nulla laoreet lacus diam, vitae " +
"sodales sapien commodo faucibus. Vestibulum et feugiat enim. Donec " +
"semper mi et euismod tempor. Sed sodales eleifend mi id varius. Nam " +
"et ornare enim, sit amet gravida sapien. Quisque gravida et enim vel " +
"volutpat. Vivamus egestas ut felis a blandit. Vivamus fringilla " +
"dignissim mollis. Maecenas imperdiet interdum hendrerit. Aliquam" +
" dictum hendrerit ultrices. Ut vitae vestibulum dolor. Donec auctor ante" +
" eget libero molestie porta. Nam tempor fringilla ultricies. Nam sem " +
"lectus, feugiat eget ullamcorper vitae, ornare et sem. Fusce dapibus ipsum" +
" sed laoreet suscipit. ";
ExpandableTextView expandableTextView = (ExpandableTextView) findViewById(R.id.lorem_ipsum);
expandableTextView.setText(yourText);
}
}
Reference: Android – Expandable TextView
Use an ObjectAnimator.
ObjectAnimator animation = ObjectAnimator.ofInt(yourTextView, "maxLines", tv.getLineCount());
animation.setDuration(200).start();
This will fully expand your TextView over 200 milliseconds. You can replace tv.getLineCount()
with however many lines of text you wish to collapse it back down.
----Update----
Here are some convenience methods you can drop in:
private void expandTextView(TextView tv){
ObjectAnimator animation = ObjectAnimator.ofInt(tv, "maxLines", tv.getLineCount());
animation.setDuration(200).start();
}
private void collapseTextView(TextView tv, int numLines){
ObjectAnimator animation = ObjectAnimator.ofInt(tv, "maxLines", numLines);
animation.setDuration(200).start();
}
If you're on API 16+, you can use textView.getMaxLines() to easily determine if your textView has been expanded or not.
private void cycleTextViewExpansion(TextView tv){
int collapsedMaxLines = 3;
ObjectAnimator animation = ObjectAnimator.ofInt(tv, "maxLines",
tv.getMaxLines() == collapsedMaxLines? tv.getLineCount() : collapsedMaxLines);
animation.setDuration(200).start();
}
Notes:
If maxLines has not been set, or you've set the height of your textView in pixels, you can get an ArrayIndexOutOfBounds exception.
The above examples always take 200ms, whether they expand by 3 lines or 400. If you want a consistent rate of expansion, you can do something like this:
int duration = (textView.getLineCount() - collapsedMaxLines) * 10;
I created an open-source library for this, because I wasn’t satisfied with the other solutions I found on the internet. I’ve put the thing on GitHub, and it’s free to use by anyone.
public class ExpandableTextView extends TextView
{
// copy off TextView.LINES
private static final int MAXMODE_LINES = 1;
private OnExpandListener onExpandListener;
private TimeInterpolator expandInterpolator;
private TimeInterpolator collapseInterpolator;
private final int maxLines;
private long animationDuration;
private boolean animating;
private boolean expanded;
private int originalHeight;
public ExpandableTextView(final Context context)
{
this(context, null);
}
public ExpandableTextView(final Context context, final AttributeSet attrs)
{
this(context, attrs, 0);
}
public ExpandableTextView(final Context context, final AttributeSet attrs, final int defStyle)
{
super(context, attrs, defStyle);
// read attributes
final TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.ExpandableTextView, defStyle, 0);
this.animationDuration = attributes.getInt(R.styleable.ExpandableTextView_animation_duration, BuildConfig.DEFAULT_ANIMATION_DURATION);
attributes.recycle();
// keep the original value of maxLines
this.maxLines = this.getMaxLines();
// create default interpolators
this.expandInterpolator = new AccelerateDecelerateInterpolator();
this.collapseInterpolator = new AccelerateDecelerateInterpolator();
}
@Override
public int getMaxLines()
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
{
return super.getMaxLines();
}
try
{
final Field mMaxMode = TextView.class.getField("mMaxMode");
mMaxMode.setAccessible(true);
final Field mMaximum = TextView.class.getField("mMaximum");
mMaximum.setAccessible(true);
final int mMaxModeValue = (int) mMaxMode.get(this);
final int mMaximumValue = (int) mMaximum.get(this);
return mMaxModeValue == MAXMODE_LINES ? mMaximumValue : -1;
}
catch (final Exception e)
{
return -1;
}
}
/**
* Toggle the expanded state of this {@link ExpandableTextView}.
* @return true if toggled, false otherwise.
*/
public boolean toggle()
{
if (this.expanded)
{
return this.collapse();
}
return this.expand();
}
/**
* Expand this {@link ExpandableTextView}.
* @return true if expanded, false otherwise.
*/
public boolean expand()
{
if (!this.expanded && !this.animating && this.maxLines >= 0)
{
this.animating = true;
// notify listener
if (this.onExpandListener != null)
{
this.onExpandListener.onExpand(this);
}
// get original height
this.measure
(
MeasureSpec.makeMeasureSpec(this.getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
);
this.originalHeight = this.getMeasuredHeight();
// set maxLines to MAX Integer
this.setMaxLines(Integer.MAX_VALUE);
// get new height
this.measure
(
MeasureSpec.makeMeasureSpec(this.getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
);
final int fullHeight = this.getMeasuredHeight();
final ValueAnimator valueAnimator = ValueAnimator.ofInt(this.originalHeight, fullHeight);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
{
@Override
public void onAnimationUpdate(final ValueAnimator animation)
{
final ViewGroup.LayoutParams layoutParams = ExpandableTextView.this.getLayoutParams();
layoutParams.height = (int) animation.getAnimatedValue();
ExpandableTextView.this.setLayoutParams(layoutParams);
}
});
valueAnimator.addListener(new AnimatorListenerAdapter()
{
@Override
public void onAnimationEnd(final Animator animation)
{
ExpandableTextView.this.expanded = true;
ExpandableTextView.this.animating = false;
}
});
// set interpolator
valueAnimator.setInterpolator(this.expandInterpolator);
// start the animation
valueAnimator
.setDuration(this.animationDuration)
.start();
return true;
}
return false;
}
/**
* Collapse this {@link TextView}.
* @return true if collapsed, false otherwise.
*/
public boolean collapse()
{
if (this.expanded && !this.animating && this.maxLines >= 0)
{
this.animating = true;
// notify listener
if (this.onExpandListener != null)
{
this.onExpandListener.onCollapse(this);
}
// get new height
final int fullHeight = this.getMeasuredHeight();
final ValueAnimator valueAnimator = ValueAnimator.ofInt(fullHeight, this.originalHeight);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
{
@Override
public void onAnimationUpdate(final ValueAnimator animation)
{
final ViewGroup.LayoutParams layoutParams = ExpandableTextView.this.getLayoutParams();
layoutParams.height = (int) animation.getAnimatedValue();
ExpandableTextView.this.setLayoutParams(layoutParams);
}
});
valueAnimator.addListener(new AnimatorListenerAdapter()
{
@Override
public void onAnimationEnd(final Animator animation)
{
// set maxLines to original value
ExpandableTextView.this.setMaxLines(ExpandableTextView.this.maxLines);
ExpandableTextView.this.expanded = false;
ExpandableTextView.this.animating = false;
}
});
// set interpolator
valueAnimator.setInterpolator(this.collapseInterpolator);
// start the animation
valueAnimator
.setDuration(this.animationDuration)
.start();
return true;
}
return false;
}
/**
* Sets the duration of the expand / collapse animation.
* @param animationDuration duration in milliseconds.
*/
public void setAnimationDuration(final long animationDuration)
{
this.animationDuration = animationDuration;
}
/**
* Sets a listener which receives updates about this {@link ExpandableTextView}.
* @param onExpandListener the listener.
*/
public void setOnExpandListener(final OnExpandListener onExpandListener)
{
this.onExpandListener = onExpandListener;
}
/**
* Returns the {@link OnExpandListener}.
* @return the listener.
*/
public OnExpandListener getOnExpandListener()
{
return onExpandListener;
}
/**
* Sets a {@link TimeInterpolator} for expanding and collapsing.
* @param interpolator the interpolator
*/
public void setInterpolator(final TimeInterpolator interpolator)
{
this.expandInterpolator = interpolator;
this.collapseInterpolator = interpolator;
}
/**
* Sets a {@link TimeInterpolator} for expanding.
* @param expandInterpolator the interpolator
*/
public void setExpandInterpolator(final TimeInterpolator expandInterpolator)
{
this.expandInterpolator = expandInterpolator;
}
/**
* Returns the current {@link TimeInterpolator} for expanding.
* @return the current interpolator, null by default.
*/
public TimeInterpolator getExpandInterpolator()
{
return this.expandInterpolator;
}
/**
* Sets a {@link TimeInterpolator} for collpasing.
* @param collapseInterpolator the interpolator
*/
public void setCollapseInterpolator(final TimeInterpolator collapseInterpolator)
{
this.collapseInterpolator = collapseInterpolator;
}
/**
* Returns the current {@link TimeInterpolator} for collapsing.
* @return the current interpolator, null by default.
*/
public TimeInterpolator getCollapseInterpolator()
{
return this.collapseInterpolator;
}
/**
* Is this {@link ExpandableTextView} expanded or not?
* @return true if expanded, false if collapsed.
*/
public boolean isExpanded()
{
return this.expanded;
}
public interface OnExpandListener
{
void onExpand(ExpandableTextView view);
void onCollapse(ExpandableTextView view);
}
}
Using the ExpandableTextView is very easy, it’s just a regular TextView with some extra functionality added to it. By defining the android:maxLines attribute, you can set the default number of lines for the TextView collapsed state.
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<at.blogc.android.views.ExpandableTextView
android:id="@+id/expandableTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/lorem_ipsum"
android:maxLines="5"
android:ellipsize="end"
app:animation_duration="1000"/>
<!-- Optional parameter animation_duration: sets the duration of the expand animation -->
<Button
android:id="@+id/button_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/expand"/>
</LinearLayout>
In your Activity or Fragment:
final ExpandableTextView expandableTextView = (ExpandableTextView) this.findViewById(R.id.expandableTextView);
final Button buttonToggle = (Button) this.findViewById(R.id.button_toggle);
// set animation duration via code, but preferable in your layout files by using the animation_duration attribute
expandableTextView.setAnimationDuration(1000L);
// set interpolators for both expanding and collapsing animations
expandableTextView.setInterpolator(new OvershootInterpolator());
// or set them separately
expandableTextView.setExpandInterpolator(new OvershootInterpolator());
expandableTextView.setCollapseInterpolator(new OvershootInterpolator());
// toggle the ExpandableTextView
buttonToggle.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(final View v)
{
expandableTextView.toggle();
buttonToggle.setText(expandableTextView.isExpanded() ? R.string.collapse : R.string.expand);
}
});
// but, you can also do the checks yourself
buttonToggle.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(final View v)
{
if (expandableTextView.isExpanded())
{
expandableTextView.collapse();
buttonToggle.setText(R.string.expand);
}
else
{
expandableTextView.expand();
buttonToggle.setText(R.string.collapse);
}
}
});
// listen for expand / collapse events
expandableTextView.setOnExpandListener(new ExpandableTextView.OnExpandListener()
{
@Override
public void onExpand(final ExpandableTextView view)
{
Log.d(TAG, "ExpandableTextView expanded");
}
@Override
public void onCollapse(final ExpandableTextView view)
{
Log.d(TAG, "ExpandableTextView collapsed");
}
});
You can easily add this library as a gradle dependency to your Android project. Take a look at the project on Github for further instructions:
https://github.com/Blogcat/Android-ExpandableTextView