仿简书APP富文本编辑器实现

最近看到了简书App中的编辑器可以实现字体的加粗,斜体,删除线等多种样式,而且可以插入图片,链接,分割线。支持字符串数据提交服务器,然后在TextView中直接展示。




如果我们没有了解其中原理之前,感觉还是挺高大上的。然后我就打算仿照他写一个类似的给大家分享。


开始我在网上找了一些类似的Demo,发现实现的关键原理是:通过WebView加载Html标签实现效果展示,然后最终获取全部的Html语句提交服务器,然后我们在请求服务器获取Html标签字符串,直接TextView展示。

不过在网上找了很多都最终达不到简书的那种效果,然后我就对部分进行了重写和添加,最终实现了和简书几乎一样的效果。

第一步:自定义WebView并初始Html化标签字符串

    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 OnScrollChangedCallback mOnScrollChangedCallback;

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

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

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

        setVerticalScrollBarEnabled(false);
        setHorizontalScrollBarEnabled(false);
        getSettings().setJavaScriptEnabled(true);
        setWebChromeClient(new WebChromeClient());
        setWebViewClient(createWebviewClient());
        loadUrl(SETUP_HTML);

        applyAttributes(context, attrs);
    }
loadUrl(SETUP_HTML);
我们可以看到加载了一个本地的Html文件
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="user-scalable=no">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <link rel="stylesheet" type="text/css" href="normalize.css">
    <link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div id="editor" contentEditable="true"></div>
<script type="text/javascript" src="rich_editor.js"></script>
</body>
</html>
    <link rel="stylesheet" type="text/css" href="normalize.css">
    <link rel="stylesheet" type="text/css" href="style.css">
<pre name="code" class="html" style="font-size: 13.3333px;">    script type="text/javascript" src="rich_editor.js"></script>
 
 
在Html文件中连接了两个css文件和一个js文件
/**
 * Copyright (C) 2015 Wasabeef
 *
 * 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
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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.
 */

@charset "UTF-8";


html {
  height: 100%;
}

body {
  overflow: scroll;
  display: table;
  table-layout: fixed;
  width: 100%;
  min-height:100%;
}

#editor {
  display: table-cell;

  -webkit-user-select: auto !important;
  -webkit-user-modify: read-write !important;

  outline: 0px solid transparent;
  background-repeat: no-repeat;
  background-position: center;
  background-size: cover;
}

blockquote{
background-color: whitesmoke;
border-left: 4px solid #999999;
font-size: 15px;
font-weight: 100;
padding: 10px 15px;
margin-left: 0px;
margin-right : 0px;
}

#editor[placeholder]:empty:not(:focus):before {
  content: attr(placeholder);
  opacity: .5;
}}
其余两个代码较多就不进行展示了,末尾有下载地址






开始编辑富文本

1,控件使用
 <span style="white-space:pre">	</span><com.niuduz.richeditor_ding.richeditor.RichEditor
                android:id="@+id/editor"
                android:layout_width="match_parent"
                android:layout_height="@dimen/dimen_300dip"
                android:layout_marginLeft="@dimen/dimen_5dip"
                android:layout_marginRight="@dimen/dimen_5dip"
                android:gravity="top|left"
                android:paddingTop="@dimen/dimen_10dip" />

