欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

安卓实现原生渲染LaTeX公式的思路及源码浅析

程序员文章站 2022-07-14 12:53:51
...

一、前言

最近自己的项目中,需要实现数学,化学公式的显示,所以对实现这一功能,进行记录,希望能帮到你,限于技术水平有限,如有错误欢迎指正。

二、现有的方案

目前博主找到的三种常见实现方案如下:
(1)嵌入webview,以MathJax渲染LaTeX公式。
(2)将计算机生成的LaTeX公式以矢量图保存,在移动端插入TextView中显示。
(3)自己实现渲染LaTeX,并且支持移动端输入公式,动态解析并渲染到TextView中。

三、正文

本文主要讨论第三种方式的实现,值得庆幸的是第三种方案已经有开源框架JLaTexMath-andriod但是此项目多年没有维护,且功能相对有限,所以想使用,是需要进行二次开发的,本文主要就实现原理进行简单讨论,帮助大家更好的二次开发或者对自己实现有所启发。

我们从项目自带的sample逐步分析,显示一个数学Precedes符号安卓实现原生渲染LaTeX公式的思路及源码浅析
从上表得知Precedes符号的LaTeX公式为\prec 下面跟随sample来看下,是如何渲染出这个符号的。

首先我们替换掉数据提供类ExampleFormula的返回结果,直接返回我们的字符串

    private static String[] mFormulaArray = new String[]{"${\\prec}$"};
    public static String[] getFormulaArray() {
        return mFormulaArray;
    }

主Activity中对库进行了初始化,传入上下文Context,并定义了ViewPager来显示多种公式效果,因为这里不是我们关心的,所以不展开讨论。

AjLatexMath.init(this);
        mExamples = ExampleFormula.getFormulaArray();
        mPager = (ViewPager) findViewById(R.id.pager);
        mAdapter = new PagesAdapter(getSupportFragmentManager());
        mPager.setAdapter(mAdapter);
        mPager.setOnPageChangeListener(this);
        setTitle(getString(R.string.app_name) + ": About");

跟踪到Fragment中,字符串交给了LaTexTextView

  mLaTexTextView = (LaTexTextView) layout.findViewById(R.id.logo);
        mSizeText = (EditText) layout.findViewById(R.id.size);
        layout.findViewById(R.id.set_textsize).setOnClickListener(this);
        return layout;
    }

    @Override
    public void onResume() {
        super.onResume();
      // setformula();
        mLaTexTextView.setLinketext(mLatex);

    }

LaTexTextView在xml布局中如下

  <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.pys.latex2.LaTexTextView
            android:id="@+id/logo"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textSize="20dp"
            android:textIsSelectable="true"
            android:textColor="@android:color/black"
            android:background="@android:color/white"/>

    </FrameLayout>

跟进setLinketext方法

    public void setLinketext(String text) {
        Log.v("size","Latex  text:"+text) ;
        //去除汉字偏移
        text = getPatternText(text);
        //同步画笔颜色,使生成图片与文字颜色一致
        AjLatexMath.setColor(getCurrentTextColor());
        //先加载空白图片占位
        setText(getSpannable(String.valueOf(Html.fromHtml(text))));
        //异步解析公式,然后生成图片
        setTaskSpannableText(text);
    }

