【富文本之图文混合】自定义图文样式及其点击事件(更新中...)

以下为原生实现 如果你的富文本编辑更复杂可转看

Android富文本编辑器进阶版(干货,IOS可照搬逻辑)

 

最新demo源码点击查看

 

前言

显示富文本是非常容易的,你可以使用span指定位置,html格式,甚至是一个容器组合多个view多种方式。搜索一下很快实现。

编辑富文本并再现则需要用Edittext或者仿类似模式,则比较复杂一点。

html显示很方便,但自定义编辑则比较麻烦。如点击事件,span则自定义很方便。但再现出数据比较麻烦,并加载。

本文是两者结合使用,span是可以直接转为html的,-最开始我就不知道这一点,导致不断的解析span,浪费了大量的时间

public String getHtmlData() {
    return Html.toHtml(getText());
}

因为能用网页表达清楚数据后,则通过正则便捷获取便签值方便再现。为什么还要加上自定义标签呢?是方便拓展功能,比如你Edittext还想插入语音并且传到服务器到其他手机上效果再现出来。语音条你可以用图片去代替。点击事件则可以通过自定义标签里面参合其他数据,给图片赋予其他的含义则可以做很多事情。

自定义便签肯定只有自己可以识别,如果没有自定义标签,则生成的数据,可以丢到txt文本改后缀html。就能显示。非常方便生成网页。

注意事项,实际操作中我们需要的是网络图片传输。所以无论是编辑还是再现,得先加载出图片,再生成view数据。

目前放出核心源码,给正在做富文本编辑并再现,且自定义比较高的同学一点灵感,具体讲解等我编辑好直接能依赖的源码,再更新此文。


如果你的富文本编辑自定义并不高,可搜索webview实现的

richeditor-android
但自定义可行性比较低,比如图片的大小怎么自定义调整,点击事件如何添加并再现,如何控制光标。
也有仿markdown编辑器的,但我们的用户只会傻瓜输入,都没有满足我的需求,才着手自己处理
需求是,类似简书或者石墨文档类的编辑器。

主要内容:

1.加粗和插入网络图片原理

2.数据的获取与再现

3.自定义html tag的解析

