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