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

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-收起-查看更多-记录下)