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