EditText-Android实现图文混排

目录

1.Span样式介绍

1.1Span类继承关系

1.2StyleSpan

1.3AbsoluteSizeSpan

1.4ForegroundColorSpan

1.5UnderlineSpan

1.6StrikethroughSpan

1.7Spanned

2.实现富文本编辑

2.1定义富文本样式类

2.2生成选中文字要设置的Span(样式)

2.3应用样式到选中文字上

2.4设置粗体示例

2.5获取点击位置的样式

2.6插入图片

3.span生成Html

3.html 转 span


需求:Android实现富文本编辑器,并实现Html解析和生成;

常见富文本编辑操作:

1.字体加粗,斜体,下划线,删除线;

2.字体设置大小 默认大(18px),中(16px),小(14px);

3.字体颜色设置;

4.换行插入图片;

5.编辑内容生成Html;

6.解析Html并且显示;

主要实现方式:

1.EditText+Span的实现方式;

2.WebView+JavaScript的实现方式;

EditText是一个输入框控件,Span控制输入框内容显示样式;

1.Span样式介绍

1.1Span类继承关系

CharacterStyle:是控制内容显示样式Span的基类;控制文字样式的类基本都是扩展CharacterStyle类,大多数样式继承自MetricAffectingSpan类,简单的样式只需要实现UpdateAppearance;

EditText-Android实现图文混排_第1张图片

1.2StyleSpan

设置字体:粗体,斜体,正常,粗体+斜体,支持样式如下:

android.graphics.Typeface
// Style
    public static final int NORMAL = 0;
    public static final int BOLD = 1;
    public static final int ITALIC = 2;
    public static final int BOLD_ITALIC = 3;

创建字体样式示例:

//粗体
new StyleSpan(Typeface.BOLD);
//斜体
new StyleSpan(Typeface.ITALIC);

1.3AbsoluteSizeSpan

设置字体大小

/**
     * Set the text size to size physical pixels,
     * or to size device-independent pixels if
     * dip is true.
     */
    //设置物理像素(px)大小(dip:false)或者设置与设备像素无关的大小(dip:true)
    public AbsoluteSizeSpan(int size, boolean dip) {
        mSize = size;
        mDip = dip;
    }

使用示例:

new AbsoluteSizeSpan(16, true);

1.4ForegroundColorSpan

设置字体颜色

public ForegroundColorSpan(@ColorInt int color) {
        mColor = color;
    }

使用示例:

new ForegroundColorSpan(Color.parseColor("#00ff00"))

1.5UnderlineSpan

设置文字内容下划线

使用示例:

new UnderlineSpan()

1.6StrikethroughSpan

设置文字内容删除线

new StrikethroughSpan()

1.7Spanned

设置文字内容样式范围(主要开始和结束位置前面或者后面继续使用设置样式)

    //包含start不包含end[start,end)
    public static final int SPAN_INCLUSIVE_EXCLUSIVE = SPAN_MARK_MARK;

    //包含start同时包含end[start,end]
    public static final int SPAN_INCLUSIVE_INCLUSIVE = SPAN_MARK_POINT;

    //不包含start同时不包含end(start,end)
    public static final int SPAN_EXCLUSIVE_EXCLUSIVE = SPAN_POINT_MARK;

    //不包含start包含end(start,end]
    public static final int SPAN_EXCLUSIVE_INCLUSIVE = SPAN_POINT_POINT;

2.实现富文本编辑

public class RichEditText extends AppCompatEditText {}

EditText-Android实现图文混排_第2张图片

2.1定义富文本样式类

/**
 * 字体样式
 */

public class FontStyle {
    public final static int NORMAL = 16;
    public final static int SMALL = 14;
    public final static int BIG = 18;

    public final static String BLACK="#FF212121";
    public final static String GREY="#FF878787";
    public final static String RED="#FFF64C4C";
    public final static String BLUE="#FF007AFF";

    public boolean isBold;
    public boolean isItalic;
    public boolean isUnderline;
    public boolean isStreak;
    public int fontSize;
    public int color;
}

2.2生成选中文字要设置的Span(样式)

RichEditText
public 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;
    }

2.3应用样式到选中文字上

setSpan是公用方法,fontStyle设置选中内容的样式,isSet设置或取消样式,tClass当前要设置Span样式class;

