Android完美实现在笔记中插入方程公式

Android完美实现在笔记中插入方程公式

历经了以下几个开发阶段

  1. iOS Pages插入方程功能分析
  2. Android插入方程技术方案分析
  3. Android实现方程编辑器
  4. Android完美解决LaTeX中文显示问题
  5. Android判断LaTeX是否为空方程
  6. Android完美支持MathML显示公式方程

终于实现了Android最强大的方程公式编辑器,接下来要做的事情就是整合到『神马笔记』中,以实现在笔记中插入方程公式。

一、目标

方程编辑器整合到『神马笔记』中,实现在笔记中插入方程公式。

二、实现过程

方程公式有2种形式,行内及行间。

神马笔记』采用RecyclerView作为笔记容器,因此只支持行间公式。

行内公式可以将文本放到公式内实现。

1. 定义存储数据

FormulaEntry的关键属性。

  • formula——用户输入的原始数据,LaTeX或者MathML
  • latex——转化后的latex文本,以避免在显示过程中转换用户输入的MathML文本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class FormulaEntry extends ParagraphEntry {

public static final String TYPE = "formula";

@SerializedName("formula")
String formula;

@SerializedName("latex")
String latex;

public FormulaEntry(String id, String formula, String latex) {
super(id);
this.type = TYPE;

this.formula = formula;
this.latex = latex;
}

public String getFormula() {
return formula;
}

public void setFormula(String formula) {
this.formula = formula;
}

public String getLatex() {
return latex;
}

public void setLatex(String latex) {
this.latex = latex;
}
}

2. 定义逻辑数据

FormulaEntity没有添加新的属性和方法,不过对FormulaEntry进行一次包装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class FormulaEntity extends ParagraphEntity<FormulaEntry> {

public FormulaEntity(Document d, FormulaEntry entry) {
super(d, entry);
}

@Override
void save() {
super.save();
}

public String getFormula() {
return entry.getFormula();
}

public void setFormula(String formula) {
entry.setFormula(formula);
}

public String getLatex() {
return entry.getLatex();
}

public void setLatex(String latex) {
entry.setLatex(latex);
}

public static final FormulaEntity create(Document d, String formula, String latex) {

String id = UUIDUtils.next();
FormulaEntry entry = new FormulaEntry(id, formula, latex);

FormulaEntity entity = new FormulaEntity(d, entry);

return entity;
}
}

3. 定义交互数据

FormulaViewHolder拷贝自PictureViewHolder,有不少冗余代码。

FormulaViewHolder实现以下几个功能。

  1. 显示方程内容
  2. 处理用户点击操作,启动方程编辑器
  3. 处理用户长按操作,删除编辑描述
  4. 给方程添加描述内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
public class FormulaViewHolder extends ComposeViewHolder<FormulaEntity> {

public static final int LAYOUT_RES_ID = R.layout.layout_compose_formula_list_item;

static final String TAG = FormulaViewHolder.class.getSimpleName();

View pictureLayout;
JLatexMathView pictureView;

EditText editText;

@Keep
public FormulaViewHolder(Callback callback, View itemView) {
super(callback, itemView);
}

@Override
public int getLayoutResourceId() {
return LAYOUT_RES_ID;
}

@Override
public void onViewCreated(@NonNull View view) {

this.pictureLayout = view.findViewById(R.id.picture_layout);
this.pictureView = view.findViewById(R.id.iv_picture);

this.editText = view.findViewById(R.id.edit_text);

{
this.setTextChangeListener(new TextChangeListener(this, editText));
}
}

@Override
public void onBind(FormulaEntity item, int position) {
super.onBind(item, position);

{
editText.setEnabled(callback.isEnable());
editText.setOnFocusChangeListener(this::onEditFocusChanged);
}

{
// set break strategy to request layout
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
editText.setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE);
}

editText.setText(item.getText());
editText.setVisibility(editText.length() > 0? View.VISIBLE: View.GONE);
}

{
pictureLayout.setEnabled(callback.isEnable());
pictureLayout.setOnClickListener(this::onItemClick);
pictureLayout.setOnLongClickListener(this::onItemLongClick);
}

{
String latex = item.getLatex();
if (TextUtils.isEmpty(latex)) {

String formula = item.getFormula();

if (MathMLTransformer.isMathML(formula)) {
MathMLTransformer transformer = WhatsApp.getInstance().getMathMLTransformer();
try {
formula = transformer.transform(formula);
} catch (TransformerException e) {
e.printStackTrace();
}
}

if (formula == null) {
formula = item.getFormula();
}

latex = formula;
}

try {
pictureView.setLatex(latex, true);
} catch (Exception e) {

}

}

}

@Override
public void onSoftInputChanged(SoftInputHelper helper, boolean visible) {
super.onSoftInputChanged(helper, visible);
if (!visible && editText.hasFocus()) {
editText.clearFocus();
}
}

void onItemClick(View view) {
if (editText.hasFocus()) {
SoftInputUtils.hide(getContext(), editText);
}

this.requestView();
}

boolean onItemLongClick(View view) {

MenuItemClickListener listener = new MenuItemClickListener();

{
listener.put(R.id.menu_delete, (id) -> this.requestRemove());
listener.put(R.id.menu_compose, (id) -> this.requestCompose());
listener.put(R.id.menu_name, (id) -> this.requestEdit());
}

{
int menuRes = R.menu.menu_compose_picture;
PopupMenu popupMenu = new PopupMenu(getContext(), itemView);
popupMenu.inflate(menuRes);
popupMenu.setOnMenuItemClickListener(listener);
popupMenu.show();
}

return true;
}