2,添加按钮布局
 <RelativeLayout
        android:id="@+id/rl_layout_editor"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="invisible">

        <View
            android:layout_width="match_parent"
            android:layout_height="@dimen/dimen_1dip"
            android:layout_above="@+id/ll_layout_editor"
            android:background="@color/split_line_color" />


        <LinearLayout
            android:id="@+id/ll_layout_editor"
            android:layout_width="match_parent"
            android:layout_height="@dimen/dimen_36dip"
            android:layout_alignParentBottom="true"
            android:background="@color/white"
            android:orientation="horizontal">

            <ImageButton
                android:id="@+id/action_undo"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:background="@null"
                android:contentDescription="@null"
                android:src="@mipmap/undo" />

            <ImageButton
                android:id="@+id/action_redo"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:background="@null"
                android:contentDescription="@null"
                android:src="@mipmap/redo" />

            <ImageButton
                android:id="@+id/action_font"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:background="@null"
                android:contentDescription="@null"
                android:src="@mipmap/font" />

            <ImageButton
                android:id="@+id/action_add"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:background="@null"
                android:contentDescription="@null"
                android:src="@mipmap/add" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/ll_layout_font"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_above="@+id/ll_layout_editor"
            android:layout_alignParentEnd="true"
            android:layout_marginBottom="-18dp"
            android:layout_marginRight="-5dp"
            android:background="@drawable/richfont_bg"
            android:gravity="center"
            android:orientation="horizontal"
            android:visibility="gone">

            <ImageButton
                android:id="@+id/action_bold"
                android:layout_width="@dimen/dimen_36dip"
                android:layout_height="@dimen/dimen_36dip"
                android:background="@null"
                android:contentDescription="@null"
                android:src="@mipmap/bold_d" />

            <ImageButton
                android:id="@+id/action_italic"
                android:layout_width="@dimen/dimen_36dip"
                android:layout_height="@dimen/dimen_36dip"
                android:background="@null"
                android:contentDescription="@null"
                android:src="@mipmap/italic_d" />


            <ImageButton
                android:id="@+id/action_strikethrough"
                android:layout_width="@dimen/dimen_36dip"
                android:layout_height="@dimen/dimen_36dip"
                android:background="@null"
                android:contentDescription="@null"
                android:src="@mipmap/strikethrough_d" />

            <ImageButton
                android:id="@+id/action_blockquote"
                android:layout_width="@dimen/dimen_36dip"
                android:layout_height="@dimen/dimen_36dip"
                android:background="@null"
                android:contentDescription="@null"
                android:src="@mipmap/blockquote_d" />

            <ImageButton
                android:id="@+id/action_heading1"
                android:layout_width="@dimen/dimen_36dip"
                android:layout_height="@dimen/dimen_36dip"
                android:background="@null"
                android:contentDescription="@null"
                android:src="@mipmap/h1_d" />

            <ImageButton
                android:id="@+id/action_heading2"
                android:layout_width="@dimen/dimen_36dip"
                android:layout_height="@dimen/dimen_36dip"
                android:background="@null"
                android:contentDescription="@null"
                android:src="@mipmap/h2_d" />

            <ImageButton
                android:id="@+id/action_heading3"
                android:layout_width="@dimen/dimen_36dip"
                android:layout_height="@dimen/dimen_36dip"
                android:background="@null"
                android:contentDescription="@null"
                android:src="@mipmap/h3_d" />

            <ImageButton
                android:id="@+id/action_heading4"
                android:layout_width="@dimen/dimen_36dip"
                android:layout_height="@dimen/dimen_36dip"
                android:background="@null"
                android:contentDescription="@null"
                android:src="@mipmap/h4_d" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/ll_layout_add"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_above="@+id/ll_layout_editor"
            android:layout_alignParentEnd="true"
            android:layout_marginBottom="-18dp"
            android:layout_marginRight="@dimen/dimen_12dip"
            android:background="@drawable/richadd_bg"
            android:gravity="center"
            android:orientation="horizontal"
            android:paddingLeft="@dimen/dimen_20dip"
            android:paddingRight="@dimen/dimen_20dip"
            android:visibility="gone">

            <ImageButton
                android:id="@+id/action_image"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:background="@null"
                android:contentDescription="@null"
                android:paddingRight="@dimen/dimen_10dip"
                android:src="@mipmap/insert_image" />

            <ImageButton
                android:id="@+id/action_link"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:background="@null"
                android:contentDescription="@null"
                android:paddingLeft="@dimen/dimen_10dip"
                android:paddingRight="@dimen/dimen_10dip"
                android:src="@mipmap/insert_link" />


            <ImageButton
                android:id="@+id/action_split"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:background="@null"
                android:contentDescription="@null"
                android:paddingLeft="@dimen/dimen_10dip"
                android:src="@mipmap/insert_split" />
        </LinearLayout>
    </RelativeLayout>



