Android用过觉得最满意的富文本编辑器(自定义方便),基于webview实现

最要类代码:
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import com.urun.media.util.Utils;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/**

  • Copyright (C) 2017 Wasabeef
  • <p>
  • Licensed under the Apache License, Version 2.0 (the "License");
  • you may not use this file except in compliance with the License.
  • You may obtain a copy of the License at
  • <p>
  • http://www.apache.org/licenses/LICENSE-2.0
  • <p>
  • Unless required by applicable law or agreed to in writing, software
  • distributed under the License is distributed on an "AS IS" BASIS,
  • WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  • See the License for the specific language governing permissions and
  • limitations under the License.
    */

public class RichEditor extends WebView {

public enum Type {
    BOLD,
    ITALIC,
    SUBSCRIPT,
    SUPERSCRIPT,
    STRIKETHROUGH,
    UNDERLINE,
    H1,
    H2,
    H3,
    H4,
    H5,
    H6,
    ORDEREDLIST,
    UNORDEREDLIST,
    JUSTIFYCENTER,
    JUSTIFYFULL,
    JUSTUFYLEFT,
    JUSTIFYRIGHT
}

public interface OnTextChangeListener {
    void onTextChange(String text);
}

public interface OnDecorationStateListener {
    void onStateChangeListener(String text, List<RichEditor.Type> types);
}

public interface AfterInitialLoadListener {
    void onAfterInitialLoad(boolean isReady);
}

private static final String SETUP_HTML = "file:///android_asset/editor.html";
private static final String CALLBACK_SCHEME = "re-callback://";
private static final String STATE_SCHEME = "re-state://";
private boolean isReady = false;
private String mContents;
private OnTextChangeListener mTextChangeListener;
private OnDecorationStateListener mDecorationStateListener;
private AfterInitialLoadListener mLoadListener;
private Context mContext;

public RichEditor(Context context) {
    this(context, null);
    mContext = context;
}

public RichEditor(Context context, AttributeSet attrs) {
    this(context, attrs, android.R.attr.webViewStyle);
    mContext = context;
}

@SuppressLint("SetJavaScriptEnabled")
public RichEditor(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    setVerticalScrollBarEnabled(false);
    setHorizontalScrollBarEnabled(false);
    getSettings().setJavaScriptEnabled(true);
    getSettings().setDomStorageEnabled(true);
    setWebChromeClient(createWebChromeClient());
    setWebViewClient(createWebviewClient());
    loadUrl(SETUP_HTML);

    applyAttributes(context, attrs);
}

protected EditorWebViewClient createWebviewClient() {
    return new EditorWebViewClient();
}

protected EditorWebChromeClient createWebChromeClient() {
    return new EditorWebChromeClient();
}

public void clearCookie() {
    CookieSyncManager.createInstance(mContext);
    CookieManager cookieManager = CookieManager.getInstance();
    cookieManager.removeAllCookie();
    CookieSyncManager.getInstance().sync();

    setWebChromeClient(null);
    setWebViewClient(null);
    getSettings().setJavaScriptEnabled(false);
    clearCache(true);
}

public void setOnTextChangeListener(OnTextChangeListener listener) {
    mTextChangeListener = listener;
}

public void setOnDecorationChangeListener(OnDecorationStateListener listener) {
    mDecorationStateListener = listener;
}

public void setOnInitialLoadListener(AfterInitialLoadListener listener) {
    mLoadListener = listener;
}

private void callback(String text) {
    mContents = text.replaceFirst(CALLBACK_SCHEME, "");
    if (mTextChangeListener != null) {
        mTextChangeListener.onTextChange(mContents);
    }
}

private void stateCheck(String text) {
    String state = text.replaceFirst(STATE_SCHEME, "").toUpperCase(Locale.ENGLISH);
    List<RichEditor.Type> types = new ArrayList<>();
    for (RichEditor.Type type : RichEditor.Type.values()) {
        if (TextUtils.indexOf(state, type.name()) != -1) {
            types.add(type);
        }
    }

    if (mDecorationStateListener != null) {
        mDecorationStateListener.onStateChangeListener(state, types);
    }
}

private void applyAttributes(Context context, AttributeSet attrs) {
    final int[] attrsArray = new int[]{
            android.R.attr.gravity
    };
    TypedArray ta = context.obtainStyledAttributes(attrs, attrsArray);

    int gravity = ta.getInt(0, NO_ID);
    switch (gravity) {
        case Gravity.LEFT:
            exec("javascript:RE.setTextAlign(\"left\")");
            break;
        case Gravity.RIGHT:
            exec("javascript:RE.setTextAlign(\"right\")");
            break;
        case Gravity.TOP:
            exec("javascript:RE.setVerticalAlign(\"top\")");
            break;
        case Gravity.BOTTOM:
            exec("javascript:RE.setVerticalAlign(\"bottom\")");
            break;
        case Gravity.CENTER_VERTICAL:
            exec("javascript:RE.setVerticalAlign(\"middle\")");
            break;
        case Gravity.CENTER_HORIZONTAL:
            exec("javascript:RE.setTextAlign(\"center\")");
            break;
        case Gravity.CENTER:
            exec("javascript:RE.setVerticalAlign(\"middle\")");
            exec("javascript:RE.setTextAlign(\"center\")");
            break;
    }

    ta.recycle();
}

public void setHtml(String contents) {
    if (contents == null) {
        contents = "";
    }
    try {
        exec("javascript:RE.setHtml('" + URLEncoder.encode(contents, "UTF-8") + "');");
    } catch (UnsupportedEncodingException e) {
        // No handling
    }
    mContents = contents;
}

public String getHtml() {
    return mContents;
}

public void setEditorFontColor(int color) {
    String hex = convertHexColorString(color);
    exec("javascript:RE.setBaseTextColor('" + hex + "');");
}

public void setEditorFontSize(int px) {
    exec("javascript:RE.setBaseFontSize('" + px + "px');");
}

@Override
public void setPadding(int left, int top, int right, int bottom) {
    super.setPadding(left, top, right, bottom);
    exec("javascript:RE.setPadding('" + left + "px', '" + top + "px', '" + right + "px', '" + bottom
            + "px');");
}

@Override
public void setPaddingRelative(int start, int top, int end, int bottom) {
    // still not support RTL.
    setPadding(start, top, end, bottom);
}

public void setEditorBackgroundColor(int color) {
    setBackgroundColor(color);
}

@Override
public void setBackgroundColor(int color) {
    super.setBackgroundColor(color);
}

@Override
public void setBackgroundResource(int resid) {
    Bitmap bitmap = Utils.decodeResource(getContext(), resid);
    String base64 = Utils.toBase64(bitmap);
    bitmap.recycle();

    exec("javascript:RE.setBackgroundImage('url(data:image/png;base64," + base64 + ")');");
}

@Override
public void setBackground(Drawable background) {
    Bitmap bitmap = Utils.toBitmap(background);
    String base64 = Utils.toBase64(bitmap);
    bitmap.recycle();

    exec("javascript:RE.setBackgroundImage('url(data:image/png;base64," + base64 + ")');");
}

public void setBackground(String url) {
    exec("javascript:RE.setBackgroundImage('url(" + url + ")');");
}

public void setEditorWidth(int px) {
    exec("javascript:RE.setWidth('" + px + "px');");
}

public void setEditorHeight(int px) {
    exec("javascript:RE.setHeight('" + px + "px');");
}

public void setPlaceholder(String placeholder) {
    exec("javascript:RE.setPlaceholder('" + placeholder + "');");
}

public void setInputEnabled(Boolean inputEnabled) {
    exec("javascript:RE.setInputEnabled(" + inputEnabled + ")");
}

public void loadCSS(String cssFile) {
    String jsCSSImport = "(function() {" +
            "    var head  = document.getElementsByTagName(\"head\")[0];" +
            "    var link  = document.createElement(\"link\");" +
            "    link.rel  = \"stylesheet\";" +
            "    link.type = \"text/css\";" +
            "    link.href = \"" + cssFile + "\";" +
            "    link.media = \"all\";" +
            "    head.appendChild(link);" +
            "}) ();";
    exec("javascript:" + jsCSSImport + "");
}

public void undo() {
    exec("javascript:RE.undo();");
}

public void redo() {
    exec("javascript:RE.redo();");
}

public void setBold() {
    exec("javascript:RE.setBold();");
}

public void setItalic() {
    exec("javascript:RE.setItalic();");
}

public void setSubscript() {
    exec("javascript:RE.setSubscript();");
}

public void setSuperscript() {
    exec("javascript:RE.setSuperscript();");
}

public void setStrikeThrough() {
    exec("javascript:RE.setStrikeThrough();");
}

public void setUnderline() {
    exec("javascript:RE.setUnderline();");
}

public void setTextColor(int color) {
    exec("javascript:RE.prepareInsert();");

    String hex = convertHexColorString(color);
    exec("javascript:RE.setTextColor('" + hex + "');");
}

public void setTextBackgroundColor(int color) {
    exec("javascript:RE.prepareInsert();");

    String hex = convertHexColorString(color);
    exec("javascript:RE.setTextBackgroundColor('" + hex + "');");
}

public void setFontSize(int fontSize) {
    if (fontSize > 7 || fontSize < 1) {
        Log.e("RichEditor", "Font size should have a value between 1-7");
    }
    exec("javascript:RE.setFontSize('" + fontSize + "');");
}

public void removeFormat() {
    exec("javascript:RE.removeFormat();");
}

public void setHeading(int heading) {
    exec("javascript:RE.setHeading('" + heading + "');");
}

public void setIndent() {
    exec("javascript:RE.setIndent();");
}

public void setOutdent() {
    exec("javascript:RE.setOutdent();");
}

public void setAlignLeft() {
    exec("javascript:RE.setJustifyLeft();");
}

public void setAlignCenter() {
    exec("javascript:RE.setJustifyCenter();");
}

public void setAlignRight() {
    exec("javascript:RE.setJustifyRight();");
}

public void setBlockquote() {
    exec("javascript:RE.setBlockquote();");
}

public void setBullets() {
    exec("javascript:RE.setBullets();");
}

public void setNumbers() {
    exec("javascript:RE.setNumbers();");
}

public void insertImage(String url, String alt) {
    exec("javascript:RE.prepareInsert();");
    exec("javascript:RE.insertImage('" + url + "', '" + alt + "');");
}

public void insertLink(String href, String title) {
    exec("javascript:RE.prepareInsert();");
    exec("javascript:RE.insertLink('" + href + "', '" + title + "');");
}

public void insertTodo() {
    exec("javascript:RE.prepareInsert();");
    exec("javascript:RE.setTodo('" + Utils.getCurrentTime() + "');");
}

public void focusEditor() {
    requestFocus();
    exec("javascript:RE.focus();");
}

public void clearFocusEditor() {
    exec("javascript:RE.blurFocus();");
}

private String convertHexColorString(int color) {
    return String.format("#%06X", (0xFFFFFF & color));
}

protected void exec(final String trigger) {
    if (isReady) {
        load(trigger);
    } else {
        postDelayed(new Runnable() {
            @Override
            public void run() {
                exec(trigger);
            }
        }, 200);
    }
}

private void load(String trigger) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        evaluateJavascript(trigger, null);
    } else {
        loadUrl(trigger);
    }
}

protected class EditorWebChromeClient extends WebChromeClient {

    @Override
    public void onProgressChanged(WebView view, int newProgress) {

// if (newProgress == 100) {
// if (mLoadListener != null) {
// mLoadListener.onAfterInitialLoad(isReady);
// }
// }
}
}

protected class EditorWebViewClient extends WebViewClient {

    @Override
    public void onPageFinished(WebView view, String url) {
        isReady = url.equalsIgnoreCase(SETUP_HTML);
        if (mLoadListener != null) {
            mLoadListener.onAfterInitialLoad(isReady);
        }
    }

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        String decode;
        try {
            decode = URLDecoder.decode(url, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            // No handling
            return false;
        }

        if (TextUtils.indexOf(url, CALLBACK_SCHEME) == 0) {
            callback(decode);
            return true;
        } else if (TextUtils.indexOf(url, STATE_SCHEME) == 0) {
            stateCheck(decode);
            return true;
        }

        return super.shouldOverrideUrlLoading(view, url);
    }
}

}

别忘了assets四个文件:editor.html ,normalize.css,rich_editor.js,style.css
demo github地址:https://github.com/wasabeef/richeditor-android

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容