void onEditFocusChanged(View v, boolean hasFocus) {
if (!hasFocus) {
editText.setVisibility(editText.length() > 0? View.VISIBLE: View.GONE);
SoftInputUtils.hide(getContext(), editText);
}
}

@Override
void requestRemove() {

// save document first
{
callback.requestSave(this);
}

{
Context context = getContext();
CharSequence title = "确定删除方程?";
CharSequence msg = null;
CharSequence negativeButton = context.getString(android.R.string.cancel);
CharSequence positiveButton = context.getString(android.R.string.yes);
DialogInterface.OnClickListener listener = ((dialog, which) -> {
if (which == DialogInterface.BUTTON_POSITIVE) {
super.requestRemove();
}
});

AlertDialogUtils.showConfirm(context, title, msg, negativeButton, positiveButton, listener);
}
}

void requestCompose() {
callback.compose(this);
}

void requestEdit() {
editText.setVisibility(View.VISIBLE);
editText.requestFocus();
editText.setSelection(editText.length());
editText.post(()-> SoftInputUtils.show(getContext(), editText));
}

@Override
public void save() {
entity.setText(editText.getText());
}

}

4. 添加插入方程

拍照照片图库插入图片的基础上,添加插入方程菜单项。

menu_compose_insertion.xml添加新元素入口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<item
android:id="@+id/menu_camera"
android:title="拍照"
android:icon="@drawable/ic_add_a_photo_white_24dp"
android:iconTint="@color/colorPrimary"
app:showAsAction="always"
android:visible="true"/>

<item
android:id="@+id/menu_photo"
android:title="照片图库"
android:icon="@drawable/ic_insert_photo_white_24dp"
android:iconTint="@color/colorPrimary"
app:showAsAction="always"
android:visible="true"/>

<item
android:id="@+id/menu_insert_formula"
android:title="方程"
android:icon="@drawable/ic_insert_formula_white_24dp"
android:iconTint="@color/colorPrimary"
app:showAsAction="always"
android:visible="true"/>

<item
android:id="@+id/menu_insert_paragraph"
android:title="分页"
android:icon="@drawable/ic_insert_paragraph_white"
android:iconTint="@color/colorPrimary"
app:showAsAction="always"
android:visible="false"/>

</menu>

5. 执行插入方程

FormulaDelegate负责2件事情

  1. 插入方程——交由FormulaInsertion完成
  2. 更新方程内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class FormulaDelegate extends BaseRequestDelegate {

FormulaEntity entity;

BaseInsertion.Callback callback;

public FormulaDelegate(Fragment f, FormulaEntity entity, BaseInsertion.Callback callback) {
super(f);

this.entity = entity;
this.callback = callback;
}

@Override
public boolean request() {
SoftInputUtils.hide(context);

try {
String formula = entity == null? "": entity.getFormula();
ComposeFormulaActivity.startForResult(parent, formula, getRequestCode());

return true;
} catch (Exception e) {

}

return false;
}

@Override
public void accept(Integer resultCode, Intent data) {

if ((resultCode != RESULT_OK) || (data == null)) {
return;
}

String formula = data.getStringExtra("formula");
if (TextUtils.isEmpty(formula)) {
return;
}

String latex = data.getStringExtra("latex");
if (TextUtils.isEmpty(latex)) {
return;
}

if (entity == null) {
new FormulaInsertion(callback, formula, latex).execute();
} else {
entity.setFormula(formula);
entity.setLatex(latex);

Document document = callback.getDocument();
int index = document.indexOf(entity);
if (index >= 0) {
callback.getRecyclerView().getAdapter().notifyItemChanged(index);
}

}
}

}

6. 实现插入方程

创建FormulaEntity对象,并添加到笔记Document中,最后更新界面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class FormulaInsertion extends BaseInsertion {

String formula;
String latex;

public FormulaInsertion(Callback callback, String formula, String latex) {
super(callback);

this.formula = formula;
this.latex = latex;
}

@Override
ParagraphEntity insert(int position, CharSequence text) {
Document document = callback.getDocument();

ParagraphEntity entity = null;

int index = position;
for (int i = 0, size = 1; i < size; i++) {

FormulaEntity pic = FormulaEntity.create(document, formula, latex);

if (pic != null) { // add target
document.add(index, pic);
++index;

CharSequence s = ((i + 1) == size)? text: "";
if (s != null) {
ParagraphEntity en = ParagraphEntity.create(document, s);

document.add(index, en);
++index;

entity = en;
}
}
}

int count = (index - position);
if (count > 0) {
getAdapter().notifyItemRangeInserted(position, count);
}

return entity;
}
}

7. 加载方程数据

DocumentManager中注册方程数据结构。

  1. 将JSON数据转化为FormulaEntry对象
1
deserializer.put(FormulaEntry.TYPE, FormulaEntry.class);
  1. FormulaEntry对象转化为FormulaEntity对象
1
factory.put(FormulaEntry.class, FormulaEntity.class);

三、回顾实现过程

过程 相关代码 说明
定义数据结构 FormulaEntry 存储数据
FormulaEntity 逻辑数据
FormulaViewHolder 交互数据
添加新元素入口 修改界面,添加菜单项
完成新元素交互 FormulaDelegate 插入及更新元素
FormulaInsertion 实现插入新元素
加载新元素数据 DocumentManager 注册新元素数据

四、遗留问题

需要7个步骤,涉及至少10个代码文件才能完成添加一种新的笔记元素。

是否有优化的空间???

五、接下来

目前已经实现在笔记中添加方程元素,接下来

  1. 导出图片、文本、Markdown时,包含方程元素
  2. 辅助编辑时,移动及删除方程元素

六、Finally

~汝说~刘伶~古今达者~醉后何妨死便埋~