· 3.注册RichEditor和各个按钮相关事件

 action_add.setOnClickListener(this);
        action_font.setOnClickListener(this);
        action_redo.setOnClickListener(this);
        action_undo.setOnClickListener(this);

        ib_Bold.setOnClickListener(this);
        ib_Italic.setOnClickListener(this);
        ib_StrikeThough.setOnClickListener(this);
        ib_BlockQuote.setOnClickListener(this);
        ib_H1.setOnClickListener(this);
        ib_H2.setOnClickListener(this);
        ib_H3.setOnClickListener(this);
        ib_H4.setOnClickListener(this);
        

        mEditor.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                if (hasFocus) {
                    imm.toggleSoftInput(0, InputMethodManager.SHOW_FORCED);
                    rl_layout_editor.setVisibility(View.VISIBLE);
//                    clickableType = 1;
                } else {
                    imm.hideSoftInputFromWindow(mEditor.getWindowToken(), 0); //强制隐藏键盘
                    rl_layout_editor.setVisibility(View.INVISIBLE);
                }
            }
        });


        /**
         *获取点击出文本的标签类型
         */
        mEditor.setOnDecorationChangeListener(new RichEditor.OnDecorationStateListener() {
            @Override
            public void onStateChangeListener(String text, List<RichEditor.Type> types) {

                if (types.contains(RichEditor.Type.BOLD)) {
                    ib_Bold.setImageResource(R.mipmap.bold_l);
                    flag1 = true;
                    isBold = true;
                } else {
                    ib_Bold.setImageResource(R.mipmap.bold_d);
                    flag1 = false;
                    isBold = false;
                }

                if (types.contains(RichEditor.Type.ITALIC)) {
                    ib_Italic.setImageResource(R.mipmap.italic_l);
                    flag2 = true;
                    isItalic = true;
                } else {
                    ib_Italic.setImageResource(R.mipmap.italic_d);
                    flag2 = false;
                    isItalic = false;
                }

                if (types.contains(RichEditor.Type.STRIKETHROUGH)) {
                    ib_StrikeThough.setImageResource(R.mipmap.strikethrough_l);
                    flag3 = true;
                    isStrikeThrough = true;
                } else {
                    ib_StrikeThough.setImageResource(R.mipmap.strikethrough_d);
                    flag3 = false;
                    isStrikeThrough = false;
                }

                //块引用
                if (types.contains(RichEditor.Type.BLOCKQUOTE)) {
                    flag4 = true;
                    flag5 = false;
                    flag6 = false;
                    flag7 = false;
                    flag8 = false;
                    isclick = true;
                    ib_BlockQuote.setImageResource(R.mipmap.blockquote_l);
                    ib_H1.setImageResource(R.mipmap.h1_d);
                    ib_H2.setImageResource(R.mipmap.h2_d);
                    ib_H3.setImageResource(R.mipmap.h3_d);
                    ib_H4.setImageResource(R.mipmap.h4_d);
                } else {
                    ib_BlockQuote.setImageResource(R.mipmap.blockquote_d);
                    flag4 = false;
                    isclick = false;
                }


                if (types.contains(RichEditor.Type.H1)) {
                    flag4 = false;
                    flag5 = true;
                    flag6 = false;
                    flag7 = false;
                    flag8 = false;

                    isclick = true;
                    ib_BlockQuote.setImageResource(R.mipmap.blockquote_d);
                    ib_H1.setImageResource(R.mipmap.h1_l);
                    ib_H2.setImageResource(R.mipmap.h2_d);
                    ib_H3.setImageResource(R.mipmap.h3_d);
                    ib_H4.setImageResource(R.mipmap.h4_d);
                } else {
                    ib_H1.setImageResource(R.mipmap.h1_d);
                    flag5 = false;
                    isclick = false;
                }

                if (types.contains(RichEditor.Type.H2)) {
                    flag4 = false;
                    flag5 = false;
                    flag6 = true;
                    flag7 = false;
                    flag8 = false;

                    isclick = true;
                    ib_BlockQuote.setImageResource(R.mipmap.blockquote_d);
                    ib_H1.setImageResource(R.mipmap.h1_d);
                    ib_H2.setImageResource(R.mipmap.h2_l);
                    ib_H3.setImageResource(R.mipmap.h3_d);
                    ib_H4.setImageResource(R.mipmap.h4_d);
                } else {
                    ib_H2.setImageResource(R.mipmap.h2_d);
                    flag6 = false;
                    isclick = false;
                }

                if (types.contains(RichEditor.Type.H3)) {
                    flag4 = false;
                    flag5 = false;
                    flag6 = false;
                    flag7 = true;
                    flag8 = false;
                    isclick = true;
                    ib_BlockQuote.setImageResource(R.mipmap.blockquote_d);
                    ib_H1.setImageResource(R.mipmap.h1_d);
                    ib_H2.setImageResource(R.mipmap.h2_d);
                    ib_H3.setImageResource(R.mipmap.h3_l);
                    ib_H4.setImageResource(R.mipmap.h4_d);
                } else {
                    ib_H4.setImageResource(R.mipmap.h3_d);
                    flag7 = false;
                    isclick = false;
                }

                if (types.contains(RichEditor.Type.H4)) {
                    flag4 = false;
                    flag5 = false;
                    flag6 = false;
                    flag7 = false;
                    flag8 = true;
                    isclick = true;
                    ib_BlockQuote.setImageResource(R.mipmap.blockquote_d);
                    ib_H1.setImageResource(R.mipmap.h1_d);
                    ib_H2.setImageResource(R.mipmap.h2_d);
                    ib_H3.setImageResource(R.mipmap.h3_d);
                    ib_H4.setImageResource(R.mipmap.h4_l);
                } else {
                    ib_H4.setImageResource(R.mipmap.h4_d);
                    flag8 = false;
                    isclick = false;
                }

然后在事件监听中,进行相关处理,这其中通常是对其他按钮作用的效果的添加和移除。这个是富文本中处理最麻烦的
因为WebView对标签的包裹并非统一实现了,基本原则:把出现标签的效果按钮就变亮;把没有出现标签效果的按钮变灰。
最后效果:



遗留问题

1, 在模拟器上撤销和返回两个按钮好像有问题,在真机上完全没事!

2,在各个事件监听逻辑中,为了添加和消除其他按钮的影响时,产生了大量的重复代码,虽然大致相同,但还是存在区别,所以感觉抽取也不是,不抽取也不是。这方便有待优化,也请在有好的处理方法的话多多指出!


猜你喜欢

转载自blog.csdn.net/ding_gc/article/details/52839316