天天看點

安卓開發SpannableStringBuilder展示富文本

在安卓開發中,如果你們的項目需要展示文本,然後文本裡面嵌套着圖檔,并且展示的文字有些字需要标記成不同的顔色,文字還需要有點選事件。如果讓你按之前的思路去實作這樣的一個效果,你會這樣來設計實作思路:

1)文字用TextView展示,圖檔用ImageView展示,然後文字需要被截取,根據背景傳回的文字索引腳标。
  2)截取的文字會和圖檔連結進行組合布局進行展示,這樣的問題就會是文字和圖檔的布局是不固定的,會根據背景傳回的文字和圖檔不同而展示不同。
  3)文字如果需要添加點選事件,需要重新設計布局添加TextView控件
           

從上面的實作思路我們可以發現,如果按之前的TextView和ImageView組合的方式進行展示的話,就會非常麻煩。

那麼我們就可以使用SpannableStringBuilder這個對象類來實作上面的需求,其原理就是把文本轉成Html網頁來進行展示。

安卓開發SpannableStringBuilder展示富文本

從上圖我們可以發現這樣設定沒有一點的問題,選中的文字會展示不同的背景顔色,但是再點選顔色的時候會綠色的文字間距背景顔色。之前一直覺得是文本轉成Html之後導緻的Html文本自帶背景顔色導緻的,後面才發現文本轉成Html文本:

Html.fromHtml(content,Html.FROM_HTML_MODE_LEGACY, mURLImageGetter, null);
           

傳回一個Spanned對象,

安卓開發SpannableStringBuilder展示富文本

富文本轉成Html網頁顯示之後,預設點選事件是需要高亮顯示的,但是這是TextView裡面設定的方法:

/**
     * Sets the color used to display the selection highlight.
     *
     * @attr ref android.R.styleable#TextView_textColorHighlight
     */
    @android.view.RemotableViewMethod
    public void setHighlightColor(@ColorInt int color) {
        if (mHighlightColor != color) {
            mHighlightColor = color;
            invalidate();
        }
    }
           

是以,我們想要去掉我們選中的Html文本的點選背景顔色,就需要設定這個高亮顯示的顔色設定成白色就行了。

setHighlightColor(Color.parseColor("#00000000"));
           

富文本經常還需要設定選中文字的功能,下面就推薦一個選中文字改變背景顔色的開源架構。

下面是安卓開發,把文本直接轉成富文本的工具類:

/**
 * 文章詳情-富文本控件
 * @author guotianhui
 */

public class FenJRichTextView extends AppCompatTextView{

    private Spannable mSpannable;
    private ClickableSpan clickableSpan;
    private URLImageGetter mURLImageGetter;//加載圖檔使用的,處理html中![在這裡插入圖檔描述]()的處理器,生成Drawable對象并傳回
    private ColorUnderlineSpan mUnderlineSpan;
    private ForegroundColorSpan mFCTxtContentSpan; //文字顔色
    private FenJRichTextViewHelper mFenJRichTextViewHelper;
    private SpannableStringBuilder mSpannableStringBuilder;
    private OnRichContentClickListener mRichContentClickListener;//圖檔點選回調


    public FenJRichTextView(Context context) {
        this(context, null);
    }

