1. 程式人生 > >RichEditeText——android圖文混排富文字文章編輯器實現詳解

RichEditeText——android圖文混排富文字文章編輯器實現詳解

需求:android 實現富文字編輯器,並且實現html解析和生成。

功能點:

  1. 字型加粗,斜體,下劃線,刪除線
  2. 字型設定大小   預設大(18px),中(16px),小(14px)
  3. 字型設定顏色
  4. 換行插入圖片
  5. 編輯內容生成html
  6. 解析html並且顯示

主要實現方式

  1. EditText + Span 的實現方式
  2. WebView + JavaScript 的實現方式

webview方式存在相容性問題,所以還是得走原生路線。EditText + Span

知識準備

span是設定 EditText 內容效果的 物件,是內容表達的載體;span派生類有StyleSpan(加粗斜體),UnderlineSpan(下劃線),StrikethroughSpan(刪除線)等等。

Spanable中的常用常量:

Spanned.SPAN_EXCLUSIVE_EXCLUSIVE --- 不包含start和end所在的端點                 (a,b)

Spanned.SPAN_EXCLUSIVE_INCLUSIVE --- 不包含端start,但包含end所在的端點       (a,b]

Spanned.SPAN_INCLUSIVE_EXCLUSIVE --- 包含start,但不包含end所在的端點          [a,b)

Spanned.SPAN_INCLUSIVE_INCLUSIVE--- 包含start和end所在的端點                       [a,b]

瞭解了大概之後,就開始寫程式碼;

1.定義FontStyle 字型樣式基類,定義初始化Span方法

/**
     * 返回 初始化 span
     * @param fontStyle
     * @return
     */
 private CharacterStyle getInitSpan(FontStyle fontStyle){
        if(fontStyle.isBold){
            return new StyleSpan(Typeface.BOLD);
        }else if(fontStyle.isItalic){
            return new StyleSpan(Typeface.ITALIC);
        }else if(fontStyle.isUnderline){
            return new UnderlineSpan();
        }else if(fontStyle.isStreak){
            return new StrikethroughSpan();
        }else if(fontStyle.fontSize>0){
            return new AbsoluteSizeSpan(fontStyle.fontSize,true);
        }else if(fontStyle.color!=0){
            return new ForegroundColorSpan(fontStyle.color);
        }
        return  null;
    }

/**
     * 通用set Span
     * @param fontStyle
     * @param isSet
     * @param tClass
     * @param <T>
     */
    private <T> void setSpan(FontStyle fontStyle,boolean isSet,Class<T> tClass){
        Log.d("setSpan","");
        int start = getSelectionStart();
        int end = getSelectionEnd();
        int mode = EXCLUD_INCLUD_MODE;
        T[] spans = getEditableText().getSpans(start,end,tClass);
        //獲取
        List<SpanPart> spanStyles = getOldFontSytles(spans,fontStyle);
        for(SpanPart spanStyle : spanStyles){
            if(spanStyle.start<start){
                if(start==end){mode=EXCLUD_MODE;}
                getEditableText().setSpan(getInitSpan(spanStyle), spanStyle.start,start,mode);
            }
            if(spanStyle.end>end){
                getEditableText().setSpan(getInitSpan(spanStyle),end, spanStyle.end,mode);
            }
        }
        if(isSet){
            if(start==end){
                mode=INCLUD_INCLUD_MODE;
            }
            getEditableText().setSpan(getInitSpan(fontStyle),start,end,mode);
        }
    }

 /**
     *  獲取當前 選中 spans
     * @param spans
     * @param fontStyle
     * @param <T>
     * @return
     */
    private <T> List<SpanPart> getOldFontSytles(T[] spans, FontStyle fontStyle){
        List<SpanPart> spanStyles = new ArrayList<>();
        for(T span:spans){
            boolean isRemove=false;
            if(span instanceof StyleSpan){//特殊處理 styleSpan
                int style_type = ((StyleSpan) span).getStyle();
                if((fontStyle.isBold&& style_type== Typeface.BOLD)
                        || (fontStyle.isItalic&&style_type== Typeface.ITALIC)){
                    isRemove=true;
                }
            }else{
                isRemove=true;
            }
            if(isRemove) {
                SpanPart spanStyle = new SpanPart(fontStyle);
                spanStyle.start = getEditableText().getSpanStart(span);
                spanStyle.end = getEditableText().getSpanEnd(span);
                if(span instanceof AbsoluteSizeSpan){
                    spanStyle.fontSize = ((AbsoluteSizeSpan) span).getSize();
                }else if(span instanceof ForegroundColorSpan){
                    spanStyle.color = ((ForegroundColorSpan) span).getForegroundColor();
                }
                spanStyles.add(spanStyle);
                getEditableText().removeSpan(span);
            }
        }
        return spanStyles;
    }

