Android-带动画的可扩展文本视图
我有一个Android-带动画的可扩展文本视图,android,android-animation,textview,Android,Android Animation,Textview,我有一个TextView,它首先显示了长文本的一小部分 用户可以按“查看更多”按钮展开TextView并查看该文本的其余部分 在进行测试时,我只需将TextView.setMaxLines的值在4(用于折叠)和Integer.MAX_(用于扩展)之间进行交换,就可以实现这一点 现在,我希望这个行为伴随着一个动画。我知道有一个解决方案几乎完成了,但我试图实现它,但没有成功 有人能帮我吗?你可以查看我在ExpandableTexTView上的博文: 想法是,最初文本视图将显示长文本的一小部分,单击它
TextView
,它首先显示了长文本的一小部分
用户可以按“查看更多”按钮展开TextView
并查看该文本的其余部分
在进行测试时,我只需将TextView.setMaxLines
的值在4(用于折叠)和Integer.MAX_(用于扩展)之间进行交换,就可以实现这一点
现在,我希望这个行为伴随着一个动画。我知道有一个解决方案几乎完成了,但我试图实现它,但没有成功
有人能帮我吗?你可以查看我在ExpandableTexTView上的博文: 想法是,最初文本视图将显示长文本的一小部分,单击它时,它将显示其余文本 下面是我如何解决它的代码
package com.rokonoid.widget;
导入android.content.Context;
导入android.content.res.TypedArray;
导入android.text.SpannableStringBuilder;
导入android.util.AttributeSet;
导入android.view.view;
导入android.widget.TextView;
/**
*用户:Bazlur Rahman Rokon
*日期:2013年7月9日-凌晨3:33
*/
公共类ExpandableTextView扩展了TextView{
专用静态最终整型默认整型长度=200;
私有静态最终字符串省略号=“…”;
私有字符序列原始文本;
私有字符序列和文本;
私有缓冲类型缓冲类型;
私有布尔修剪=真;
私有整数长度;
公共可扩展文本视图(上下文){
这个(上下文,空);
}
公共可扩展文本视图(上下文、属性集属性){
超级(上下文,attrs);
TypedArray TypedArray=context.actainStyledAttributes(attrs,R.styleable.ExpandableTextView);
this.trimLength=typedArray.getInt(R.styleable.ExpandableTextView\u trimLength,默认值\u TRIM\u LENGTH);
typedArray.recycle();
setOnClickListener(新的OnClickListener(){
@凌驾
公共void onClick(视图v){
修剪=!修剪;
setText();
requestFocusFromTouch();
}
});
}
私有void setText(){
super.setText(getDisplayableText(),bufferType);
}
私有CharSequence getDisplayableText(){
返回修剪?修剪文本:原始文本;
}
@凌驾
public void setText(CharSequence text,BufferType){
原始文本=文本;
trimmedText=getTrimmedText(文本);
缓冲类型=类型;
setText();
}
私有CharSequence gettrimmetext(CharSequence text){
if(originalText!=null&&originalText.length()>trimLength){
返回新的SpannableStringBuilder(originalText,0,trimLength+1)。追加(省略号);
}否则{
返回原始文本;
}
}
公共字符序列getOriginalText(){
返回原始文本;
}
公共无效设置trimLength(int trimLength){
this.trimLength=trimLength;
trimmedText=getTrimmedText(原始文本);
setText();
}
public int getTrimLength(){
返回长度;
}
}
并在attr.xml中添加以下行
使用一个
这将在200毫秒内完全扩展TextView。您可以将tv.getLineCount()
替换为您希望将其向下折叠的任意多行文本
----更新----
以下是一些您可以访问的方便方法:
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();
}
如果您使用的是API 16+,则可以使用textView.getMaxLines()轻松确定您的textView是否已展开
private void cycleTextViewExpansion(TextView tv){
int collapsedMaxLines = 3;
ObjectAnimator animation = ObjectAnimator.ofInt(tv, "maxLines",
tv.getMaxLines() == collapsedMaxLines? tv.getLineCount() : collapsedMaxLines);
animation.setDuration(200).start();
}
注:
如果尚未设置maxLines,或者您已经设置了maxLines,则可以获得ArrayIndexOutOfBounds异常
上述示例始终需要200ms,无论它们是扩展3行还是400行。如果您想要一致的扩展速率,可以执行以下操作:
int duration = (textView.getLineCount() - collapsedMaxLines) * 10;
平滑展开(使用高度和)
仅供参考:需要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();
}
另外,我假设TextView受到maxLines的限制。
另请注意:感谢ObjectAnimator示例如果您想根据行数进行此操作,请使用以下方法:
(全部代码)
/**
*当文本行超过{@link#makeExpandable}方法提供的值时,请省略文本。
*根据需要追加{@link#MORE}或{@link#LESS}。
*TODO:添加动画
*由vedant于2015年3月10日创建。
*/
公共类ExpandableTextView扩展了TextView{
私有静态最终字符串标记=“ExpandableTextView”;
私有静态最终字符串ELLIPSIZE=“…”;
私有静态最终字符串MORE=“MORE”;
私有静态最终字符串LESS=“LESS”;
私有字符串;
私人互联网;
//…构造器。。。
public void makeExpandable(字符串全文,int-maxLines){
mFullText=全文;
mMaxLines=最大线;
ViewTreeObserver vto=getViewTreeObserver();
vto.addOnGlobalLayoutListener(新的ViewTreeObserver.OnGlobalLayoutListener(){
@凌驾
公共图书馆{
ViewTreeObserver obs=getViewTreeObserver();
obs.removeOnGlobalLayoutListener(此);
如果(getLineCount()以下是使用上述一些响应对我有效的方法(我在示例中使用的是ButterKnife):
当用户单击描述时,它将根据最大行折叠或展开。这仅适用于API 16+。
我遇到的问题是,测线计数在点处返回零,测线计数和最大计数在某些点处是相同的值。我为此创建了一个开源库,因为我对在internet上找到的其他解决方案不满意。我已将该产品放在GitHub上,任何人都可以免费使用
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);
}
}
使用ExpandableTextView非常简单,它只是一个常规的TextView,添加了一些额外的功能
/**
* Ellipsize the text when the lines of text exceeds the value provided by {@link #makeExpandable} methods.
* Appends {@link #MORE} or {@link #LESS} as needed.
* TODO: add animation
* Created by vedant on 3/10/15.
*/
public class ExpandableTextView extends TextView {
private static final String TAG = "ExpandableTextView";
private static final String ELLIPSIZE = "... ";
private static final String MORE = "more";
private static final String LESS = "less";
private String mFullText;
private int mMaxLines;
//...constructors...
public void makeExpandable(String fullText, int maxLines) {
mFullText =fullText;
mMaxLines = maxLines;
ViewTreeObserver vto = getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
ViewTreeObserver obs = getViewTreeObserver();
obs.removeOnGlobalLayoutListener(this);
if (getLineCount() <= maxLines) {
setText(mFullText);
} else {
setMovementMethod(LinkMovementMethod.getInstance());
showLess();
}
}
});
}
/**
* truncate text and append a clickable {@link #MORE}
*/
private void showLess() {
int lineEndIndex = getLayout().getLineEnd(mMaxLines - 1);
String newText = mFullText.substring(0, lineEndIndex - (ELLIPSIZE.length() + MORE.length() + 1))
+ ELLIPSIZE + MORE;
SpannableStringBuilder builder = new SpannableStringBuilder(newText);
builder.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
showMore();
}
}, newText.length() - MORE.length(), newText.length(), 0);
setText(builder, BufferType.SPANNABLE);
}
/**
* show full text and append a clickable {@link #LESS}
*/
private void showMore() {
// create a text like subText + ELLIPSIZE + MORE
SpannableStringBuilder builder = new SpannableStringBuilder(mFullText + LESS);
builder.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
showLess();
}
}, builder.length() - LESS.length(), builder.length(), 0);
setText(builder, BufferType.SPANNABLE);
}
}
private static final MAX_LINE_COUNT = 3;
@Bind(R.id.description)
TextView mDescription;
@Override
protected void onCreate(Bundle savedInstanceState) {
if(!TextUtils.isEmpty(mDescription)) {
mDescription.setText(mItem.description);
mDescription.setMaxLines(MAX_LINE_COUNT);
mDescription.setEllipsize(TextUtils.TruncateAt.END);
} else {
mDescription.setVisibility(View.GONE);
}
}
@OnClick(R.id.description)
void collapseExpandTextView(TextView tv) {
if (tv.getMaxLines() == MAX_LINE_COUNT) {
// collapsed - expand it
tv.setEllipsize(null);
tv.setMaxLines(Integer.MAX_VALUE);
} else {
// expanded - collapse it
tv.setEllipsize(TextUtils.TruncateAt.END);
tv.setMaxLines(MAX_LINE_COUNT);
}
ObjectAnimator animation = ObjectAnimator.ofInt(tv, "maxLines", tv.getMaxLines());
animation.setDuration(200).start();
}
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);
}
}
<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>
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");
}
});
private void makeTextViewResizable(final TextView tv, final int maxLine, final String expandText, final boolean viewMore){
try {
if (tv.getTag() == null) {
tv.setTag(tv.getText());
}
//OnGlobalLayoutListener
ViewTreeObserver vto = tv.getViewTreeObserver();
vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
ViewTreeObserver obs = tv.getViewTreeObserver();
// obs.removeGlobalOnLayoutListener((ViewTreeObserver.OnGlobalLayoutListener) mActivity);
obs.removeOnPreDrawListener(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, 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, 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, expandText,
viewMore), TextView.BufferType.SPANNABLE);
}
return true;
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
// initialize integers
int collapsedHeight, expandedHeight;
// get collapsed height after TextView is drawn
textView.post(new Runnable() {
@Override
public void run() {
collapsedHeight = textView.getMeasuredHeight();
}
});
// view that will expand/collapse your TextView
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// number of max lines when collapsed
if (textView.getMaxLines() == 2) {
// expand
textView.setMaxLines(Integer.MAX_VALUE);
textView.measure(View.MeasureSpec.makeMeasureSpec(notifMessage.getMeasuredWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED));
expandedHeight = textView.getMeasuredHeight();
ObjectAnimator animation = ObjectAnimator.ofInt(textView, "height", collapsedHeight, expandedHeight);
animation.setDuration(250).start();
} else {
// collapse
ObjectAnimator animation = ObjectAnimator.ofInt(textView, "height", expandedHeight, collapsedHeight);
animation.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
// number of max lines when collapsed
textView.setMaxLines(2);
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
animation.setDuration(250).start();
}
}
});
package com.example.android.widgets;
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v7.widget.AppCompatTextView;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.util.AttributeSet;
import com.example.android.R;
public class TruncatingTextView extends AppCompatTextView {
public static final String TWO_SPACES = " ";
private int truncateAfter = Integer.MAX_VALUE;
private String suffix;
private RelativeSizeSpan truncateTextSpan = new RelativeSizeSpan(0.75f);
private ForegroundColorSpan viewMoreTextSpan = new ForegroundColorSpan(Color.BLUE);
private static final String MORE_STRING = getContext().getString(R.string.more);
private static final String ELLIPSIS = getContext().getString(R.string.ellipsis);
public TruncatingTextView(Context context) {
super(context);
}
public TruncatingTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TruncatingTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setText(CharSequence fullText, @Nullable CharSequence afterTruncation, int truncateAfterLineCount) {
this.suffix = TWO_SPACES + MORE_STRING;
if (!TextUtils.isEmpty(afterTruncation)) {
suffix += TWO_SPACES + afterTruncation;
}
// Don't call setMaxLines() unless we have to, since it does a redraw.
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();
if (getText().length() <= lastCharToShowOfFullTextAfterTruncation) {
// No idea why this would be the case, but to prevent a crash, here it is. Besides, if this is true, we should be less than our maximum lines and thus good to go.
return;
}
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 + MORE_STRING.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
setText(truncatedSpannableString);
}
}
}
<com.ms.square.android.expandabletextview.ExpandableTextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:expandableTextView="http://schemas.android.com/apk/res-auto"
android:id="@+id/expand_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
expandableTextView:maxCollapsedLines="4"
expandableTextView:animDuration="200">
<TextView
android:id="@id/expandable_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:textSize="16sp"
android:textColor="#666666" />
<ImageButton
android:id="@id/expand_collapse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:layout_gravity="right|bottom"
android:background="@android:color/transparent"/>
</com.ms.square.android.expandabletextview.ExpandableTextView>
TextView expandableTextView = (ExpandableTextView) findViewById(R.id.expand_text_view);
// YourCustomeClass.class [your customized class]
// yourTextView [ TextView yourTextView = findViewById(R.id.yourTextView) ];
YourCustomeClass.doResizeTextView(yourTextView, 3, "More", true);
// 3 - No of lines after user wants to expand it.
// "More" : text want to see end of your TextView after shrink
// True : flag for viewMore
fun toggleReadMoreTextView(linesWhenCollapsed: Float) {
if (viewDataBinding.textView.maxLines != Integer.MAX_VALUE) {
// exapand
viewDataBinding.textView.maxLines = Integer.MAX_VALUE
} else {
// collapse
viewDataBinding.textView.maxLines = linesWhenCollapsed
}
// start animation
TransitionManager.beginDelayedTransition(viewDataBinding.constraintLayout)
}
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
dependencies {
implementation 'com.github.zacharee:Android-ExpandableTextView:Tag'
}
<at.blogc.android.views.ExpandableTextView
...
android:maxLines="10"
/>
if (expandable.isExpanded) {
expandable.collapse()
else {
expandable.expand()
}
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:startColor="#11FFFFFF"
android:centerColor="#33FFFFFF"
android:endColor="#99FFFFFF"
android:angle="270" />
</shape>
<TextView
android:id="@+id/overviewText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:maxLines="3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/textView8"
app:layout_constraintTop_toBottomOf="@+id/textView8" />
<ImageView
android:id="@+id/seeMoreImage"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/background_white"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/overviewText"
app:layout_constraintEnd_toEndOf="@+id/overviewText"
app:layout_constraintStart_toStartOf="@+id/overviewText"
app:srcCompat="@drawable/ic_arrow_down"
tools:ignore="VectorDrawableCompat" />
var isTextViewClicked = true
if (binding.overviewText.lineCount > 3)
binding.seeMoreImage.visibility = View.VISIBLE
binding.seeMoreImage.setOnClickListener {
isTextViewClicked = if(isTextViewClicked){
binding.overviewText.maxLines = Integer.MAX_VALUE
binding.seeMoreImage.setImageResource(R.drawable.ic_arrow_up)
false
} else {
binding.overviewText.maxLines = 3
binding.seeMoreImage.setImageResource(R.drawable.ic_arrow_down)
true
}
}
...
<TextView
android:id="@+id/item_info_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
tools:text="Test long text info\nTest long text info\nTest long text info\nTest long text info | Test long text info | Test long text info"
android:maxLines="@integer/info_collected_lines"
android:fontFamily="@string/font_roboto_regular"
android:textColor="@color/text_second"
android:layout_marginTop="8dp"
android:ellipsize="end"/>
<TextView
android:id="@+id/item_more_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
android:text="@string/see_more"
android:singleLine="true"
android:fontFamily="@string/font_roboto_regular"
android:textColor="@color/text_accent"
android:ellipsize="marquee"/>
...
<color name="text_accent">#0070AA</color>
<color name="text_second">#616161</color>
<string name="font_roboto_regular" translatable="false">sans-serif</string>
<string name="font_roboto_medium" translatable="false">sans-serif-medium</string>
<string name="see_more">Show more</string>
<integer name="club_info_collected_lines">4</integer>
<integer name="club_info_expanded_lines">10</integer>
class ItemHolder(view: View) : RecyclerView.ViewHolder(view) {
...
private val infoText = view.findViewById<TextView>(R.id.item_info_text)
private val moreText = view.findViewById<TextView>(R.id.item_more_text)
fun bind(item: Item, callback: Callback) {
infoText.text = item.info
// This is extension (show code later) need for getting correct [TextView.getLineCount]. Because before draw view it always == 0.
infoText.afterLayoutConfiguration {
val hasEllipsize = infoText.layout.getEllipsisCount(infoText.lineCount - 1) > 0
moreText.visibility = if (hasEllipsize) View.VISIBLE else View.GONE
if (hasEllipsize) {
val maxLines = itemView.context.resources.getInteger(R.integer.club_info_expanded_lines)
moreText.setOnClickListener {
infoText.maxLines = maxLines
it.visibility = View.GONE
}
}
}
...
}
// Call this inside [RecyclerView.Adapter.onViewRecycled] for prevent memory leaks.
fun unbind() {
moreText.setOnClickListener(null)
}
}
/**
* Function for detect when layout completely configure.
*/
fun View.afterLayoutConfiguration(func: () -> Unit) {
viewTreeObserver?.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver?.removeOnGlobalLayoutListener(this)
func()
}
})
}