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文本。
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进行一次包装。

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. 给方程添加描述内容
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添加新元素入口。

<?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. 更新方程内容
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中,最后更新界面。

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对象
deserializer.put(FormulaEntry.TYPE, FormulaEntry.class);
  1. FormulaEntry对象转化为FormulaEntity对象
factory.put(FormulaEntry.class, FormulaEntity.class);

三、回顾实现过程

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

四、遗留问题

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

是否有优化的空间???

五、接下来

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

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

六、Finally

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

发布了172 篇原创文章 · 获赞 15 · 访问量 5万+
展开阅读全文
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览