美文网首页
android-收起-查看更多-记录下

android-收起-查看更多-记录下

作者: 逆光_初见 | 来源:发表于2020-06-09 11:06 被阅读0次

    package com.i23app.app23.widget;

    import android.content.Context;

    import android.content.res.TypedArray;

    import android.graphics.Canvas;

    import android.graphics.Color;

    import android.graphics.Typeface;

    import android.graphics.drawable.Drawable;

    import android.text.Layout;

    import android.text.SpannableString;

    import android.text.Spanned;

    import android.text.StaticLayout;

    import android.text.TextPaint;

    import android.text.TextUtils;

    import android.text.method.LinkMovementMethod;

    import android.text.style.ClickableSpan;

    import android.text.style.ImageSpan;

    import android.util.AttributeSet;

    import android.util.Log;

    import android.view.View;

    import com.i23app.app23.R;

    /**

    * 创建者    michael

    * 创建时间  2020/5/14

    * 描述

    *

    * 更新者    $

    * 更新时间  $

    * 更新描述

    */

    public class FolderTextViewextends androidx.appcompat.widget.AppCompatTextView {

    // TAG

        private static final StringTAG ="FolderTextView";

        // 默认打点文字

        private static final StringDEFAULT_ELLIPSIZE ="...";

        // 默认收起文字

        private static final StringDEFAULT_FOLD_TEXT ="收起";

        // 默认展开文字

        private static final StringDEFAULT_UNFOLD_TEXT ="更多";

        // 默认固定行数

        private static final int DEFAULT_FOLD_LINE =2;

        // 默认收起和展开文字颜色

        private static final int DEFAULT_TAIL_TEXT_COLOR = Color.BLACK;

        // 默认是否可以再次收起

        private static final boolean DEFAULT_CAN_FOLD_AGAIN =true;

        // 是否粗体

        private static final boolean DEFAULT_IS_FACE =false;

        // 收起文字

        private StringmFoldText;

        // 展开文字

        private StringmUnFoldText;

        // 固定行数

        private int mFoldLine;

        // 尾部文字颜色

        private int mTailColor;

        // 是否可以再次收起

        private boolean mCanFoldAgain =false;

        // 是否粗体

        private boolean mIsBold =false;

        // 收缩状态

        private boolean mIsFold =false;

        // 绘制,防止重复进行绘制

        private boolean mHasDrawn =false;

        // 内部绘制

        private boolean mIsInner =false;

        // 全文本

        private StringmFullText;

        // 行间距倍数

        private float mLineSpacingMultiplier =1.0f;

        // 行间距额外像素

        private float mLineSpacingExtra =0.0f;

        // 统计使用二分法裁剪源文本的次数

        private int mCountBinary =0;

        // 统计使用备用方法裁剪源文本的次数

        private int mCountBackUp =0;

        // 统计onDraw调用的次数

        private int mCountOnDraw =0;

        // 点击处理

        private ClickableSpanclickSpan =new ClickableSpan() {

    @Override

            public void onClick(View widget) {

    mIsFold = !mIsFold;

                mHasDrawn =false;

                invalidate();

            }

    @Override

            public void updateDrawState(TextPaint ds) {

    if (mIsBold) {

    ds.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));

                }else {

    ds.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL));

                }

    ds.setColor(mTailColor);

            }

    };

        public abstract class ClickableImageSpanextends ImageSpan {

    public ClickableImageSpan(Drawable b) {

    super(b);

            }

    public abstract void onClick(View view);

        }

    /**

    * 构造

    *

        * @param context 上下文

    */

        public FolderTextView(Context context) {

    this(context, null);

        }

    /**

    * 构造

    *

        * @param context 上下文

        * @param attrs  属性

    */

        public FolderTextView(Context context, AttributeSet attrs) {

    this(context, attrs, 0);

        }

    /**

    * 构造

    *

        * @param context      上下文

        * @param attrs        属性

        * @param defStyleAttr 样式

    */

        public FolderTextView(Context context, AttributeSet attrs, int defStyleAttr) {

    super(context, attrs, defStyleAttr);

            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FolderTextView);

            mFoldText = a.getString(R.styleable.FolderTextView_foldText);

            if (null ==mFoldText) {

    mFoldText =DEFAULT_FOLD_TEXT;

            }

    mUnFoldText = a.getString(R.styleable.FolderTextView_unFoldText);

            if (null ==mUnFoldText) {

    mUnFoldText =DEFAULT_UNFOLD_TEXT;

            }

    mFoldLine = a.getInt(R.styleable.FolderTextView_foldLine, DEFAULT_FOLD_LINE);

            if (mFoldLine <1) {

    throw new RuntimeException("foldLine must not less than 1");

            }

    mTailColor = a.getColor(R.styleable.FolderTextView_tailTextColor, DEFAULT_TAIL_TEXT_COLOR);

            mCanFoldAgain = a.getBoolean(R.styleable.FolderTextView_canFoldAgain, DEFAULT_CAN_FOLD_AGAIN);

            mIsBold = a.getBoolean(R.styleable.FolderTextView_typeface, DEFAULT_IS_FACE);

            a.recycle();

        }

    @Override

        public void setText(CharSequence text, BufferType type) {

    if (TextUtils.isEmpty(mFullText) || !mIsInner) {

    mHasDrawn =false;

                mFullText = String.valueOf(text);

            }

    super.setText(text, type);

        }

    @Override

        public void setLineSpacing(float extra, float multiplier) {

    mLineSpacingExtra = extra;

            mLineSpacingMultiplier = multiplier;

            super.setLineSpacing(extra, multiplier);

        }

    @Override

        public void invalidate() {

    super.invalidate();

        }

    @Override

        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

            // 必须解释下:由于为了得到实际一行的宽度(makeTextLayout()中需要使用),必须要先把源文本设置上,然后再裁剪至指定行数;

    // 这就导致了该TextView会先布局一次高度很高(源文本行数高度)的布局,裁剪后再次布局成指定行数高度,因而下方View会抖动;

    // 这里的处理是,super.onMeasure()已经计算出了源文本的实际宽高了,取出指定行数的文本再次测量一下其高度,

    // 然后把这个高度设置成最终的高度就行了!

            if (!mIsFold) {

    Layout layout = getLayout();

                int line = getFoldLine();

                if (line < layout.getLineCount()) {

    int index = layout.getLineEnd(line -1);

                    if (index >0) {

    // 得到一个字符串,该字符串恰好占据mFoldLine行数的高度

                        String strWhichHasExactlyFoldLine = getText().subSequence(0, index).toString();

                        Log.d(TAG, "strWhichHasExactlyFoldLine-->" + strWhichHasExactlyFoldLine);

                        layout = makeTextLayout(strWhichHasExactlyFoldLine);

                        // 把这个高度设置成最终的高度,这样下方View就不会抖动了

                        setMeasuredDimension(getMeasuredWidth(), layout.getHeight() + getPaddingTop() + getPaddingBottom());

                    }

    }

    }

    }

    @Override

        protected void onDraw(Canvas canvas) {

    Log.d(TAG, "onDraw() " +mCountOnDraw++ +", getMeasuredHeight() " + getMeasuredHeight());

            if (!mHasDrawn) {

    resetText();

            }

    super.onDraw(canvas);

            mHasDrawn =true;

            mIsInner =false;

        }

    /**

    * 获取折叠文字

    *

        * @return 折叠文字

    */

        public StringgetFoldText() {

    return mFoldText;

        }

    /**

    * 设置折叠文字

    *

        * @param foldText 折叠文字

    */

        public void setFoldText(String foldText) {

    mFoldText = foldText;

            invalidate();

        }

    /**

    * 获取展开文字

    *

        * @return 展开文字

    */

        public StringgetUnFoldText() {

    return mUnFoldText;

        }

    /**

    * 设置展开文字

    *

        * @param unFoldText 展开文字

    */

        public void setUnFoldText(String unFoldText) {

    mUnFoldText = unFoldText;

            invalidate();

        }

    /**

    * 获取折叠行数

    *

        * @return 折叠行数

    */

        public int getFoldLine() {

    return mFoldLine;

        }

    /**

    * 设置折叠行数

    *

        * @param foldLine 折叠行数

    */

        public void setFoldLine(int foldLine) {

    mFoldLine = foldLine;

            invalidate();

        }

    /**

    * 获取尾部文字颜色

    *

        * @return 尾部文字颜色

    */

        public int getTailColor() {

    return mTailColor;

        }

    /**

    * 设置尾部文字颜色

    *

        * @param tailColor 尾部文字颜色

    */

        public void setTailColor(int tailColor) {

    mTailColor = tailColor;

            invalidate();

        }

    /**

    * 获取是否可以再次折叠

    *

        * @return 是否可以再次折叠

    */

        public boolean isCanFoldAgain() {

    return mCanFoldAgain;

        }

    /**

    * 获取全文本

    *

        * @return 全文本

    */

        public StringgetFullText() {

    return mFullText;

        }

    /**

    * 设置是否可以再次折叠

    *

        * @param canFoldAgain 是否可以再次折叠

    */

        public void setCanFoldAgain(boolean canFoldAgain) {

    mCanFoldAgain = canFoldAgain;

            invalidate();

        }

    public void setTypeFace(boolean isBold) {

    mIsBold = isBold;

            invalidate();

        }

    /**

    * 获取TextView的Layout,注意这里使用getWidth()得到宽度

    *

        * @param text 源文本

        * @return Layout

    */

        private LayoutmakeTextLayout(String text) {

    return new StaticLayout(text, getPaint(), getWidth() - getPaddingLeft() - getPaddingRight(), Layout.Alignment

    .ALIGN_NORMAL, mLineSpacingMultiplier, mLineSpacingExtra, true);

        }

    /**

    * 重置文字

    */

        private void resetText() {

    // 文字本身就小于固定行数的话,不添加尾部的收起/展开文字

            Layout layout = makeTextLayout(mFullText);

            if (layout.getLineCount() <= getFoldLine()) {

    setText(mFullText);

    return;

            }

    SpannableString spanStr =new SpannableString(mFullText);

            if (mIsFold) {

    // 收缩状态

                if (mCanFoldAgain) {

    spanStr = createUnFoldSpan(mFullText);

                }

    }else {

    // 展开状态

                spanStr = createFoldSpan(mFullText);

            }

    updateText(spanStr);

            setMovementMethod(LinkMovementMethod.getInstance());

        }

    /**

    * 不更新全文本下,进行展开和收缩操作

    *

        * @param text 源文本

    */

        private void updateText(CharSequence text) {

    mIsInner =true;

            setText(text);

        }

    /**

    * 创建展开状态下的Span

    *

        * @param text 源文本

        * @return 展开状态下的Span

    */

        private SpannableStringcreateUnFoldSpan(String text) {

    String destStr = text +mFoldText;

            int start = destStr.length() -mFoldText.length();

            int end = destStr.length();

            SpannableString spanStr =new SpannableString(destStr);

            //创建ImageSpan对象

            Drawable rightDrawable = getResources().getDrawable(R.mipmap.ic_triangle_up);

            rightDrawable.setBounds(0, 0, rightDrawable.getMinimumWidth(), rightDrawable.getMinimumHeight()); //设置边界

            ClickableImageSpan imageSpan =new ClickableImageSpan(rightDrawable){

    @Override

                public void onClick(View widget) {

    mIsFold = !mIsFold;

                    mHasDrawn =false;

                    invalidate();

                }

    @Override

                public void updateDrawState(TextPaint ds) {

    if (mIsBold) {

    ds.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));

                    }else {

    ds.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL));

                    }

    ds.setColor(mTailColor);

                }

    };

            spanStr.setSpan(clickSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

            return spanStr;

        }

    /**

    * 创建收缩状态下的Span

    *

        * @param text

        * @return 收缩状态下的Span

    */

        private SpannableStringcreateFoldSpan(String text) {

    long startTime = System.currentTimeMillis();

            String destStr = tailorText(text);

            Log.d(TAG, (System.currentTimeMillis() - startTime) +"ms");

            int start = destStr.length() -mUnFoldText.length();

            int end = destStr.length();

            SpannableString spanStr =new SpannableString(destStr);

            spanStr.setSpan(clickSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

            return spanStr;

        }

    /**

    * 裁剪文本至固定行数(备用方法)

    *

        * @param text 源文本

        * @return 裁剪后的文本

    */

        private StringtailorTextBackUp(String text) {

    Log.d(TAG, "使用备用方法: tailorTextBackUp() " +mCountBackUp++);

            String destStr = text +DEFAULT_ELLIPSIZE +mUnFoldText;

            Layout layout = makeTextLayout(destStr);

            // 如果行数大于固定行数

            if (layout.getLineCount() > getFoldLine()) {

    int index = layout.getLineEnd(getFoldLine() -1);

                if (text.length() < index) {

    index = text.length();

                }

    // 从最后一位逐渐试错至固定行数(可以考虑用二分法改进)

                if (index <=1) {

    return DEFAULT_ELLIPSIZE +mUnFoldText;

                }

    String subText = text.substring(0, index -1);

                return tailorText(subText);

            }else {

    return destStr;

            }

    }

    /**

    * 裁剪文本至固定行数(二分法)。经试验,在文字长度不是很长时,效率比备用方法高不少;当文字长度过长时,备用方法则优势明显。

    *

        * @param text 源文本

        * @return 裁剪后的文本

    */

        private StringtailorText(String text) {

    // return tailorTextBackUp(text);

            int start =0;

            int end = text.length() -1;

            int mid = (start + end) /2;

            int find = finPos(text, mid);

            while (find !=0 && end > start) {

    Log.d(TAG, "使用二分法: tailorText() " +mCountBinary++);

                if (find >0) {

    end = mid -1;

                }else if (find <0) {

    start = mid +1;

                }

    mid = (start + end) /2;

                find = finPos(text, mid);

            }

    Log.d(TAG, "mid is: " + mid);

            String ret;

            if (find ==0) {

    ret = text.substring(0, mid) +DEFAULT_ELLIPSIZE +mUnFoldText;

            }else {

    ret = tailorTextBackUp(text);

            }

    return ret;

        }

    /**

    * 查找一个位置P,到P时为mFoldLine这么多行,加上一个字符‘A’后则刚好为mFoldLine+1这么多行

    *

        * @param text 源文本

        * @param pos  位置

        * @return 查找结果

    */

        private int finPos(String text, int pos) {

    String destStr = text.substring(0, pos) +DEFAULT_ELLIPSIZE +mUnFoldText;

            Layout layout = makeTextLayout(destStr);

            Layout layoutMore = makeTextLayout(destStr +"A");

            int lineCount = layout.getLineCount();

            int lineCountMore = layoutMore.getLineCount();

            if (lineCount == getFoldLine() && (lineCountMore == getFoldLine() +1)) {

    // 行数刚好到折叠行数

                return 0;

            }else if (lineCount > getFoldLine()) {

    // 行数比折叠行数多

                return 1;

            }else {

    // 行数比折叠行数少

                return -1;

            }

    }

    }

    相关文章

      网友评论

          本文标题:android-收起-查看更多-记录下

          本文链接:https://www.haomeiwen.com/subject/azhstktx.html