    public FenJRichTextView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FenJRichTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mURLImageGetter = new URLImageGetter(this);
        mFenJRichTextViewHelper = new FenJRichTextViewHelper();
    }

    /**
     * 設定富文本内容
     * @param content 文章内容(包含img标簽)
     * @param markSpans 馬克筆清單
     */
    public void setRichContent(String content, List<LevelAnalysisBean> analysisBeans, List<MarkerPenBean> markSpans) {
        try {
            //顯示帶圖檔的html的處理方法,第二個參數imageGetter是加載圖檔使用的,第三個參數是過濾标簽使用的
            Spanned spanned = null;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                spanned = Html.fromHtml(content,Html.FROM_HTML_MODE_LEGACY, mURLImageGetter, null);
            }else {
                spanned = Html.fromHtml(content,mURLImageGetter, null);
            }
            if (spanned instanceof SpannableStringBuilder) {
                mSpannableStringBuilder = (SpannableStringBuilder) spanned;
            } else {
                mSpannableStringBuilder = new SpannableStringBuilder(spanned);
            }
            mSpannable = null;
            setMarkPen(markSpans);
            setImageClickable();
            setHighlightColor(Color.parseColor("#00000000"));
            setTextClickableAnalysis(analysisBeans);
            setMovementMethod(LinkMovementMethod.getInstance());
            super.setText(mSpannableStringBuilder);

        }catch (Exception e){
            LogUtils.e(">>>>>>>>>>>>","設定文章内容出現異常:"+e);
        }
    }

    /**
     * 設定馬克筆
     * @param markSpans
     */
    public void setMarkPen(List<MarkerPenBean> markSpans) {
        setMarkPenBgColor(markSpans);
        //增加馬克筆點選事件
        setTextClickable(markSpans);
    }

    /**
     * 設定馬克筆的背景色
     * @param markSpans 馬克筆清單
     */
    private void setMarkPenBgColor(List<MarkerPenBean> markSpans) {
        try {
            if (ObjectUtils.isNotEmpty(markSpans)) {
                for (MarkerPenBean articleDetailMark : markSpans) {
                    int color;
                    if (TextUtils.isEmpty(articleDetailMark.getMarkColor())) {
                        //容錯,顔色傳回空使用預設顔色
                        color = Color.parseColor("#fbfab7");
                    } else {
                        color = Color.parseColor(articleDetailMark.getMarkColor());
                    }
                    int startPosition = articleDetailMark.getMarkStartPosition();
                    int endPosition = articleDetailMark.getMarkEndPosition();
                    int lg = mSpannableStringBuilder.length();
                    if (endPosition > 0 && lg >= endPosition) {
                        updateTxtBgColor(color, startPosition, endPosition);
                    }
                }
            }
        }catch (Exception e){
            LogUtils.e(">>>>>>>>>>>>>>>>","設定馬克筆背景色異常:"+e);
        }
    }

    /**
     * 設定文字背景色
     * @param color
     * @param startPosition
     * @param endPosition
     */
    private void updateTxtBgColor(int color, int startPosition, int endPosition) {
        try {
            int contentLength = mSpannableStringBuilder.length();
            if (endPosition > contentLength) {
                endPosition = contentLength;
            }
            Log.e(">>>>>>>>>>>>>>>>","設定文字背景色color:"+color);
            mSpannableStringBuilder.setSpan(new BackgroundColorSpan(color), startPosition, endPosition,
                    Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
        }catch (Exception e){
            LogUtils.e(">>>>>>>>>>>>","設定文字背景色數組腳标越界:"+e);
        }
    }

    /**
     * 設定文字顔色
     * @param color 顔色
     * @param startPosition 開始位置(包含)
     * @param endPosition 結束位置(不包含)
     */
    public void updateTxtColor(int color, int startPosition, int endPosition) {
        try {
            if (mSpannable == null) {
                super.setText(getText(), BufferType.SPANNABLE);
                CharSequence charSequence = getText();
                if (charSequence instanceof Spannable) {
                    mSpannable = (Spannable) charSequence;
                }
            }
            if (mFCTxtContentSpan == null) {
                mFCTxtContentSpan = new ForegroundColorSpan(color);
            }
             Log.e(">>>>>>>>>>>>>>>>","設定文字顔色color:"+color);
            int articleL = getText().toString().length();
            if (endPosition > articleL) {
                endPosition = articleL;
            }
            mSpannable.setSpan(mFCTxtContentSpan, startPosition, endPosition,
                    Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        } catch (Exception e){
            LogUtils.e(">>>>>>>>>>>>更改文字顔色報錯:"+e.getMessage());
        }
    }


    /**
     * 某段文字添加下劃線
     * @param color
     * @param startPosition 下劃線開始位置
     * @param endPosition 下劃線結束位置
     */
    public void addTxtUnderline(int color, int startPosition, int endPosition) {
        try {
            if (mSpannable == null) {
                super.setText(getText(), BufferType.SPANNABLE);
                CharSequence charSequence = getText();
                if (charSequence instanceof Spannable) {
                    mSpannable = (Spannable) charSequence;
                }
            }
            if (mUnderlineSpan == null) {
                mUnderlineSpan = new ColorUnderlineSpan(color);
            }
            int contentL = getText().toString().length();
            if (endPosition < contentL) {
                mSpannable.setSpan(mUnderlineSpan, startPosition, endPosition, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
            } else if (contentL > startPosition) {
                mSpannable.setSpan(mUnderlineSpan, startPosition, contentL, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
            } else {

            }
        }catch (Exception e){
            LogUtils.e(">>>>>>>>>>>","文字添加下劃線報錯:"+e);
        }
    }


    /**
     * 移除文字顔色
     */
    public void removeTxtColor() {
        if (mSpannable != null && mFCTxtContentSpan != null) {
            mSpannable.removeSpan(mFCTxtContentSpan);
            mFCTxtContentSpan = null;
        }
    }

    /**
     * 移除文字下滑線
     */
    public void removeTxtUnLine() {
        if (mSpannable != null && mUnderlineSpan != null) {
            mSpannable.removeSpan(mUnderlineSpan);
            mUnderlineSpan = null;
        }
    }

    /**
     * 移除點選事件
     */
    public void removeContentClickableSpan() {
        if (mSpannable != null && clickableSpan != null) {
            mSpannable.removeSpan(clickableSpan);
            clickableSpan = null;
        }
    }


    /**
     * 設定文字背景色
     * @param content
     * @param color
     */
    public void setRichTxtBgColor(String content, int color) {
        if (TextUtils.isEmpty(content)) {
            return;
        }
        //顯示帶圖檔的html的處理方法
        Spanned spanned = Html.fromHtml(content, mURLImageGetter, null);

        if (spanned instanceof SpannableStringBuilder) {
            mSpannableStringBuilder = (SpannableStringBuilder) spanned;
        } else {
            mSpannableStringBuilder = new SpannableStringBuilder(spanned);
        }
        mSpannable = null;
        Log.e(">>>>>>>>>>>>>>>"," 設定文字背景色color:"+color);
        updateTxtBgColor(color, 0, mSpannableStringBuilder.length());
        setMovementMethod(LinkMovementMethod.getInstance());
        super.setText(spanned);
    }

    /**
     * 設定富本文内容
     * @param content 文章内容(包含img标簽)
     */
    public void setRichContent(String content) {
        if (TextUtils.isEmpty(content)) {
            return;
        }
        //顯示帶圖檔的html的處理方法
        Spanned spanned = Html.fromHtml(content, mURLImageGetter, null);

        if (spanned instanceof SpannableStringBuilder) {
            mSpannableStringBuilder = (SpannableStringBuilder) spanned;
        } else {
            mSpannableStringBuilder = new SpannableStringBuilder(spanned);
        }
        mSpannable = null;
//        setMovementMethod(LinkMovementMethod.getInstance());
        super.setText(spanned);
    }

    /**
     *  處理圖檔的點選事件
     */
    private void setImageClickable() {
        try {
            ImageSpan[] imageSpans = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ImageSpan.class);
            final ArrayList<PictureItem> imageUrls = new ArrayList<>();
            if (ObjectUtils.isNotEmpty(imageSpans)) {
                for (int i = 0, size = imageSpans.length; i < size; i++) {
                    ImageSpan imageSpan = imageSpans[i];
                    String imageUrl = imageSpan.getSource();
                    int start = mSpannableStringBuilder.getSpanStart(imageSpan);
                    int end = mSpannableStringBuilder.getSpanEnd(imageSpan);
                    PictureItem pictureItem = new PictureItem();
                    pictureItem.setLevelPictureUrl(imageUrl);
                    imageUrls.add(pictureItem);
                    final int position = i;
                    ClickableSpan clickableSpan = new ClickableSpan() {
                        @Override
                        public void onClick(View widget) {
                            if (mRichContentClickListener != null) {
                                mRichContentClickListener.onImageClick(imageUrls, position);
                            }
                        }
                    };
                    //增加事件
                    mSpannableStringBuilder.setSpan(clickableSpan, start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
                }
            }
        }catch (Exception e){
            LogUtils.e(">>>>>>>>>>>>>>>>", "文章圖檔點選報錯:"+e);
        }
    }

    /**
     *設定可以點選的解析卡文字
     * @param analysisBeans
     */
    public void setTextClickableAnalysis(final List<LevelAnalysisBean> analysisBeans) {
        try {
            if(ObjectUtils.isNotEmpty(analysisBeans)) {
                for (int i = 0, size = analysisBeans.size(); i < size; i++) {
                    LevelAnalysisBean imageSpan = analysisBeans.get(i);
                    int start = imageSpan.getLocation().getIndexClickable();
                    if (start < 0) {
                        continue;
                    }
                    int end = start + imageSpan.getLocation().getLengthClickable();
                    final int position = i;
                    ClickableSpan clickableSpan = new ClickableSpan() {
                        @Override
                        public void onClick(View widget) {
                            if (mRichContentClickListener != null) {
                                mRichContentClickListener.onTextClickAnalysis(analysisBeans, position);
                            }
                        }

                        //去除超連結下劃線
                        @Override
                        public void updateDrawState(TextPaint ds) {
                            /**set textColor**/
//                    ds.setColor(ds.linkColor);
                            /**Remove the underline**/
                            ds.setUnderlineText(false);
                        }
                    };
                    //超文本點選事件
                    if (end > 0 && mSpannableStringBuilder.length() >= end) {
                        mSpannableStringBuilder.setSpan(clickableSpan, start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
                    }
                }
            }
        }catch (Exception e){
            LogUtils.e(">>>>>>>>>>>>>>>","設定文章的解析卡報錯:"+e);
        }
    }

    /**
     * 設定文字點選事件
     */
    private void setTextClickable(List<MarkerPenBean> imageSpans) {
        try {
            if(ObjectUtils.isNotEmpty(imageSpans)) {
                final List<MarkerPenBean> markList = new ArrayList<>();
                for (int i = 0, size = imageSpans.size(); i < size; i++) {
                    MarkerPenBean imageSpan = imageSpans.get(i);
                    int start = imageSpan.getMarkStartPosition();
                    int end = imageSpan.getMarkEndPosition();
                    markList.add(imageSpan);
                    final int position = i;
                    ClickableSpan clickableSpan = new ClickableSpan() {
                        @Override
                        public void onClick(View widget) {
                            if (mRichContentClickListener != null) {
                                mRichContentClickListener.onTextClickMark(markList, position);
                            }
                        }
                        //去除超連結下劃線
                        @Override
                        public void updateDrawState(TextPaint ds) {
                            /**set textColor**/
//                    ds.setColor(ds.linkColor);
                            /**Remove the underline**/
                            ds.setUnderlineText(false);
                        }
                    };
                    //超文本點選事件
                    if (end > 0 && mSpannableStringBuilder.length() >= end) {
                        mSpannableStringBuilder.setSpan(clickableSpan, start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
                    }
                }
            }
        }catch (Exception e){
            LogUtils.e(">>>>>>>>>>>>>>>>","設定文字的點選事件:"+e);
        }
    }

    /**
     * 修改下劃線顔色
     */
    public void updateUnlineColor(final int color, int startPosition, int endPosition) {
        if (clickableSpan == null) {
            clickableSpan = new ClickableSpan() {
                @Override
                public void onClick(View widget) {
                }

                @Override
                public void updateDrawState(TextPaint ds) {
                    /**set textColor**/
                    ds.setColor(getColor_(color));
                    /**Remove the underline**/
                    ds.setUnderlineText(true);
                }
            };

        }
        if (mSpannable == null) {
            super.setText(getText(), BufferType.SPANNABLE);
            CharSequence charSequence = getText();
            if (charSequence instanceof Spannable) {
                mSpannable = (Spannable) charSequence;
            }
        }
        //增加超文本點選事件
        mSpannable.setSpan(clickableSpan, startPosition, endPosition, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
    }

    public void removeClickableSpan(int start, int end) {
        ClickableSpan[] clickableSpans = mSpannableStringBuilder.getSpans(start, end, ClickableSpan.class);
        if (clickableSpans != null && clickableSpans.length != 0) {
            for (ClickableSpan cs : clickableSpans) {
                mSpannableStringBuilder.removeSpan(cs);
            }
        }
        //super.setTag(getText());
    }

    public void onDestroy() {
        if (mURLImageGetter != null) {
            mURLImageGetter.clear();
            mURLImageGetter = null;
        }
        this.setText(null);
    }

    /**
     * 設定圖檔監聽事件
     */
    public void setRichContentClickListener(OnRichContentClickListener mRichContentClickListener) {
        this.mRichContentClickListener = mRichContentClickListener;
    }

    public interface OnRichContentClickListener {
        /**
         * 圖檔被點選後的回調方法
         * @param imageUrls 本篇富文本内容裡的全部圖檔
         * @param position  點選處圖檔在imageUrls中的位置
         */
        void onImageClick(ArrayList<PictureItem> imageUrls, int position);

        /**
         * 文字被點選後的回調方法
         * @param textStrs
         * @param position
         */
        void onTextClick(List<String> textStrs, int position);

        /**
         * 文字被點選後的回調方法
         * @param textStrs
         * @param position
         */
        void onTextClickMark(List<MarkerPenBean> textStrs, int position);

        void onTextClickAnalysis(List<LevelAnalysisBean> textStrs, int position);

    }

    /**
     * 圖檔标簽
     * @param imgUrl
     * @return
     */
    public String getImgLabel(String imgUrl) {
        return FenJRichTextViewHelper.getImgLabel(imgUrl);
    }

    /**
     * 馬克筆标簽
     * @param marker 文字背景顔色
     * @return
     */
    public String getTextMarker(String color, String marker) {
        return mFenJRichTextViewHelper.getTextMarker(color, marker);
    }

    /**
     * 封裝擷取顔色
     */
    public int getColor_(int colorId) {
        return ContextCompat.getColor(getContext(), colorId);
    }

    private class ColorUnderlineSpan extends UnderlineSpan {
        private int underlineColor;

        public ColorUnderlineSpan(int underlineColor) {
            super();
            this.underlineColor = underlineColor;
        }

        @Override
        public void updateDrawState(TextPaint ds) {
            super.updateDrawState(ds);
//            ds.setColor(underlineColor);
//            ds.linkColor = underlineColor;
        }
    }

    /**
     * @param content
     * @param resId
     * @param vipW
     * @param vipH
     */
    public void setIconText(String content, int resId, int vipW, int vipH) {
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId);
        Bitmap localB = zoomImg(bitmap,dip2px(getContext(), bitmap.getWidth()),dip2px(getContext(), bitmap.getHeight()));
        CenterAlignImageSpan imgSpan = new CenterAlignImageSpan(localB);
        SpannableString spanString = new SpannableString("V");
        spanString.setSpan(imgSpan, 0, "V".length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        super.setText(spanString);
        append(content);
    }

    private Drawable getDrawable(int resId) {
        return ContextCompat.getDrawable(getContext(), resId);
    }

    public Html.ImageGetter getImageGetterInstance(final CharSequence content) {
        Html.ImageGetter imgGetter = new Html.ImageGetter() {
            @Override
            public Drawable getDrawable(String source) {
                int id = Integer.parseInt(source);
                int hdp = dip2px(getContext(), 19);
                int wdp = dip2px(getContext(), 42);
                Drawable drawable = new TextDrawable(getContext(),  source,hdp,10);
//                int hdp = drawable.getIntrinsicWidth();
                drawable.setBounds(0, 0, wdp, hdp);
                return drawable;

            }
        };
        return imgGetter;
    }

    public static int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

    public void clearAllMarkerPen() {
        if (mSpannableStringBuilder != null) {
            mSpannableStringBuilder.clearSpans();
        }
    }

    public void clear() {
        clearAllMarkerPen();
        if (mSpannableStringBuilder != null) {
            mSpannableStringBuilder.clear();
        }
    }

    public class CenterAlignImageSpan extends ImageSpan {

        public CenterAlignImageSpan(Drawable drawable) {
            super(drawable);

        }

        public CenterAlignImageSpan(Bitmap b) {
            super(b);
        }

        @Override
        public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x,
                         int top, int y, int bottom, @NonNull Paint paint) {

            Drawable b = getDrawable();
            Paint.FontMetricsInt fm = paint.getFontMetricsInt();
            int transY = (y + fm.descent + y + fm.ascent) / 2 - b.getBounds().bottom / 2;//計算y方向的位移
            canvas.save();
            canvas.translate(x, transY);//繪制圖檔位移一段距離
            b.draw(canvas);
            canvas.restore();
        }
    }

    /**
     * 放大圖檔
     * @param bm
     * @param newWidth
     * @param newHeight
     * @return
     */
    public Bitmap zoomImg(Bitmap bm, int newWidth ,int newHeight){
        // 獲得圖檔的寬高   
        int width = bm.getWidth();
        int height = bm.getHeight();
        // 計算縮放比例   
        float scaleWidth = ((float) newWidth) / width;
        float scaleHeight = ((float) newHeight) / height;
        // 取得想要縮放的matrix參數   
        Matrix matrix = new Matrix();
        matrix.postScale(scaleWidth, scaleHeight);
        // 得到新的圖檔   www.2cto.com
        Bitmap newbm = Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true);
        return newbm;
    }
}
           

繼續閱讀