public  void setSpan(FontStyle fontStyle, boolean isSet, Class tClass){
        //获取选中文字的开始和结束位置
        int start = getSelectionStart();
        int end = getSelectionEnd();
        //不包含start字符包含end字符,选中end位置以后继续使用设置样式
        int mode = Spannable.SPAN_EXCLUSIVE_INCLUSIVE;
        //获取文字tClass(例如:StyleSpan.class)类型样式数组(Span)
        //tClass主要设置文字样式的class
        T[] spans = getEditableText().getSpans(start,end,tClass);
        //移除选中文字的样式,同时保存样式
        List spanStyles = getOldFontStyles(spans, fontStyle);
        //重新设置样式
        for (SpanPart spanPart : spanStyles){
            if(spanPart.startend){//例如粗体设置abcdef,cde修改为非粗体,为f设置为粗体
                getEditableText().setSpan(getInitSpan(spanPart), end, spanPart.end, mode);
            }
        }
        //为选中文字设置样式
        if(isSet){
            if(start == end){//光标位置,光标位置继续输入使用fontStyle样式
                //包含start和end
                mode = Spannable.SPAN_INCLUSIVE_INCLUSIVE;
            }
            getEditableText().setSpan(getInitSpan(fontStyle), start, end, mode);
        }
    }


    /**
     * 获取当前选中文字的样式(spans)
     * 移除选中文字的样式,同时获取选中文字样式,例如abcdef 都是粗体,我们想把cde设置正常,
     * 把abcdef粗体设置都移除,然后ab和f重新设置粗体
     *
     * @param spans
     * @param fontStyle
     * @param 
     * @return
     */
    private  List getOldFontStyles(T[] spans, FontStyle fontStyle){
        List spanParts = new ArrayList<>();
        for(T span : spans){    //遍历样式
            //移除当前选中文字样式
            boolean isRemove =  false;
            //特殊处理styleSpan
            if(span instanceof 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 spanPart = new SpanPart(fontStyle);
                //获取span样式的开始和结束位置
                spanPart.start = getEditableText().getSpanStart(span);
                spanPart.end = getEditableText().getSpanEnd(span);
                //保留样式,字体大小或者颜色
                if(span instanceof AbsoluteSizeSpan){
                    spanPart.fontSize = ((AbsoluteSizeSpan)span).getSize();
                }else if(span instanceof ForegroundColorSpan){
                    spanPart.color = ((ForegroundColorSpan)span).getForegroundColor();
                }
                spanParts.add(spanPart);
                //移除选中文字要设置的样式
                getEditableText().removeSpan(span);
            }
        }
        //返回选中文字样式
        return spanParts; 
    }

setSpan处理思路:

a.获取选中位置已设置样式的tClass样式数组,getEditableText().getSpans(start,end,tClass);

b.如果有,在getOldFontStyles(T[] spans, FontStyle fontStyle)方法中移除;(例如,设置粗体,再次设置取消);

c.span设置样式和 html 类似,是通过始末设tag来控制区间样式的,所以,你选中区间样式CD,可能与原有样式区间AB是包含,交集关系。因此,当你移除旧样式的时候,需要补始末的tag,这样才能保持未选中的区间样式不变。代码getOldFontSytles后for 循环执行补tag 逻辑;

d.当非选中状态下,即光标移至某处,设置字体样式,随后输入的文字都是当前设置样式,需要判断start =end ,然后变更span设置mode 方式。需要使用SPAN_INCLUSIVE_INCLUSIVE;

2.4设置粗体示例

richEditText.setBold(isBold);

定义设置粗体样式方法

RichEditText
    public void setBold(boolean  isBold){
        setStyleSpan(isBold, Typeface.BOLD);
    }
/**
     * 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);
    }

2.5获取点击位置的样式

获取点击位置样式,可以更新样式按钮颜色,以便知道当前选中位置样式;

 @Override
    public void onClick(View v) {
        int start = getSelectionStart()-1;
        if(start==-1) start=0;
        if(start<-1){
            return;
        }
        FontStyle fontStyle = getFontStyle(start, start);
        setBold(fontStyle.isBold);
        setItalic(fontStyle.isItalic);
        setUnderline(fontStyle.isUnderline);
        setStreak(fontStyle.isStreak);
        setFontSize(fontStyle.fontSize);
        setForegroundColor(fontStyle.color);
        if(onSelectChangeListener != null){
            onSelectChangeListener.onFontStyleChang(fontStyle);
            onSelectChangeListener.onSelect(start, start);
        }
    }
/**
     * 获取某位置的样式
     * @param start
     * @param end
     * @return
     */
    private FontStyle getFontStyle(int start, int end){
        FontStyle fontStyle = new FontStyle();
        CharacterStyle[] characterStyles = getEditableText().getSpans(start, end, CharacterStyle.class);
        for(CharacterStyle style : characterStyles){
            if(style instanceof StyleSpan){
                int type = ((StyleSpan)style).getStyle();
                if(type == Typeface.BOLD){
                    fontStyle.isBold = true;
                }else if(type == Typeface.ITALIC){
                    fontStyle.isItalic = true;
                }
            }else if(style instanceof UnderlineSpan){
                fontStyle.isUnderline = true;
            }else if(style instanceof StrikethroughSpan){
                fontStyle.isStreak = true;
            }else if(style instanceof AbsoluteSizeSpan){
                fontStyle.fontSize = ((AbsoluteSizeSpan)style).getSize();
            }else if(style instanceof ForegroundColorSpan){
                fontStyle.fontSize = ((ForegroundColorSpan)style).getForegroundColor();
            }
        }
        return fontStyle;
    }

