關(guān)于ExpandableTextView幾點(diǎn)優(yōu)化

前一段時(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/ExpandableTextViewChen-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)”按鈕消失;

圖1 APP效果圖展示.png

為何會(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),多多包涵。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容