关于ExpandableTextView几点优化

前一段时间公司项目需要用到类似于朋友圈效果的折叠和收起功能。具体功能如下:1.点击翻译时,全文展开,并显示下方翻译结果;2.点击收起翻译时,全文收起,翻译结果隐藏;3.item展开或收起状态需要保存。上网搜索到了Manabu-GT/ExpandableTextView和Chen-Sir/ExpandableTextView,三下五除二快速完成交给测试,简直so easy!

但是随后测试提交给我的bug却给我了很大的难题:

1.内容足够长,超出一屏, mCollapsedHeight计算的有问题;
2.当显示文字的View错位的时候,点击“收起/展开”事件无效。
3.多次滑动列表过程中,重复点击“收起/展开”操作时,有时文字不可见,并“收起/展开”按钮消失;

关于ExpandableTextView几点优化_第1张图片

为何会出现上述情况,首页先ExpandableTextView看看有木有解决办法,但是看了一圈的Issue,上面出现的问题依然没有得到解决。下面记录着我是如何解决问题和分享问题的思路,仅供参考,不一定适用于所用项目。

一、内容足够长,超出一屏, mCollapsedHeight为0的解决方法

从下面的ExpandableTextView可以看出折叠高度在OnMeasure获取,当点击“收起/展开”按钮时,将高度赋给View,目前按程序代码上看没有什么大的问题。那么就只能从Debug出手,在Debug跟踪过程中,我们发现在点击“收起”时,mCollapsedHeight高度为0。明明我们存储高度,为何高度为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;
        }
    }

这时我们要冷静下来,先分析一波,首先我用的RecyclerView,ExpandableTextView放在item中,这会不会是View错位而引发的问题呢?果然Debug中,我查到当前View的已不是同一个。这时我做了分析,首先ExpandableTextView在OnMeasure拿到View的高度是折叠时的高度,当多次RecyclerView列表后,点击“收起”按钮,我们应该将高度赋值进ExpandableTextView,根据产品的需求特性,我们对代码进行如下修改(PS:各位可以根据自己项目实况,做相应的修改):

1.将测量之后的高度放到监听事件中
2.在Adapter中将监听事件的高度赋值给全局变量;
3.在RecyclerView滑动时,会重新执行onBindViewHolder方法,此时将高度传入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);
        }
        ...
    }
}

二、当显示文字的View错位的时候,点击“收起/展开”事件无效

经过上面的代码修改,超过一屏之长问题得到解决,但是多次进行滑动和“收起/展开”的操作时,偶现当View中文字错位时,“展开/收起”的点击事件无效,重新下拉刷新列表,仍然点击事件无效。

    @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;
    }

从上面代码中我们看出当前View是否响应事件,是onInterceptTouchEvent的状态决定的。于是我Debug调试,发现出现改情况是mAnimating状态总是为false,那么我们就知道问了,是动画结束的监听没有执行。

onInterceptTouchEvent()是用于处理事件(类似于预处理,当然也可以不处理)并改变事件的传递方向,也就是决定是否允许Touch事件继续向下(子控件)传递,一但返回True(代表事件在当前的viewGroup中会被处理),则向下传递之路被截断(所有子控件将没有机会参与Touch事件),同时把事件传递给当前的控件的onTouchEvent()处理;返回false,则把事件交给子控件的onInterceptTouchEvent(),因此我们去查看mAnimating状态的变化。

于是度娘发现一个比较有说服力的理由。

动画播放完毕之后给我们的回调onAnimationEnd函数里面可能系统有一些逻辑没有执行,我们就执行了清除动画等操作,没有给系统留出一定的时间去处理。

在ExpandableTextView中Issue也有人提出过可能是动画问题,于是我用ObjectAnimator动画来替换该动画

@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) {
            }
        });
    }

###三、多次点击“收起/展开”按钮,偶现文字消失的情况
我们知道在ListView、RecyclerView等控件中,每个Item是与数据进行一对一的绑定,那么现在就好办了。将展开是否展开和收起的状态放在实体类中,并与上面获取高度的方法一起用,能够达到效果。RecyclerView滑动时,onBindView将该状态赋值。同时也可解决Recyclerview加载更多同时展开全文,而引起的空白问题。
Bean实体类中的字段我就在不在描述了。

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());
    }

因为工作中遇到的这些问题,真的很棘手,多亏了顾爷和小秦的帮助,才让我赶在上线之前完成开发。这也督促需要多多学习,这也是增强了我开始写博客记录自己工作中遇到问题及如何解决问题的决心,感谢他们。最后由于本人第一次写博客,如有问题,还请多多指点,多多包涵。

你可能感兴趣的:(Android)