2.6插入图片

设置图片,需要用到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);
        this.mUri = uri;
    }

    @Override
    public String getSource() {
        return mUri.toString();//super.getSource();
    }
}

插入ImageSpan

ImagePlate

/**
     * 图片加载
     * @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() {
                    @Override
                    public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition 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\n");
        RichImageSpan imageSpan = new RichImageSpan(mContext, pic, uri);

        ss.setSpan(imageSpan, 1, img_str.length()+1, Spanned. SPAN_EXCLUSIVE_EXCLUSIVE);
        view.getEditableText().insert(start, ss);// 设置ss要添加的位置
        //设置点击事件
        setClick(start+1, start+ss.length()-2, uri.getPath());

        view.requestLayout();
        view.requestFocus();
    }

    public void setClick(int start, int end, final  String path){
        view.setMovementMethod(LinkMovementMethod.getInstance());
        ClickableSpan clickableSpan = new ClickableSpan() {
            @Override
            public void onClick(View widget) {
//                view.clearFocus(); //跳转时候弹键盘
                InputMethodManager imm = (InputMethodManager)mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
                //强制隐藏键盘
                imm.hideSoftInputFromInputMethod(view.getWindowToken(), 0);
                Toast.makeText(mContext, path, Toast.LENGTH_LONG).show();
            }
        };
        view.getEditableText().setSpan(clickableSpan,start,end,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }

3.span生成Html

目前原生 htm能够支持进行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("");
                    }
                    if ((s & Typeface.ITALIC) != 0) {
                        out.append("");
                    }
                }
                if (style[j] instanceof TypefaceSpan) {
                    String s = ((TypefaceSpan) style[j]).getFamily();

                    if ("monospace".equals(s)) {
                        out.append("");
                    }
                }
                if (style[j] instanceof SuperscriptSpan) {
                    out.append("");
                }
                if (style[j] instanceof SubscriptSpan) {
                    out.append("");
                }
                if (style[j] instanceof UnderlineSpan) {
                    out.append("");
                }
                if (style[j] instanceof StrikethroughSpan) {
//                    out.append("");
                    out.append("");
                }
                if (style[j] instanceof URLSpan) {
                    out.append("");
                }
                if (style[j] instanceof ImageSpan) {
                    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("", sizeDip));
                }
                if (style[j] instanceof RelativeSizeSpan) {
                    float sizeEm = ((RelativeSizeSpan) style[j]).getSizeChange();
                    out.append(String.format("", sizeEm));
                }
                if (style[j] instanceof ForegroundColorSpan) {
                    tmp_fColor_span = ((ForegroundColorSpan) style[j]);
//                    int color = ((ForegroundColorSpan) style[j]).getForegroundColor();
//                    out.append(String.format("", 0xFFFFFF & color));
                }
                if (style[j] instanceof BackgroundColorSpan) {
                    int color = ((BackgroundColorSpan) style[j]).getBackgroundColor();
                    out.append(String.format("",
                            0xFFFFFF & color));
                }
            }
            //处理字体 颜色
            StringBuilder style_font = new StringBuilder();
            if(tmp_fColor_span!=null||tmp_rel_span!=null){
                style_font.append("0){
                out.append(style_font+">");
            }
            withinStyle(out, text, i, next);
            if(style_font.length()>0){
                out.append("");
            }
            for (int j = style.length - 1; j >= 0; j--) {
                if (style[j] instanceof BackgroundColorSpan) {
                    out.append("");
                }
                if (style[j] instanceof ForegroundColorSpan) {
//                    out.append("");
                }
                if (style[j] instanceof RelativeSizeSpan) {
                    out.append("");
                }
                if (style[j] instanceof AbsoluteSizeSpan) {
//                    out.append("");
                }
                if (style[j] instanceof URLSpan) {
                    out.append("");
                }
                if (style[j] instanceof StrikethroughSpan) {
//                    out.append("");
                    out.append("");
                }
                if (style[j] instanceof UnderlineSpan) {
                    out.append("");
                }
                if (style[j] instanceof SubscriptSpan) {
                    out.append("");
                }
                if (style[j] instanceof SuperscriptSpan) {
                    out.append("");
                }
                if (style[j] instanceof TypefaceSpan) {
                    String s = ((TypefaceSpan) style[j]).getFamily();

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

                    if ((s & Typeface.BOLD) != 0) {
                        out.append("");
                    }
                    if ((s & Typeface.ITALIC) != 0) {
                        out.append("");
                    }
                }
            }
        }
    }

生成html:

String content = CustomHtml.toHtml(richEditText.getEditableText(), CustomHtml.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE);

生成html结果:

yyyfff

3.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 
for each
// 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 : https://github.com/awarmisland

https://www.jianshu.com/p/8fd4b10d5f4b

你可能感兴趣的:(Android拓展,EditText,富文本)