setSpan 是公共設定樣式方法,通過fontStyle傳參,設定對應的樣式,例如設定加粗和斜體

/**
     * bold italic
     * @param isSet
     * @param type
     */
    private void setStyleSpan(boolean isSet,int type){
        FontStyle fontStyle = new FontStyle();
        if(type== Typeface.BOLD){
            fontStyle.isBold=true;
        }else if(type== Typeface.ITALIC){
            fontStyle.isItalic=true;
        }
        setSpan(fontStyle,isSet,StyleSpan.class);
    }

setSpan處理思路:

  1. 獲取當前選中位置position,在該位置是否已經設定了 需要處理樣式,如 加粗;
  2. 如果有,在getOldFontSytles 方法中,會進行判斷移除;(因為假如選中位置有加粗,再設定一次就是取消)
  3. span設定樣式和 html 類似,是通過始末設tag來控制區間樣式的,所以,你選中區間樣式CD,可能與原有樣式區間AB是包含,交集關係。因此,當你移除舊樣式的時候,需要補始末的tag,這樣才能保持未選中的區間樣式不變。程式碼getOldFontSytles後for 迴圈執行補tag 邏輯。
  4. 當非選中狀態下,即游標移至某處,設定字型樣式,隨後輸入的文字都是當前設定樣式,需要判斷start =end ,然後變更span設定mode 方式。需要使用SPAN_INCLUSIVE_INCLUSIVE。

加粗斜體效果

2.插入圖片

設定圖片,需要用到ImageSpan  ImageSpan(Context context, Bitmap b)   通過重定義RichImageSpan 繼承 ImageSpan 同時重寫getSource方法,賦值uri 這樣利用Glide管理bitmap,防止記憶體溢位。(\nimg\n 是為了讓圖片佔位,可以自行設定別的,沒有要求)

public class RichImageSpan extends ImageSpan {
    private Uri mUri;
    public RichImageSpan(Context context, Bitmap b, Uri uri) {
        super(context, b);
        mUri = uri;
    }