setText(Html.fromHtml(content,
        null, new HtmlNewTagHandler((Activity) getContext(), httpDraws))

先以加粗和插入网络图片为例子。文章借助了glide加载网络图片

 

   /**
     * 插入一个在线图片
     *
     * @param imgPath
     */
    public void insertImage(final String imgPath) {
        insertImage(imgPath, null);
    }


    /**
     * @param imgPath       图片地址
     * @param clickableSpan 点击事件
     */
    public void insertImage(final String imgPath, final IClickableSpan clickableSpan) {
        is2ndChanged = true;
        getText().append("\n");

        Glide.with(getContext()).asDrawable().load(imgPath).into(new SimpleTarget<Drawable>() {
            @Override
            public void onResourceReady(Drawable drawable, Transition<? super Drawable> transition) {
                //图片过大的话 此处进行压缩
                float bili = drawable.getIntrinsicHeight() * 1.0f / drawable.getIntrinsicWidth();
                int height = (int) (getWidth() * bili);
                /**
                 * 1.getText.toString可以得到图片对应部分会显示
                 * 2.删除图片会删除这一串
                 * 3.做表情的话可以用这个比较方便
                 *
                 */
                String fromat_imgPath = "Custom text messages for ‘getText.toString’";   // edittext内部记录

                SpannableString spannableString = new SpannableString(fromat_imgPath);
                drawable.setBounds(0, 0, getWidth() / 2, height / 2);

                /**
                 * 1.drawable实际显示图片
                 * 2.imgPath是内部记录  span.getSource() 或者转html的时候src可以得到
                 */
                ImageSpan span = new ImageSpan(drawable, imgPath);
                is2ndChanged = true;
                spannableString.setSpan(span, 0, fromat_imgPath.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                // 注释4.1效果前后都不包括  例如word编辑,粗体后面继续打字 还是粗体
                // 注释4.2 但中间是包括的也就是一串粗体中间插入新的文字

                if (clickableSpan != null) {
                    spannableString.setSpan(clickableSpan, 0, fromat_imgPath.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
                int start = getSelectionStart();//
                getText().insert(start, spannableString);
                setMovementMethod(LinkMovementMethod.getInstance());

                //按照需求加一行换行
                setSelection(getText().length());
                getText().append("\n");
                setSelection(getText().length());
                //让点击事件生效


                setFocusable(true);
                setFocusableInTouchMode(true);
                requestFocus();//获取焦点 光标出现
            }


        });

    }
public abstract class IClickableSpan extends ClickableSpan {


    private String data;

    public IClickableSpan(String data) {
        this.data = data;
    }

    @Override
    public void onClick(View widget) {
        onCustomClick(widget, data);
    }

    public abstract void onCustomClick(View widget, String data);

    @Override
    public void updateDrawState(@NonNull TextPaint ds) {
        super.updateDrawState(ds);
        ds.setColor(Color.RED);
        ds.setUnderlineText(false);
        ds.clearShadowLayer();
    }

 

加粗(加前景色 背景色 原理类似)

      addTextChangedListener(new TextWatcher() {
            private CharSequence beforeTextChangedStr = "";
            private CharSequence onTextChangedStr = "";

            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                beforeTextChangedStr = s;
            }


            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                if (is2ndChanged) {//处理文本处理中导致的onTextChanged
                    is2ndChanged = false;
                    return;
                }
                //如果before大于0则代表删除的内容
                if (count > 0) {//count部分即为新增的str 此时已经添加到了et里面
                    CharSequence charSequence = s.subSequence(start, start + count);
                    if (isBold) {//如果加粗模式开启

                        SpannableString spannableString = new SpannableString(charSequence);
                        /**
                         * SPAN_EXCLUSIVE_EXCLUSIVE(前后都不包括)
                         * Spanned.SPAN_INCLUSIVE_INCLUSIVE:在开始或结尾处处插入新内容时,会与原来的SpannableString混合在一起,组成一个新的SpannableString
                         */
                        spannableString.setSpan(new StyleSpan(android.graphics.Typeface.BOLD),
                                0, charSequence.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); //加粗模式下输入的文字 均加粗

                        //后续功能:加个颜色等功能
                        //spannableString.setSpan(new ForegroundColorSpan(Color.BLUE), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

//                        if (clickableSpan != null) {
//                            spannableString.setSpan(clickableSpan, 0, charSequence.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
//                        }


                        int pStart = getSelectionStart();//获取光标位置

                        needChange();//阻止再次onTextChanged

                        getText().delete(pStart - count, pStart);//删除输入未加粗数据

                        needChange();

                        int pStart2 = getSelectionStart();//获取光标位置

                        getText().insert(pStart2, spannableString);//插入加粗后的数据


                    }
                }

                if (before > 0) {//往前删除
                    //系统默认处理不会影响span
                }


            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        });

自定义标签的解析和普通标签数据的获取

java获取html标签数据

  /**
     * 获取指定HTML标签的指定属性的值    * @param source 要匹配的源文本    * @param element 标签名称    * @param attr 标签的属性名称    * @return 属性值列表
     */
    public static List<String> match(String source, String element, String attr) {
        List<String> result = new ArrayList<String>();
        String reg = "<" + element + "[^<>]*?\\s" + attr + "=['\"]?(.*?)['\"]?(\\s.*?)?>";
        Matcher m = Pattern.compile(reg).matcher(source);
        while (m.find()) {
            String r = m.group(1);
            result.add(r);
        }
        return result;
    }

 

package rex.richetlibrary;

import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.Html;
import android.text.Spannable;
import android.text.style.ImageSpan;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import android.widget.Toast;
import org.xml.sax.XMLReader;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by Rex on 2018/5/17.
 * 对html网页标签的自定义处理 
custom_img

* 源码依据 Html 846行 *

package rex.richetlibrary;

import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.Html;
import android.text.Spannable;
import android.text.style.ImageSpan;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import android.widget.Toast;
import org.xml.sax.XMLReader;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by Rex on 2018/5/17.
 * 对html网页标签的自定义处理
 * 源码依据 Html 846行
 */

public class HtmlNewTagHandler implements Html.TagHandler {
    public static final String TAG_NEW_IMG = "custom_img";

    private int startIndex = 0;
    private int stopIndex = 0;
    final HashMap<String, String> attributes = new HashMap<String, String>();

    private Activity mContext;
    private Map<String, Drawable> httpDraws;

    public HtmlNewTagHandler(Activity context, Map<String, Drawable> httpDraws) {
        mContext = context;
        this.httpDraws = httpDraws;
    }

    @Override
    public void handleTag(boolean opening, String tag, Editable mSpannableStringBuilder, XMLReader mReader) {
        processAttributes(mReader);
        //该类型只有start处理
        //<img src="http://www.1honeywan.com/dachshund/image/7.21/7.21_3_thumb.JPG">
        if (tag.equalsIgnoreCase(TAG_NEW_IMG)) {//<>
            if (opening) {
                startHttpImg(mSpannableStringBuilder);
            }
        } else {//</>

        }
    }

    public int getScreenWidth(Context context) {
        WindowManager wm = (WindowManager) context
                .getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(outMetrics);
        return outMetrics.widthPixels;
    }

    public void startHttpImg(Editable text) {
        String src = attributes.get("src");
        String url = src;
        int screenWidth = getScreenWidth(mContext) / 2;//占屏幕一半 根据需求调整

        Drawable d = httpDraws.get(url);
        if (d == null) {
            Toast.makeText(mContext, "图片预加载失败", Toast.LENGTH_SHORT).show();
        }
        float bili = d.getIntrinsicWidth() * 1.0f / d.getIntrinsicHeight();
        int w = screenWidth;
        int h = (int) (screenWidth / bili);
        d.setBounds(0, 0, w, h);
        int len = text.length();
        text.append("\uFFFC");
        text.setSpan(new ImageSpan(d, src), len, text.length(),
                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);


    }

    private void processAttributes(final XMLReader xmlReader) {
        try {
            Field elementField = xmlReader.getClass().getDeclaredField("theNewElement");
            elementField.setAccessible(true);
            Object element = elementField.get(xmlReader);
            Field attsField = element.getClass().getDeclaredField("theAtts");
            attsField.setAccessible(true);
            Object atts = attsField.get(element);
            Field dataField = atts.getClass().getDeclaredField("data");
            dataField.setAccessible(true);
            String[] data = (String[]) dataField.get(atts);
            Field lengthField = atts.getClass().getDeclaredField("length");
            lengthField.setAccessible(true);
            int len = (Integer) lengthField.get(atts);

            for (int i = 0; i < len; i++) {
                attributes.put(data[i * 5 + 1], data[i * 5 + 4]);
            }
        } catch (Exception e) {

        }
    }

}

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值