前一段時(shí)間公司項(xiàng)目需要用到類似于朋友圈效果的折疊和收起功能。
1.點(diǎn)擊翻譯時(shí),全文展開(kāi),并顯示下方翻譯結(jié)果;
2.點(diǎn)擊收起翻譯時(shí),全文收起,翻譯結(jié)果隱藏;
3.item展開(kāi)或收起狀態(tài)需要保存。上網(wǎng)搜索到了Manabu-GT/ExpandableTextView和Chen-Sir/ExpandableTextView,三下五除二快速完成交給測(cè)試,so easy!
但是隨后測(cè)試提交給我的bug卻給我了很大的難題:
1.內(nèi)容足夠長(zhǎng),超出一屏, mCollapsedHeight計(jì)算的有問(wèn)題;
2.當(dāng)顯示文字的View錯(cuò)位的時(shí)候,點(diǎn)擊“收起/展開(kāi)”事件無(wú)效。
3.多次滑動(dòng)列表過(guò)程中,重復(fù)點(diǎn)擊“收起/展開(kāi)”操作時(shí),有時(shí)文字不可見(jiàn),并“收起/展開(kāi)”按鈕消失;

為何會(huì)出現(xiàn)上述情況,首頁(yè)先ExpandableTextView看看有木有解決辦法,但是看了一圈的Issue,上面出現(xiàn)的問(wèn)題依然沒(méi)有得到解決。下面記錄著我是如何解決問(wèn)題和分享問(wèn)題的思路,僅供參考,不一定適用于所用項(xiàng)目。
一、內(nèi)容足夠長(zhǎng),超出一屏, mCollapsedHeight為0的解決方法
從下面的ExpandableTextView可以看出折疊高度在OnMeasure獲取,當(dāng)點(diǎn)擊“收起/展開(kāi)”按鈕時(shí),將高度賦給View,目前按程序代碼上看沒(méi)有什么大的問(wèn)題。那么就只能從Debug出手,在Debug跟蹤過(guò)程中,我們發(fā)現(xiàn)在點(diǎn)擊“收起”時(shí),mCollapsedHeight高度為0。明明我們存儲(chǔ)高度,為何高度為0呢?Why??
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!mRelayout || getVisibility() == View.GONE) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
mRelayout = false;
...
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTv.getLineCount() <= mMaxCollapsedLines) {
return;
}
...
// Re-measure with new setup
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mCollapsed) {
...
mCollapsedHeight = getMeasuredHeight();
if (mListener != null) {
mListener.onCollapsedHeight(mCollapsedHeight);
}
}
}
@Override
public void onClick(View view) {
...
Animation animation;
if (mCollapsed) {
animation = new ExpandCollapseAnimation(this, getHeight(), mCollapsedHeight);
} else {
animation = new ExpandCollapseAnimation(this, getHeight(), getHeight() +
mTextHeightWithMaxLines - mTv.getHeight());
}
...
}
class ExpandCollapseAnimation extends Animation {
private final View mTargetView;
private final int mStartHeight;
private final int mEndHeight;
public ExpandCollapseAnimation(View view, int startHeight, int endHeight) {
mTargetView = view;
mStartHeight = startHeight;
mEndHeight = endHeight;
setDuration(mAnimationDuration);
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final int newHeight = (int)((mEndHeight - mStartHeight) * interpolatedTime + mStartHeight);
mTv.setMaxHeight(newHeight - mMarginBetweenTxtAndBottom);
if (Float.compare(mAnimAlphaStart, 1.0f) != 0) {
applyAlphaAnimation(mTv, mAnimAlphaStart + interpolatedTime * (1.0f - mAnimAlphaStart));
}
mTargetView.getLayoutParams().height = newHeight;
mTargetView.requestLayout();
}
@Override
public void initialize( int width, int height, int parentWidth, int parentHeight ) {
super.initialize(width, height, parentWidth, parentHeight);
}
@Override
public boolean willChangeBounds( ) {
return true;
}
}
這時(shí)我們要冷靜下來(lái),先分析一波,首先我用的RecyclerView,ExpandableTextView放在item中,這會(huì)不會(huì)是View錯(cuò)位而引發(fā)的問(wèn)題呢?果然Debug中,我查到當(dāng)前View的已不是同一個(gè)。這時(shí)我做了分析,首先ExpandableTextView在OnMeasure拿到View的高度是折疊時(shí)的高度,當(dāng)多次RecyclerView列表后,點(diǎn)擊“收起”按鈕,我們應(yīng)該將高度賦值進(jìn)ExpandableTextView,根據(jù)產(chǎn)品的需求特性,我們對(duì)代碼進(jìn)行如下修改(PS:各位可以根據(jù)自己項(xiàng)目實(shí)況,做相應(yīng)的修改):
1.將測(cè)量之后的高度放到監(jiān)聽(tīng)事件中
2.在Adapter中將監(jiān)聽(tīng)事件的高度賦值給全局變量;
3.在RecyclerView滑動(dòng)時(shí),會(huì)重新執(zhí)行onBindViewHolder方法,此時(shí)將高度傳入ExpandableTextView中
ExpandableTextView源碼中
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
if (mCollapsed) {
...
if (mListener != null) {
mListener.onCollapsedHeight(mCollapsedHeight);
}
}
}
public void setmCollapsedHeight(int mCollapsedHeight) {
this.mCollapsedHeight = mCollapsedHeight;
}
public interface OnExpandStateChangeListener {
void onExpandStateChanged(TextView textView, boolean isExpanded);
void onCollapsedHeight(int mCollapsedHeight);
}
RecyclerView中Adapter的部分代碼
public class FeedAllRvAdapter extends RecyclerView.Adapter<FeedAllRvAdapter.FeedViewHolder> implements Const {
private int mCollapsedHeight = -1;
...
@Override
public FeedViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
...
if (onFeedAllAdapterListener != null) {
holder.tvContent.setOnExpandStateChangeListener(new ExpandableTextView.OnExpandStateChangeListener() {
@Override
public void onExpandStateChanged(TextView textView, boolean isExpanded) {
lists.get(holder.position).setmCollapsedStatus(!isExpanded);
onFeedAllAdapterListener.toMixpanelTrack(isExpanded);
}
@Override
public void onCollapsedHeight(int mCollapsedHeight) {
FeedAllRvAdapter.this.mCollapsedHeight = mCollapsedHeight;
}
});
}
return holder;
}
@Override
public void onBindViewHolder(final FeedViewHolder holder, int position) {
...
if (mCollapsedHeight != -1) {
holder.tvContent.setmCollapsedHeight(mCollapsedHeight);
}
...
}
}
二、當(dāng)顯示文字的View錯(cuò)位的時(shí)候,點(diǎn)擊“收起/展開(kāi)”事件無(wú)效
經(jīng)過(guò)上面的代碼修改,超過(guò)一屏之長(zhǎng)問(wèn)題得到解決,但是多次進(jìn)行滑動(dòng)和“收起/展開(kāi)”的操作時(shí),偶現(xiàn)當(dāng)View中文字錯(cuò)位時(shí),“展開(kāi)/收起”的點(diǎn)擊事件無(wú)效,重新下拉刷新列表,仍然點(diǎn)擊事件無(wú)效。
@Override
public void onClick(View view) {
...
mAnimating = true;
...
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
applyAlphaAnimation(mTv, mAnimAlphaStart);
}
@Override
public void onAnimationEnd(Animation animation) {
clearAnimation();
mAnimating = false;
...
}
@Override
public void onAnimationRepeat(Animation animation) { }
});
...
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mAnimating;
}
從上面代碼中我們看出當(dāng)前View是否響應(yīng)事件,是onInterceptTouchEvent的狀態(tài)決定的。于是我Debug調(diào)試,發(fā)現(xiàn)出現(xiàn)改情況是mAnimating狀態(tài)總是為false,那么我們就知道問(wèn)了,是動(dòng)畫結(jié)束的監(jiān)聽(tīng)沒(méi)有執(zhí)行。
onInterceptTouchEvent()是用于處理事件(類似于預(yù)處理,當(dāng)然也可以不處理)并改變事件的傳遞方向,也就是決定是否允許Touch事件繼續(xù)向下(子控件)傳遞,一但返回True(代表事件在當(dāng)前的viewGroup中會(huì)被處理),則向下傳遞之路被截?cái)啵ㄋ凶涌丶](méi)有機(jī)會(huì)參與Touch事件),同時(shí)把事件傳遞給當(dāng)前的控件的onTouchEvent()處理;返回false,則把事件交給子控件的onInterceptTouchEvent(),因此我們?nèi)ゲ榭磎Animating狀態(tài)的變化。
于是度娘發(fā)現(xiàn)一個(gè)比較有說(shuō)服力的理由。
動(dòng)畫播放完畢之后給我們的回調(diào)onAnimationEnd函數(shù)里面可能系統(tǒng)有一些邏輯沒(méi)有執(zhí)行,我們就執(zhí)行了清除動(dòng)畫等操作,沒(méi)有給系統(tǒng)留出一定的時(shí)間去處理。
在ExpandableTextView中Issue也有人提出過(guò)可能是動(dòng)畫問(wèn)題,于是我用ObjectAnimator動(dòng)畫來(lái)替換該動(dòng)畫
@Override
public void onClick(View view) {
if (mStateTv.getVisibility() != View.VISIBLE) {
return;
}
mCollapsed = !mCollapsed;
mStateTv.setText(mCollapsed ? mExpandString : mCollapsedString);
mAnimating = true;
ObjectAnimator animator3 = ObjectAnimator.ofFloat(this.view, "alpha", 1f, 0f);//變淡
final AnimatorSet set = new AnimatorSet();
set.playTogether(animator3);
set.setDuration(mAnimationDuration).start();
animator3.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int mStartHeight = getHeight();
int mEndHeight;
if (mCollapsed) {
mEndHeight = mCollapsedHeight;
} else {
mEndHeight = getHeight() + mTextHeightWithMaxLines - mTv.getHeight();
}
final int newHeight = (int) ((mEndHeight - mStartHeight) * animation.getAnimatedFraction() + mStartHeight);
mTv.setMaxHeight(newHeight - mMarginBetweenTxtAndBottom);
ExpandableTextView.this.getLayoutParams().height = newHeight;
ExpandableTextView.this.requestLayout();
}
});
set.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
clearAnimation();
mAnimating = false;
if (mListener != null) {
mListener.onExpandStateChanged(mTv, !mCollapsed);
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
三、多次點(diǎn)擊“收起/展開(kāi)”按鈕,偶現(xiàn)文字消失的情況
我們知道在ListView、RecyclerView等控件中,每個(gè)Item是與數(shù)據(jù)進(jìn)行一對(duì)一的綁定,那么現(xiàn)在就好辦了。將展開(kāi)是否展開(kāi)和收起的狀態(tài)放在實(shí)體類中,并與上面獲取高度的方法一起用,能夠達(dá)到效果。RecyclerView滑動(dòng)時(shí),onBindView將該狀態(tài)賦值。同時(shí)也可解決Recyclerview加載更多同時(shí)展開(kāi)全文,而引起的空白問(wèn)題。
Bean實(shí)體類中的字段我就在不在描述了。
ExpandableTextView修改如下
public void setText(@Nullable SpannableStringBuilder originContent,
boolean isCollapsed) {
clearAnimation();
mCollapsed = isCollapsed;
mStateTv.setText(mCollapsed ? mExpandString : mCollapsedString);
setText(originContent);
getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
requestLayout();
}
public void setText(@Nullable SpannableStringBuilder originContent) {
mRelayout = true;
// mTv.setText(text);
mTv.setText(originContent);
setVisibility(TextUtils.isEmpty(originContent) ? View.GONE : View.VISIBLE);
mTv.setMovementMethod(LinkMovementClickMethod.getInstance());
}
Adapter中修改
@Override
public void onBindViewHolder(final FeedViewHolder holder, int position) {
if (mCollapsedHeight != -1) {
holder.tvContent.setmCollapsedHeight(mCollapsedHeight);
}
holder.tvContent.setText(feedBean.getShowContent(), feedBean.ismCollapsedStatus());
}
因?yàn)楣ぷ髦杏龅降倪@些問(wèn)題,真的很棘手,多虧了顧爺和小秦的幫助,才讓我趕在上線之前完成開(kāi)發(fā)。這也督促需要多多學(xué)習(xí),這也是增強(qiáng)了我開(kāi)始寫博客記錄自己工作中遇到問(wèn)題及如何解決問(wèn)題的決心,感謝他們。最后由于本人第一次寫博客,技術(shù)點(diǎn)角度敘述力度可能不夠,如有問(wèn)題,還請(qǐng)多多指點(diǎn),多多包涵。