Android 图文混排TextView和EditText,EditText可以撤销和重做

我们在做项目时,可能会用到图文混排功能,例如做日记app,github上有很多RichText(富文本编辑器),但是看来看去都不能满足自己的要求,因为图片地址也要随着文本一起上传到服务器,以便于下次展示,用户使用时可以用TextView浏览,也可以使用EditText编辑。

下面就说说我的实现方法:

主要使用到Android SDK带的Html类的两个方法

public static android.text.Spanned fromHtml(java.lang.String source,
                                            Html.ImageGetter imageGetter,
                                            Html.TagHandler tagHandler)

public static java.lang.String toHtml(android.text.Spanned text)

fromHtml是将图文混排文本(也就是带有html标签的文本)转为TextView和EditText可以识别的图文用于展示,toHtml是将TextView和EditText编辑好的图文转为图文混排文本,再进行保存等下一步操作。两个方法参数具体含义不明白的自己去了解。

例如这样一个文本

"您消耗的总热量约等于4杯" + "<img src='http://calendar.bj.bcebos.com/img/icon_shwnl.png'/>"
        + "\n+5只" + "<img src='" + R.mipmap.ic_launcher
        + "'/>" + "+10个" + "<img src='"
        + R.mipmap.ic_launcher + "'/>"
这里的文本包括图片内容都是用img标签来包含,上面的fromHtml方法中的ImageGetter参数,是一个接口,实现这个接口后,需要实现它唯一的方法

public Drawable getDrawable(String source),source参数就是每个img标签src里面的内容,可以根据source中的内容来分别处理不同情况。

下面附上ImageGetter的代码

ImageGetter.java

package com.example.zhouwenpeng.myapplication.widgets;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.text.Html;
import android.text.Spanned;
import android.view.View;
import android.widget.TextView;

import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import com.nostra13.universalimageloader.utils.MemoryCacheUtils;

import java.io.File;
import java.util.List;

/**
 * Created by 周文鹏 on 16/7/11.
 *
 */
public class ImageGetter implements Html.ImageGetter {

    private TextView textView;
    private Context context;
    private ImageLoader imageLoader;

    public ImageGetter(TextView textView) {
        this.textView = textView;
        this.context = textView.getContext();

        imageLoader = ImageLoader.getInstance();
        if (!imageLoader.isInited()) {
            DisplayImageOptions opts = new DisplayImageOptions.Builder().cacheOnDisk(true).cacheInMemory(true).build();
            ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
                    .defaultDisplayImageOptions(opts).build();
            imageLoader.init(config);
        }
    }

    @Override
    public Drawable getDrawable(String source) {
        Drawable drawable = null;
        if (source.startsWith("http")) {
            List<Bitmap> bitmaps = MemoryCacheUtils.findCachedBitmapsForImageUri(source, imageLoader.getMemoryCache());
            if (bitmaps.size() > 0) {
                Bitmap bitmap = bitmaps.get(0);
                drawable = new BitmapDrawable(context.getResources(), bitmap);
            } else {
                File file = imageLoader.getDiskCache().get(source);
                if (file != null) {
                    drawable = Drawable.createFromPath(file.getAbsolutePath());
                }
            }
            if (drawable == null) {
                imageLoader.loadImage(source, new ImageLoadingListener() {
                    @Override
                    public void onLoadingStarted(String imageUri, View view) {
                    }

                    @Override
                    public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
                    }

                    @Override
                    public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
                        CharSequence text = textView.getText();
                        if (text instanceof Spanned) {
                            String html = Html.toHtml((Spanned) text);
                            textView.setText(html);
                        } else {
                            textView.setText(text);
                        }

                    }

                    @Override
                    public void onLoadingCancelled(String imageUri, View view) {
                    }
                });
            }
        } else if (isNumeric(source)) {
            int id = Integer.parseInt(source);
            drawable = context.getResources().getDrawable(id);
        } else {
            drawable = Drawable.createFromPath(source);
        }

        if (drawable != null) {
            drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
        }
        return drawable;
    }

    // 加入ZPUtil
    public static boolean isNumeric(String str) {
        for (int i = str.length(); --i >= 0; ) {
            if (!Character.isDigit(str.charAt(i))) {
                return false;
            }
        }
        return true;
    }
}

