天天看点

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

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

    • 一、目标
    • 二、实现过程
      • 1. 定义存储数据
      • 2. 定义逻辑数据
      • 3. 定义交互数据
      • 4. 添加插入方程
      • 5. 执行插入方程
      • 6. 实现插入方程
      • 7. 加载方程数据
    • 三、回顾实现过程
    • 四、遗留问题
    • 五、接下来
    • 六、Finally

历经了以下几个开发阶段

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

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

一、目标

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

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

    对象
  1. FormulaEntry

    对象转化为

    FormulaEntity

    对象

三、回顾实现过程

过程 相关代码 说明
定义数据结构

FormulaEntry

存储数据

FormulaEntity

逻辑数据

FormulaViewHolder

交互数据
添加新元素入口 修改界面,添加菜单项
完成新元素交互

FormulaDelegate

插入及更新元素

FormulaInsertion

实现插入新元素
加载新元素数据

DocumentManager

注册新元素数据

四、遗留问题

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

是否有优化的空间???

五、接下来

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

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

六、Finally

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