/* * Copyright 2013 serso aka se.solovyev * * 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. * * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Contact details * * Email: se.solovyev@gmail.com * Site: http://se.solovyev.org */ package org.solovyev.android.calculator.view; import android.graphics.Typeface; import android.support.annotation.NonNull; import android.text.SpannableStringBuilder; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import com.google.common.collect.Lists; import org.solovyev.android.Check; import org.solovyev.android.calculator.BaseNumberBuilder; import org.solovyev.android.calculator.Engine; import org.solovyev.android.calculator.LiteNumberBuilder; import org.solovyev.android.calculator.NumberBuilder; import org.solovyev.android.calculator.math.MathType; import org.solovyev.android.calculator.text.TextProcessor; import org.solovyev.android.calculator.text.TextProcessorEditorResult; import java.util.ArrayList; import java.util.List; import javax.annotation.Nonnull; public class TextHighlighter implements TextProcessor<TextProcessorEditorResult, String> { private final int red; private final int green; private final int blue; private final boolean formatNumber; private final int dark; @Nonnull private final Engine engine; public TextHighlighter(int color, boolean formatNumber, @Nonnull Engine engine) { this.formatNumber = formatNumber; this.engine = engine; red = red(color); green = green(color); blue = blue(color); dark = isDark(red, green, blue) ? 1 : -1; } private static int blue(int color) { return color & 0xFF; } private static int green(int color) { return (color >> 8) & 0xFF; } private static int red(int color) { return (color >> 16) & 0xFF; } public static boolean isDark(int color) { return isDark(red(color), green(color), color & 0xFF); } public static boolean isDark(int red, int green, int blue) { final float y = 0.2126f * red + 0.7152f * green + 0.0722f * blue; return y < 128; } @Nonnull @Override public TextProcessorEditorResult process(@Nonnull String text) { final SpannableStringBuilder sb = new SpannableStringBuilder(); final BaseNumberBuilder nb = !formatNumber ? new LiteNumberBuilder(engine) : new NumberBuilder(engine); final MathType.Result result = new MathType.Result(); int offset = 0; int groupsCount = 0; int openGroupsCount = 0; for (int i = 0; i < text.length(); i++) { MathType.getType(text, i, nb.isHexMode(), result, engine); offset += nb.process(sb, result); final String match = result.match; switch (result.type) { case open_group_symbol: openGroupsCount++; groupsCount = Math.max(groupsCount, openGroupsCount); sb.append(text.charAt(i)); break; case close_group_symbol: openGroupsCount--; sb.append(text.charAt(i)); break; case operator: i += append(sb, match); break; case function: i += append(sb, match); makeItalic(sb, i + 1 - match.length(), i + 1); break; case constant: case numeral_base: i += append(sb, match); makeBold(sb, i + 1 - match.length(), i + 1); break; default: if (result.type == MathType.text || match.length() <= 1) { sb.append(text.charAt(i)); } else { i += append(sb, match); } } } if (nb instanceof NumberBuilder) { offset += ((NumberBuilder) nb).processNumber(sb); } if (groupsCount == 0) { return new TextProcessorEditorResult(sb, offset); } final List<GroupSpan> groupSpans = new ArrayList<>(groupsCount); fillGroupSpans(sb, 0, 0, groupsCount, groupSpans); for (GroupSpan groupSpan : Lists.reverse(groupSpans)) { makeColor(sb, groupSpan.start, groupSpan.end, getColor(groupSpan.group, groupsCount)); } return new TextProcessorEditorResult(sb, offset); } private int append(SpannableStringBuilder t, String match) { t.append(match); if (match.length() > 1) { return match.length() - 1; } return 0; } private static void makeItalic(@Nonnull SpannableStringBuilder t, int start, int end) { setSpan(t, new StyleSpan(Typeface.ITALIC), start, end); } private static void makeBold(@Nonnull SpannableStringBuilder t, int start, int end) { setSpan(t, new StyleSpan(Typeface.BOLD), start, end); } private static void makeColor(@Nonnull SpannableStringBuilder t, int start, int end, int color) { setSpan(t, new ForegroundColorSpan(color), start, end); } private static void setSpan(@Nonnull SpannableStringBuilder t, @NonNull Object span, int start, int end) { start = Math.max(0, Math.min(start, t.length())); end = Math.max(0, Math.min(end, t.length())); if (start >= end) { return; } t.setSpan(span, start, end, SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE); } private int fillGroupSpans(@Nonnull SpannableStringBuilder sb, int start, int group, int groupsCount, @Nonnull List<GroupSpan> spans) { for (int i = start; i < sb.length(); i++) { final char c = sb.charAt(i); if (MathType.isOpenGroupSymbol(c)) { i = highlightGroup(sb, i, group + 1, groupsCount, spans); } else if (MathType.isCloseGroupSymbol(c)) { return i; } } return sb.length(); } private int highlightGroup(SpannableStringBuilder sb, int start, int group, int groupsCount, @Nonnull List<GroupSpan> spans) { final int end = Math.min(sb.length(), fillGroupSpans(sb, start + 1, group, groupsCount, spans)); if (start + 1 < end) { spans.add(new GroupSpan(start + 1, end, group)); } return end; } private int getColor(int group, int groupsCount) { final int offset = (int) (dark * 255 * 0.6) * group / (groupsCount + 1); return (0xFF << 24) | ((red + offset) << 16) | ((green + offset) << 8) | (blue + offset); } private static class GroupSpan { final int start; final int end; final int group; private GroupSpan(int start, int end, int group) { Check.isTrue(start < end); this.start = start; this.end = end; this.group = group; } } }