这里主要有三种情况,图片是http网络上的图片,图片是本地文件中的图片,和图片是项目中的资源文件。这里用到了UniversalImageLoader这个库,是github的开源库,需要自行导入。

ImageGetter只是个工具,主要给TextView和EditText使用,下面附上TextView和EditText代码


ZPMixedTextView.java

package com.example.zhouwenpeng.myapplication.widgets;

import android.content.Context;
import android.text.Html;
import android.text.Spanned;
import android.util.AttributeSet;
import android.widget.TextView;

/**
 * Created by 周文鹏 on 16/7/11.
 * 图文混排
 */
public class ZPMixedTextView extends TextView {

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

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

    public ZPMixedTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public void setText(CharSequence text, BufferType type) {
        if (text instanceof Spanned) {
            super.setText(text, type);
        } else {
            Spanned spanned = Html.fromHtml(text.toString(), new ImageGetter(this), null);
            super.setText(spanned, type);
        }
    }

    public CharSequence getText2() {
        CharSequence text = super.getText();
        if (text instanceof Spanned) {
            String html = Html.toHtml((Spanned) text);
            html = html.replaceFirst("<p dir=\"ltr\">", "");
            html = replaceLast(html, "</p>", "");
            return html;
        } else {
            return text;
        }
    }

    @Override
    public void append(CharSequence text, int start, int end) {
        if (text instanceof Spanned) {
            super.append(text, start, end);
        } else {
            Spanned spanned = Html.fromHtml(text.toString(), new ImageGetter(this), null);
            super.append(spanned, start, spanned.length());
        }
    }

    // 放在ZPUtil中
    public static String replaceLast(String text, String regex, String replacement) {
        return text.replaceFirst("(?s)" + regex + "(?!.*?" + regex + ")", replacement);
    }
}


ZPComplexEditText.java
package com.example.zhouwenpeng.myapplication.widgets;

import android.content.Context;
import android.text.Editable;
import android.text.Html;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.widget.EditText;

import java.util.ArrayList;
import java.util.List;

/**
 * 记忆+图文混排EditText
 * @author 周文鹏
 */
public class ZPComplexEditText extends EditText {
    private List<EditHistory> undoHistories; // 撤销文字
    private List<EditHistory> redoHistories; // 重做文字
    private boolean activate = false; // 是否激活redo
    private boolean clear = true; // 是否清空redo
    private OnTextChangeListener listener; // 文字改变监听器

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