因为解析Latex公式,和创建bitmap都是耗时操作,所以引入了bolts开启异步。
getSpannable方法源码如下

 public SpannableString getSpannable(String text) {
        //主要是使用SpannableString类,
        SpannableString spannableString = new SpannableString(text);
        //设置正则表达式的各种格式。
        Pattern pattern = Pattern.compile(LATEXPATTERN);
        //查找正则表达式的管理类
        Matcher matcher = pattern.matcher(text);

        while (matcher.find()) {//查看是否复合正则表达式

            //去除里面复合正则表达式
            final String group = matcher.group();
            if (group.startsWith("$")) {//是一串 LaTexMath公式
                //先判断缓存中有没有该公式的图片,
                Bitmap image = BitmapCacheUtil.getInstance().getBitmapFromMemCache(group);
                if (image == null) {//如果没有,先加载空白图片
                    image = BitmapCacheUtil.getInstance().getBitmapFromMemCache("10" + "3");
                    if (image == null) {
                        //创建空的bitmap
                        image = getNullBitmap(10, 3);
                        //存储到内存
                        BitmapCacheUtil.getInstance().addBitmapToMemoryCache("10" + "3", image);
                    }
                }
                App.setLog(""+image.getByteCount());
                spannableString.setSpan(new VerticalImageSpan(mContext, image), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }//scaleBitmap(image,(float) 0.3))
        }
        return spannableString;
    }

为了节约内存,和重复利用已经存在的公式,所以采用了LruCache在内存中来缓存bitmap,创建新的bitmap会先在LruCache中查找是否已经存在公式对应的bitmap。

  private void setTaskSpannableText(final String text) {
        Task.callInBackground(new Callable<ArrayList<LaTeXInfo>>() {
            @Override
            public ArrayList<LaTeXInfo> call() throws Exception {
                return getLaTexInfoList(String.valueOf(Html.fromHtml(text)));
            }
        }).continueWith(new Continuation<ArrayList<LaTeXInfo>, Object>() {
            @Override
            public Object then(Task<ArrayList<LaTeXInfo>> task) throws Exception {
                ArrayList<LaTeXInfo> laTeXInfos = task.getResult();
                if (laTeXInfos == null) {
                    return null;
                }
                SpannableString spannableString = new SpannableString(Html.fromHtml(text));
                for (int i = 0; i < laTeXInfos.size(); i++) {
                    LaTeXInfo laTeXInfo = laTeXInfos.get(i);

                    Bitmap image = BitmapCacheUtil.getInstance().getBitmapFromMemCache(laTeXInfo.getGroup()+getPaint().getTextSize() / getPaint().density);
                    if (image == null) {

                        image = getBitmap(laTeXInfo.getTeXFormula());
                        BitmapCacheUtil.getInstance().addBitmapToMemoryCache(laTeXInfo.getGroup()+getPaint().getTextSize() / getPaint().density, image);
                    }
                    
                    App.setLog(”“+image.getByteCount());
                   spannableString.setSpan(new VerticalImageSpan(mContext,image), laTeXInfo.getStart(), laTeXInfo.getEnd(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

                   ExampleActivity.image.setImageBitmap(scaleBitmap(image,(float) 0.3));


                }
                setText(spannableString);
                return null;
            }
        }, Task.UI_THREAD_EXECUTOR);
    }

setTaskSpannableText方法,完成异步解析公式,生成bitmap的工作,核心有两个方法,getLaTexInfoList解析公式并完成初始化创建公式bitmap的信息,getBitmap方法获取生成的bitmap交给spannableString,关于SpannableString的详细用法,不在本文讨论范围,请自行查阅资料。接下来看下getLaTexInfoList

    public ArrayList<LaTeXInfo> getLaTexInfoList(String text) {
        //设置正则表达式的各种格式。
        Pattern pattern = Pattern.compile(LATEXPATTERN);
        //查找正则表达式的管理类
        Matcher matcher = pattern.matcher(text);
        ArrayList<LaTeXInfo> mLaTexInfos = new ArrayList<>();
        while (matcher.find()) {//查看是否复合正则表达式
            //去除里面复合正则表达式
            final String group = matcher.group();
            if (group.startsWith("$")) {//是一串 LaTexMath公式
                TeXFormula teXFormula = TeXFormula.getPartialTeXFormula(group);
//                TeXFormula teXFormula = new TeXFormula(group);
                LaTeXInfo laTeXInfo = new LaTeXInfo(teXFormula, matcher.start(), matcher.end(), group);
                mLaTexInfos.add(laTeXInfo);
            }
        }
        return mLaTexInfos;
    }

TexFormula是接下来要跟进的类,原作者对他的解释如下
Represents a logical mathematical formula that will be displayed (by creating a TeXIcon from it and painting it) using algorithms that are based on the TeX algorithms.
These formula’s can be built using the built-in primitive TeX parser (methods with String arguments) or using other TeXFormula objects. Most methods have (an) equivalent(s) where one or more TeXFormula arguments are replaced with String arguments. These are just shorter notations, because all they do is parse the string(s) to TeXFormula’s and call an equivalent method with (a) TeXFormula argument(s). Most methods also come in 2 variants. One kind will use this TeXFormula to build another mathematical construction and then change this object to represent the newly build construction. The other kind will only use other TeXFormula’s (or parse strings), build a mathematical construction with them and insert this newly build construction at the end of this TeXFormula. Because all the provided methods return a pointer to this (modified) TeXFormula (except for the createTeXIcon method that returns a TeXIcon pointer), method chaining is also possible.

继续跟进getPartialTeXFormula()

	public static TeXFormula getPartialTeXFormula(String formula) {
		TeXFormula f = new TeXFormula();
		if (formula == null) {
			f.add(new EmptyAtom());
			return f;
		}
		TeXParser parser = new TeXParser(true, formula, f);
		try {
			parser.parse();
		} catch (Exception e) {
			if (f.root == null) {
				f.root = new EmptyAtom();
			}
		}
		return f;
	}

终于到了解析公式的关键部分,跟进parse()

	public void parse() throws ParseException {
		if (len != 0) {
			char ch;
			while (pos < len) {
				ch = parseString.charAt(pos);
				switch (ch) {
				case '\n':
					line++;
					col = pos;
				case '\t':
				case '\r':
					pos++;
					break;
				case ' ':
					pos++;
					if (!ignoreWhiteSpace) {// We are in a mbox
						formula.add(new SpaceAtom());
						formula.add(new BreakMarkAtom());
						while (pos < len) {
							ch = parseString.charAt(pos);
							if (ch != ' ' || ch != '\t' || ch != '\r')
								break;
							pos++;
						}
					}
					break;
				case DOLLAR:
					pos++;
					if (!ignoreWhiteSpace) {// We are in a mbox
						int style = TeXConstants.STYLE_TEXT;
						boolean doubleDollar = false;
						if (parseString.charAt(pos) == DOLLAR) {
							style = TeXConstants.STYLE_DISPLAY;
							doubleDollar = true;
							pos++;
						}
						formula.add(new MathAtom(new TeXFormula(this,
								getDollarGroup(DOLLAR), false).root, style));
						if (doubleDollar) {
							if (parseString.charAt(pos) == DOLLAR) {
								pos++;
							}
						}
					}
					break;
				case ESCAPE:
					Atom at = processEscape();
					formula.add(at);
					if (arrayMode && at instanceof HlineAtom) {
						((ArrayOfAtoms) formula).addRow();
					}
					if (insertion) {
						insertion = false;
					}
					break;
				case L_GROUP:
					Atom atom = getArgument();
					if (atom != null) {
						atom.type = TeXConstants.TYPE_ORDINARY;
					}
					formula.add(atom);
					break;
				case R_GROUP:
					group--;
					pos++;
					if (group == -1)
						throw new ParseException("Found a closing '" + R_GROUP
								+ "' without an opening '" + L_GROUP + "'!");
					return;
				case SUPER_SCRIPT:
					formula.add(getScripts(ch));
					break;
				case SUB_SCRIPT:
					if (ignoreWhiteSpace) {
						formula.add(getScripts(ch));
					} else {
						formula.add(new UnderscoreAtom());
						pos++;
					}
					break;
				case '&':
					if (!arrayMode)
						throw new ParseException(
								"Character '&' is only available in array mode !");
					((ArrayOfAtoms) formula).addCol();
					pos++;
					break;
				case PRIME:
					if (ignoreWhiteSpace) {
						formula.add(new CumulativeScriptsAtom(getLastAtom(),
								null, SymbolAtom.get("prime")));
					} else {
						formula.add(convertCharacter(PRIME, true));
					}
					pos++;
					break;
				case BACKPRIME:
					if (ignoreWhiteSpace) {
						formula.add(new CumulativeScriptsAtom(getLastAtom(),
								null, SymbolAtom.get("backprime")));
					} else {
						formula.add(convertCharacter(BACKPRIME, true));
					}
					pos++;
					break;
				case DQUOTE:
					if (ignoreWhiteSpace) {
						formula.add(new CumulativeScriptsAtom(getLastAtom(),
								null, SymbolAtom.get("prime")));
						formula.add(new CumulativeScriptsAtom(getLastAtom(),
								null, SymbolAtom.get("prime")));
					} else {
						formula.add(convertCharacter(PRIME, true));
						formula.add(convertCharacter(PRIME, true));
					}
					pos++;
					break;
				default:
					formula.add(convertCharacter(ch, false));
					pos++;
				}
			}
		}
		if (formula.root == null && !arrayMode) {
			formula.add(new EmptyAtom());
		}
	}

逐个字符的解析,并调用TexFormula.add((Atom el)添加Atom的子类,Atom是一个抽象类,他的子类可以确定自己的类型和排列方式,并且实现一个可绘制的矩形区域(Box)。Box这个抽象类负责具体字符的绘制,后面会对他进行深入剖析,先回到LaTexTextView,看一下getBitmap方法

    private Bitmap getBitmap(TeXFormula formula) {

        TeXIcon icon = formula.new TeXIconBuilder()
                .setStyle(TeXConstants.STYLE_DISPLAY)
                .setSize(getPaint().getTextSize() / getPaint().density)
                .setWidth(TeXConstants.UNIT_SP, getPaint().getTextSize() / getPaint().density, TeXConstants.ALIGN_LEFT)
                .setIsMaxWidth(true)
                .setInterLineSpacing(TeXConstants.UNIT_SP,
                        AjLatexMath.getLeading(getPaint().getTextSize() / getPaint().density))
                .build();
        icon.setInsets(new Insets(5, 5, 5, 5));
        Bitmap image = Bitmap.createBitmap(icon.getIconWidth(), icon.getIconHeight(),
                Bitmap.Config.ARGB_4444);
        System.out.println(" width=" + icon.getBox().getWidth() + " height=" + icon.getBox().getHeight() +
                " iconwidth=" + icon.getIconWidth() + " iconheight=" + icon.getIconHeight());
        Canvas g2 = new Canvas(image);
        g2.drawColor(Color.TRANSPARENT);
        icon.paintIcon(g2, 0, 0);
      //  ExampleActivity.image.setImageBitmap(image);
        App.setLog("getbitmap:"+saveImg(image,"图片",mContext));
        return image;
    }

将canvas 传入了TeXIcon,跟进paintIcon方法

	public void paintIcon(Canvas g, int x, int y) {
		Canvas g2 = (Canvas) g;

		g2.scale(size, size); // the point size
		// draw formula box
		box.draw(g2, (x + insets.left) / size,
				(y + insets.top) / size + box.getHeight());
	}

这里出现了前面介绍的Box这个重要的抽象类,但是这里究竟是调用了他的哪个子类呢?
继续跟进box这个变量的初始化

protected TeXIcon(Box b, float size, boolean trueValues) {
		box = b;

他在构造器中被初始化,现在再回到getBitmap方法,跟进获取TeXIcon实例的build()方法

	public TeXIcon build() {
			if (style == null) {
				throw new IllegalStateException(
						"A style is required. Use setStyle()");
			}
			if (size == null) {
				throw new IllegalStateException(
						"A size is required. Use setStyle()");
			}
			DefaultTeXFont font = (type == null) ? new DefaultTeXFont(size)
					: createFont(size, type);
			TeXEnvironment te;
			if (widthUnit != null) {
				te = new TeXEnvironment(style, font, widthUnit, textWidth);
			} else {
				te = new TeXEnvironment(style, font);
			}

			if (interLineUnit != null) {
				te.setInterline(interLineUnit, interLineSpacing);
			}

			Box box = createBox(te);
			TeXIcon ti;
			if (widthUnit != null) {
				HorizontalBox hb;
				if (interLineUnit != null) {
					float il = interLineSpacing
							* SpaceAtom.getFactor(interLineUnit, te);
					Box b = BreakFormula.split(box, te.getTextwidth(), il);

					hb = new HorizontalBox(b, isMaxWidth ? b.getWidth()
							: te.getTextwidth(), align);
				} else {
					hb = new HorizontalBox(box, isMaxWidth ? box.getWidth()
							: te.getTextwidth(), align);
				}

				ti = new TeXIcon(hb, size, trueValues);
			} else {
				ti = new TeXIcon(box, size, trueValues);
			}
			if (fgcolor != null) {
				ti.setForeground(fgcolor);
			}
			ti.isColored = te.isColored;
			return ti;
		}

可以发现,Box的子类是HorizontalBox或者根据TeXEnvironment生成其他子类。根据我们需要显示的内容,这里的子类是HorizontalBox,鉴于此类代码过多,所以只看我们关心的draw方法的实现。

	public void draw(Canvas g2, float x, float y) {
		startDraw(g2, x, y);
		float xPos = x;
		for (Box box : children) {
			box.draw(g2, xPos, y + box.shift);
			xPos += box.getWidth();
		}
		endDraw(g2);
	}

跟进startDraw

	protected void startDraw(Canvas g2, float x, float y) {
		prevColor = AjLatexMath.getPaint().getColor();
		if (background != null) { // draw background
			AjLatexMath.getPaint().setColor(background);
		}
		if (foreground == null) {
			AjLatexMath.getPaint().setColor(prevColor); // old foreground color
		} else {
			AjLatexMath.getPaint().setColor(foreground); // overriding foreground														// color
		}
		drawDebug(g2, x, y);
	}

跟进drawDebug

	protected void drawDebug(Canvas g2, float x, float y, boolean showDepth) {
		if (DEBUG) {
			Paint st = AjLatexMath.getPaint();
			int c = st.getColor();
			st.setColor(markForDEBUG);
			st.setStyle(Style.FILL_AND_STROKE);
			if (markForDEBUG != null) {
				g2.drawRect(x, y - height, width, height + depth, st);
			}
			if (width < 0) {
				x += width;
				width = -width;
			}
			g2.drawRect(x, y - height, width, height + depth, st);
			if (showDepth) {
				st.setColor(Color.RED);
				if (depth > 0) {
					g2.drawRect(x, y, width, depth, st);
				} else if (depth < 0) {
					g2.drawRect(x, y + depth, width, -depth, st);
				} else {
				}
			}
			st.setColor(c);
		}
	}

这里可以看出,startDraw方法的作用是绘制纯色的矩形区域。既然白纸已经铺好,接下来自然是拿起笔画画了,所以回到HorizontalBox的draw方法,来看这个for循环。遍历children,children是一个Box的List集合,经过调试发现,这里他绘制符号所调用的Box的子类是CharBox,那么看一下CharBox的draw方法的实现

public void draw(Canvas g2, float x, float y) {
		drawDebug(g2, x, y);
		g2.save();
		g2.translate(x, y);
		Typeface font = FontInfo.getFont(cf.fontId);
		if (size != 1) {
			g2.scale(size, size);
		}
		Paint st = AjLatexMath.getPaint();
		st.setTextSize(TeXFormula.PIXELS_PER_POINT);
		st.setTypeface(font);
		st.setStyle(Style.FILL);
		st.setAntiAlias(true);
		st.setStrokeWidth(0);
		arr[0] = cf.c;
		st.setTypeface(font);
		g2.drawText(arr, 0, 1, 0, 0,st);
		g2.restore();
	}

这就是符号绘制的核心地方了,Paint设置自定义字体,最后以Canvas.drawText()绘制到SpannableString中的bitmap上。这里有两处核心代码,需要深入跟进看一下实现。
首先,显示的文本是cf.c ,cf是CharFont变量,在构造器中初始化

public CharBox(Char c) {
		cf = c.getCharFont();
		size = c.getMetrics().getSize();
		width = c.getWidth();
		height = c.getHeight();
		depth = c.getDepth();
	}

通过调试,发现CharBox在SymbolAtom的createBox方法中被实例化。

public Box createBox(TeXEnvironment env) {
		TeXFont tf = env.getTeXFont();
		int style = env.getStyle();
		Char c = tf.getChar(name, style);
		Box cb = new CharBox(c);
		if (env.getSmallCap() && unicode != 0 && Character.isLowerCase(unicode)) {
			try {
				cb = new ScaleBox(new CharBox(tf.getChar(
						TeXFormula.symbolTextMappings[Character
								.toUpperCase(unicode)], style)), 0.8, 0.8);
			} catch (SymbolMappingNotFoundException e) {
			}
		}
		
		if (type == TeXConstants.TYPE_BIG_OPERATOR) {
			if (style < TeXConstants.STYLE_TEXT && tf.hasNextLarger(c))
				c = tf.getNextLarger(c, style);
			cb = new CharBox(c);
			cb.setShift(-(cb.getHeight() + cb.getDepth()) / 2
					- env.getTeXFont().getAxisHeight(env.getStyle()));
			float delta = c.getItalic();
			HorizontalBox hb = new HorizontalBox(cb);
			if (delta > TeXFormula.PREC)
				hb.add(new StrutBox(delta, 0, 0, 0));
			return hb;
		}
		return cb;
	}

SymbolAtom是继承自CharSymbol的子类,而CharSymbol继承自Atom,正是前面解析字符时TexFormula.add((Atom el)方法进行的实现。
TeXFont是一个接口,经过调试发现他的实现类是DefaultTeXFont,进入他的getChar方法

	public Char getChar(String symbolName, int style)
			throws SymbolMappingNotFoundException {

		Object obj = symbolMappings.get(symbolName);

		if (obj == null) {// no symbol mapping found!
			throw new SymbolMappingNotFoundException(symbolName);
		} else {
			return getChar((CharFont) obj, style);
		}
	}

这里传入的symbolName的值为prec,而返回的CharFont所携带,最终交给Canvas的字符是Á,这是如何完成的转换呢?又为什么是这个字符呢?别急,继续跟进,看看obj是如何被创建出来的。symbolMappings是一个Map集合,签名为

private static Map<String, CharFont> symbolMappings;
...
symbolMappings = parser.parseSymbolMappings();

跟进parseSymbolMappings

	public Map<String, CharFont> parseSymbolMappings()
			throws ResourceParseException {
		Map<String, CharFont> res = new HashMap<String, CharFont>();
		Element symbolMappings = (Element) root.getElementsByTagName(
				"SymbolMappings").item(0);
		if (symbolMappings == null)
			// "SymbolMappings" is required!
			throw new XMLResourceParseException(RESOURCE_NAME,"SymbolMappings");
		else { // element present
				// iterate all mappings
			NodeList list = symbolMappings.getElementsByTagName("Mapping");
			for (int i = 0; i < list.getLength(); i++) {
				String include = getAttrValueAndCheckIfNotNull("include",
						(Element) list.item(i));
				Element map;
				try {
					InputStream is = null;
					if (base == null) {
						map = factory
								.newDocumentBuilder()
								.parse(is = AjLatexMath.getAssetManager().open(
										include)).getDocumentElement();
					} else {
						map = factory
								.newDocumentBuilder()
								.parse(is = AjLatexMath.getAssetManager().open(
										include)).getDocumentElement();
					}
					is.close();
				} catch (Exception e) {
					throw new XMLResourceParseException("Cannot find the file "
							+ include + "!");
				}
				NodeList listM = map.getElementsByTagName(SYMBOL_MAPPING_EL);
				for (int j = 0; j < listM.getLength(); j++) {
					Element mapping = (Element) listM.item(j);


					// get string attribute
					String symbolName = getAttrValueAndCheckIfNotNull("name",
							mapping);
					// get integer attributes
					int ch = getIntAndCheck("ch", mapping);
					String fontId = getAttrValueAndCheckIfNotNull("fontId",
							mapping);
					// put mapping in table
					String boldFontId = null;
					try {
						boldFontId = getAttrValueAndCheckIfNotNull("boldId",
								mapping);
					} catch (ResourceParseException e) {
					}

					if (boldFontId == null) {
						//Log.v("liv",symbolName+"--"+ch+"--"+((char) ch));
						res.put(symbolName,
								new CharFont((char) ch, Font_ID.indexOf(fontId)));
					} else {
						res.put(symbolName,
								new CharFont((char) ch,
										Font_ID.indexOf(fontId), Font_ID
												.indexOf(boldFontId)));
					}
				}
			}

			return res;
		}
	}

原来是int转换成了char,接下来跟进getIntAndCheck看看int的由来

	public static int getIntAndCheck(String attrName, Element element)
			throws ResourceParseException {
		String attrValue = getAttrValueAndCheckIfNotNull(attrName, element);
		// try parsing string to integer value
		int res = 0;
		try {
			res = Integer.parseInt(attrValue);
		} catch (NumberFormatException e) {
			throw new XMLResourceParseException(RESOURCE_NAME,
					element.getTagName(), attrName,
					"has an invalid integer value!");
		}
		// parsing OK
		return res;
	}

从getAttrValueAndCheckIfNotNull获得String字符串,在经过Integer.parseInt转换为int值,继续跟进getAttrValueAndCheckIfNotNull

	private static String getAttrValueAndCheckIfNotNull(String attrName,
			Element element) throws ResourceParseException {
		String attrValue = element.getAttribute(attrName);
		if (attrValue.equals(""))
			throw new XMLResourceParseException(RESOURCE_NAME,
					element.getTagName(), attrName, null);
		return attrValue;
	}

这里可以发现是,从Element获得的数据,那么他在哪里解析的XML呢,回到最初的parseSymbolMappings,注意到

map = factory.newDocumentBuilder().parse(is = AjLatexMath.getAssetManager().
open(include)).getDocumentElement();

	....
        NodeList listM = map.getElementsByTagName(SYMBOL_MAPPING_EL);
				for (int j = 0; j < listM.getLength(); j++) {
					Element mapping = (Element) listM.item(j);
...
					int ch = getIntAndCheck("ch", mapping);

其中include代表了需要解析哪些xml,而include的获得,则来自于另外一个Element,也是由xml中所定义,听起来有点绕,甚至有点套娃的感觉,但是并不复杂,多点耐心就好了,限于篇幅就不继续展开include的获取过程了。这里根据我们的内容,主要加载了
jlm_amssymb.map.xml
jlm_amsfonts.map.xml
jlm_stmaryrd.map.xml
jlm_special.map.xml
四个xml。

接着我们回到CharBox的draw看一看第二处关键代码:

Typeface font = FontInfo.getFont(cf.fontId);
st.setTypeface(font);

这里的font是FontInfo根据 CharFont的fontId获取的Typeface,底层也是一个Map集合,集合初始化过程和上面字符编码集合的初始化过程很相似,可以自己阅读源码查看。
这里加载的是jlm_cmsy10.ttf字体文件,打开字体文件如下:
安卓实现原生渲染LaTeX公式的思路及源码浅析

其中Precedes符号的unicode码为\u00C1,转换为字符是Á。到这里整个流程基本就分析完毕了,后续做二次开发还可以加入自己的字体库,支持更多公式,另外项目对中文支持还有缺陷,有可能爆内存,子线程处理有时图片会错位,需要在UI线程执行。这些都是需要优化的地方,感谢阅读!

相关标签: Android