    @Override
    public String getSource() {
        return mUri.toString();
    }
} 
/**
     * 圖片載入
     * @param path
     */
 public void image(String path) {
        final Uri uri = Uri.parse(path);
        final int maxWidth = view.getMeasuredWidth() -view. getPaddingLeft() - view.getPaddingRight();
        RequestOptions options = new RequestOptions()
                .centerCrop()
                .placeholder(R.mipmap.ic_launcher)
                .error(R.mipmap.ic_launcher);
        glideRequests.asBitmap()
                .load(new File(path))
                .apply(options)
                .into(new SimpleTarget<Bitmap>() {
                    @Override
                    public void onResourceReady(Bitmap resource, Transition<? super Bitmap> transition) {
                        Bitmap bitmap = zoomBitmapToFixWidth(resource, maxWidth);
                        image(uri, bitmap);
                    }
                });
    }

    public void image(Uri uri, Bitmap pic) {
        String img_str="img";
        int start = view.getSelectionStart();
        SpannableString ss = new SpannableString("\nimg\n");
        RichImageSpan myImgSpan = new RichImageSpan(mContext, pic, uri);
        ss.setSpan(myImgSpan, 1, img_str.length()+1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        view.getEditableText().insert(start, ss);// 設定ss要新增的位置
        view.requestLayout();
        view.requestFocus();
//        setClick(ss.getSpanStart(myImgSpan),ss.getSpanEnd(myImgSpan),img_str);
    }

插入圖片效果

3.span生成html

目前原生 hmtl  能夠支援進行html 解析,但是想做定製化的解析,需要對其進行修改。拷貝一份Html.java 為CustomHtml.java;

檢視原始碼得知,html 將span 轉化 html 是通過 withinParagraph方法,遍歷當前控制元件樣式CharacterStyle 陣列,然後根據對應樣式,加入對應css 標籤(現在主流是style 方式, 目前我只是簡單使用了常規html標籤做樣式控制,可以改)。

部分核心程式碼如下

    private static void withinParagraph(StringBuilder out, Spanned text, int start, int end) {
        int next;
        for (int i = start; i < end; i = next) {
            next = text.nextSpanTransition(i, end, CharacterStyle.class);
            CharacterStyle[] style = text.getSpans(i, next, CharacterStyle.class);
            AbsoluteSizeSpan tmp_rel_span = null;
            ForegroundColorSpan tmp_fColor_span =null;
            for (int j = 0; j < style.length; j++) {
                if (style[j] instanceof StyleSpan) {
                    int s = ((StyleSpan) style[j]).getStyle();

                    if ((s & Typeface.BOLD) != 0) {
                        out.append("<b>");
                    }
                    if ((s & Typeface.ITALIC) != 0) {
                        out.append("<i>");
                    }
                }
                if (style[j] instanceof TypefaceSpan) {
                    String s = ((TypefaceSpan) style[j]).getFamily();

                    if ("monospace".equals(s)) {
                        out.append("<tt>");
                    }
                }
                if (style[j] instanceof SuperscriptSpan) {
                    out.append("<sup>");
                }
                if (style[j] instanceof SubscriptSpan) {
                    out.append("<sub>");
                }
                if (style[j] instanceof UnderlineSpan) {
                    out.append("<u>");
                }
                if (style[j] instanceof StrikethroughSpan) {
//                    out.append("<span style=\"text-decoration:line-through;\">");
                    out.append("<strike>");
                }
                if (style[j] instanceof URLSpan) {
                    out.append("<a href=\"");
                    out.append(((URLSpan) style[j]).getURL());
                    out.append("\">");
                }
                if (style[j] instanceof ImageSpan) {
                    out.append("<img src=\"");
                    out.append(((ImageSpan) style[j]).getSource());
                    out.append("\">");

                    // Don't output the dummy character underlying the image.
                    i = next;
                }
                if (style[j] instanceof AbsoluteSizeSpan) {
                    tmp_rel_span= ((AbsoluteSizeSpan) style[j]);
//                    AbsoluteSizeSpan s = ((AbsoluteSizeSpan) style[j]);
//                    float sizeDip = s.getSize();
//                    if (!s.getDip()) {
//                        Application application = CustomApplication.currentApplication();
//                        sizeDip /= application.getResources().getDisplayMetrics().density;
//                    }
//
//                    // px in CSS is the equivalance of dip in Android
//                    out.append(String.format("<span style=\"font-size:%.0fpx\";>", sizeDip));
                }
                if (style[j] instanceof RelativeSizeSpan) {
                    float sizeEm = ((RelativeSizeSpan) style[j]).getSizeChange();
                    out.append(String.format("<span style=\"font-size:%.2fem;\">", sizeEm));
                }
                if (style[j] instanceof ForegroundColorSpan) {
                    tmp_fColor_span = ((ForegroundColorSpan) style[j]);
//                    int color = ((ForegroundColorSpan) style[j]).getForegroundColor();
//                    out.append(String.format("<span style=\"color:#%06X;\">", 0xFFFFFF & color));
                }
                if (style[j] instanceof BackgroundColorSpan) {
                    int color = ((BackgroundColorSpan) style[j]).getBackgroundColor();
                    out.append(String.format("<span style=\"background-color:#%06X;\">",
                            0xFFFFFF & color));
                }
            }
            //處理字型 顏色
            StringBuilder style_font = new StringBuilder();
            if(tmp_fColor_span!=null||tmp_rel_span!=null){
                style_font.append("<font ");
            }
            //顏色
            if(tmp_fColor_span!=null){
                style_font.append(String.format("color='#%06X' ", 0xFFFFFF &  tmp_fColor_span.getForegroundColor()));
            }
            //字型
            if(tmp_rel_span!=null){
                String value = "16px";
                if(tmp_rel_span.getSize()== FontStyle.BIG){
                    value="18px";
                }else if(tmp_rel_span.getSize()==FontStyle.SMALL){
                    value="14px";
                }
                style_font.append("style='font-size:"+value+";'");
            }
            if(style_font.length()>0){
                out.append(style_font+">");
            }
            withinStyle(out, text, i, next);
            if(style_font.length()>0){
                out.append("</font>");
            }
            for (int j = style.length - 1; j >= 0; j--) {
                if (style[j] instanceof BackgroundColorSpan) {
                    out.append("</span>");
                }
                if (style[j] instanceof ForegroundColorSpan) {
//                    out.append("</span>");
                }
                if (style[j] instanceof RelativeSizeSpan) {
                    out.append("</span>");
                }
                if (style[j] instanceof AbsoluteSizeSpan) {
//                    out.append("</span>");
                }
                if (style[j] instanceof URLSpan) {
                    out.append("</a>");
                }
                if (style[j] instanceof StrikethroughSpan) {
//                    out.append("</span>");
                    out.append("</strike>");
                }
                if (style[j] instanceof UnderlineSpan) {
                    out.append("</u>");
                }
                if (style[j] instanceof SubscriptSpan) {
                    out.append("</sub>");
                }
                if (style[j] instanceof SuperscriptSpan) {
                    out.append("</sup>");
                }
                if (style[j] instanceof TypefaceSpan) {
                    String s = ((TypefaceSpan) style[j]).getFamily();

                    if (s.equals("monospace")) {
                        out.append("</tt>");
                    }
                }
                if (style[j] instanceof StyleSpan) {
                    int s = ((StyleSpan) style[j]).getStyle();

                    if ((s & Typeface.BOLD) != 0) {
                        out.append("</b>");
                    }
                    if ((s & Typeface.ITALIC) != 0) {
                        out.append("</i>");
                    }
                }
            }
        }
    }

接下來我們就剛剛gif 輸入內容生成html看看效果

copy出來在W3School上看顯示效果

p.s.圖片顯示不出,因為路徑是手機本地,若需要,應當在轉html時,先上傳獲得圖片url,在賦值轉html。

html 轉 span 

轉換核心在於 CustomHtmlToSpannedConverter類,它通過識別html的標籤 然後對應處理 生成span;我主要處理了handleStartTag ,handleEndTag 方法,增加了圖片處理通過繼承 ImageGetter (網上一般處理方法)重寫getDrawable。

 private void handleStartTag(String tag, Attributes attributes) {
        if (tag.equalsIgnoreCase("br")) {
            // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
            // so we can safely emit the linebreaks when we handle the close tag.
        } else if (tag.equalsIgnoreCase("p")) {
            startBlockElement(mSpannableStringBuilder, attributes, getMarginParagraph());
            startCssStyle(mSpannableStringBuilder, attributes);
        } else if (tag.equalsIgnoreCase("ul")) {
            startBlockElement(mSpannableStringBuilder, attributes, getMarginList());
        } else if (tag.equalsIgnoreCase("li")) {
            startLi(mSpannableStringBuilder, attributes);
        } else if (tag.equalsIgnoreCase("div")) {
            startBlockElement(mSpannableStringBuilder, attributes, getMarginDiv());
        } else if (tag.equalsIgnoreCase("span")) {
            startCssStyle(mSpannableStringBuilder, attributes);
        } else if (tag.equalsIgnoreCase("strong")) {
            start(mSpannableStringBuilder, new Bold());
        } else if (tag.equalsIgnoreCase("b")) {
            start(mSpannableStringBuilder, new Bold());
        } else if (tag.equalsIgnoreCase("em")) {
            start(mSpannableStringBuilder, new Italic());
        } else if (tag.equalsIgnoreCase("cite")) {
            start(mSpannableStringBuilder, new Italic());
        } else if (tag.equalsIgnoreCase("dfn")) {
            start(mSpannableStringBuilder, new Italic());
        } else if (tag.equalsIgnoreCase("i")) {
            start(mSpannableStringBuilder, new Italic());
        } else if (tag.equalsIgnoreCase("big")) {
            start(mSpannableStringBuilder, new Big());
        } else if (tag.equalsIgnoreCase("small")) {
            start(mSpannableStringBuilder, new Small());
        } else if (tag.equalsIgnoreCase("font")) {
            startFont(mSpannableStringBuilder, attributes);
        } else if (tag.equalsIgnoreCase("blockquote")) {
            startBlockquote(mSpannableStringBuilder, attributes);
        } else if (tag.equalsIgnoreCase("tt")) {
            start(mSpannableStringBuilder, new Monospace());
        } else if (tag.equalsIgnoreCase("a")) {
            startA(mSpannableStringBuilder, attributes);
        } else if (tag.equalsIgnoreCase("u")) {
            start(mSpannableStringBuilder, new Underline());
        } else if (tag.equalsIgnoreCase("del")) {
            start(mSpannableStringBuilder, new Strikethrough());
        } else if (tag.equalsIgnoreCase("s")) {
            start(mSpannableStringBuilder, new Strikethrough());
        } else if (tag.equalsIgnoreCase("strike")) {
            start(mSpannableStringBuilder, new Strikethrough());
        } else if (tag.equalsIgnoreCase("sup")) {
            start(mSpannableStringBuilder, new Super());
        } else if (tag.equalsIgnoreCase("sub")) {
            start(mSpannableStringBuilder, new Sub());
        } else if (tag.length() == 2 &&
                Character.toLowerCase(tag.charAt(0)) == 'h' &&
                tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
            startHeading(mSpannableStringBuilder, attributes, tag.charAt(1) - '1');
        } else if (tag.equalsIgnoreCase("img")) {
            startImg(mSpannableStringBuilder, attributes, mImageGetter);
        } else if (mTagHandler != null) {
            mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
        }
    }

如上程式碼所示,可以根據自己定義的協議,修改對應tag標籤處理。

總體效果圖

已上傳github,喜歡的朋友,可以收藏給個心;

地址