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