    public ZPComplexEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public ZPComplexEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    /**
     * 初始化
     */
    private void init() {
        undoHistories = new ArrayList<>();
        redoHistories = new ArrayList<>();
        addTextChangedListener(new TextWatcher() {
            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                if (listener != null) {
                    listener.onTextChange(ZPComplexEditText.this, s);
                }
            }

            /**
             *
             * @param s 改变之前的字符串
             * @param start 改变的起始位置
             * @param count 按键盘back键时删除的长度
             * @param after 增加内容时增加的长度
             */
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count,
                                          int after) {
                if (activate) {
                    redoHistories.add(new EditHistory(new SpannableStringBuilder(s), start, count, after));
                    activate = false;
                } else {
                    // count和after同时大于0只有输入英文带下划线才会有这种情况,相当于先back,再增加
                    if (count > 0 && after > 0) {
                        int position = undoHistories.size() - 1;
                        if (position >= 0 && undoHistories.get(position).start == start) {
                            undoHistories.remove(position);
                        }
                        count = 0;
                    }
                    undoHistories.add(new EditHistory(new SpannableStringBuilder(s), start, count, after));

                    if (clear) {
                        redoHistories.clear();
                    }
                    clear = true;
                }
            }

            @Override
            public void afterTextChanged(Editable s) {
            }
        });
    }

    /**
     * 撤销
     */
    public void undo() {
        if (canUndo()) {
            activate = true;
            Editable editable = getEditableText();
            EditHistory history = undoHistories.remove(undoHistories.size() - 1);
            if (history.count > 0) {
                // 使用insert解决在中间删除后,使用undo会追加到末尾的问题,下同
//                editable.append(history.ssb.subSequence(history.start, history.start + history.count));
                editable.insert(history.start, history.ssb.subSequence(history.start, history.start + history.count));
            }
            if (history.after > 0) {
                editable.delete(history.start, history.start + history.after);
            }
        }
    }

    /**
     * 重做
     */
    public void redo() {
        if (canRedo()) {
            clear = false;
            Editable editable = getEditableText();
            EditHistory history = redoHistories.remove(redoHistories.size() - 1);
            if (history.count > 0) {
//                editable.append(history.ssb.subSequence(history.start, history.start + history.count));
                editable.insert(history.start, history.ssb.subSequence(history.start, history.start + history.count));
            }
            if (history.after > 0) {
                editable.delete(history.start, history.start + history.after);
            }
        }
    }

    /**
     * 是否可以撤销
     */
    public boolean canUndo() {
        return undoHistories.size() > 0;
    }

    /**
     * 是否可以重做
     */
    public boolean canRedo() {
        return redoHistories.size() > 0;
    }

    /**
     * 获取文字改变监听器
     */
    public OnTextChangeListener getOnTextChangeListener() {
        return listener;
    }

    /**
     * 设置文字改变监听器
     */
    public void setOnTextChangeListener(OnTextChangeListener listener) {
        this.listener = listener;
    }

    /**
     * 存储Edit操作
     * @author 周文鹏
     */
    private class EditHistory {
        public SpannableStringBuilder ssb;
        public int start;
        public int count;
        public int after;

        public EditHistory(SpannableStringBuilder ssb, int start, int count, int after) {
            this.ssb = ssb;
            this.start = start;
            this.count = count;
            this.after = after;
        }

        @Override
        public String toString() {
            return "EditHistory [text=" + ssb + ", start=" + start
                    + ", count=" + count + ", after=" + after + "]";
        }
    }

    /**
     * 文字改变监听器
     * @author 周文鹏
     */
    public interface OnTextChangeListener {
        void onTextChange(ZPComplexEditText editText, CharSequence text);
    }

    // 以下是图文混排
    @Override
    public void setText(CharSequence text, BufferType type) {
        if (text instanceof Spanned) {
            super.setText(text, type);
        } else {
            Spanned spanned = Html.fromHtml(text.toString(), new ImageGetter(this), null);
            super.setText(spanned, type);
        }
    }

    public CharSequence getText2() {
        String html = Html.toHtml(super.getText());
        html = html.replaceFirst("<p dir=\"ltr\">", "");
        html = replaceLast(html, "</p>", "");
        return html;
    }

    @Override
    public void append(CharSequence text, int start, int end) {
        if (text instanceof Spanned) {
            super.append(text, start, end);
        } else {
            Spanned spanned = Html.fromHtml(text.toString(), new ImageGetter(this), null);
            super.append(spanned, start, spanned.length());
        }
    }

    // 放在ZPUtil中
    public static String replaceLast(String text, String regex, String replacement) {
        return text.replaceFirst("(?s)" + regex + "(?!.*?" + regex + ")", replacement);
    }
}

上面两个类,EditText明显比TextView代码多很多,是因为EditText增加了undo,redo功能,也就是撤销,重做功能,图文混排主要就是重写了setText和append函数,而getText函数本来也可以重写,但是我在调试时发现,不管是TextView还是EditText在进行文本处理时都频繁调用自己的getText方法,为了性能也为了避免不必要的bug,所以就自己重新写了个getText2,这个方法只有一个用途,就是获取编辑好的带有html标签图文混排文本,注意这里我去掉了最外层的标签,否则下次使用时,如果添加内容会自动带有换行。

getText2获取的文本是这样的

"&#24744;&#28040;&#32791;&#30340;&#24635;&#28909;&#37327;&#32422;&#31561;&#20110;4&#26479;<img src=\"http://calendar.bj.bcebos.com/img/icon_shwnl.png\"> +5&#21482;<img src=\"2130903040\">+10&#20010;<img src=\"2130903040\">"

中文已经被转为unicode编码,这个不用管,下次直接用Html.fromHtml转换,显示出来的还是一样的,当然你也可以自己转成中文。


  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值