Add KanjiVG view

Julien LepillerSun Jun 26 22:58:35+0200 2022

a24e9b5

Add KanjiVG view

app/src/main/java/eu/lepiller/nani/ResultPagerAdapter.java

1919
import java.util.List;
2020
import java.util.Map;
2121
22+
import eu.lepiller.nani.views.KanjiStrokeView;
2223
import eu.lepiller.nani.views.PitchContourView;
2324
import eu.lepiller.nani.views.PitchDiagramView;
2425
import me.weilunli.views.RubyTextView;

7273
                TextView kanjiView = child_result.findViewById(R.id.kanji_view);
7374
                TextView strokesView = child_result.findViewById(R.id.kanji_strokes);
7475
                TextView elementsView = child_result.findViewById(R.id.kanji_elements);
76+
                KanjiStrokeView strokeView = child_result.findViewById(R.id.kanji_stroke_view);
7577
                LinearLayout senses_view = child_result.findViewById(R.id.sense_view);
7678
                TextView onView = child_result.findViewById(R.id.on_reading);
7779
                TextView kunView = child_result.findViewById(R.id.kun_reading);

8082
                kanjiView.setText(result.getKanji());
8183
                strokesView.setText(String.format(context.getResources().getQuantityString(R.plurals.kanji_stroke, result.getStroke()),
8284
                        result.getStroke()));
85+
                strokeView.setStrokes(result.getStrokes());
86+
                if(result.getStrokes() == null)
87+
                    strokeView.setVisibility(View.GONE);
8388
8489
                elementsView.setText(String.format(context.getResources().getQuantityString(R.plurals.kanji_elements, result.getElements().size()),
8590
                        getContent(result.getElements())));

app/src/main/java/eu/lepiller/nani/dictionary/KanjiVG.java

11
package eu.lepiller.nani.dictionary;
22
3+
import android.graphics.Path;
34
import android.util.Log;
5+
import android.util.Pair;
46
57
import java.io.File;
68
import java.io.FileNotFoundException;

9193
        return null;
9294
    }
9395
96+
    private static class ParseException extends Exception {
97+
        ParseException() {}
98+
    }
99+
100+
    private static Pair<Integer, Float> parsePathFloat(String command, int pos) {
101+
        boolean negative = false;
102+
        int integerPart = 0;
103+
        int fractionPart = 0;
104+
        int fractionSize = 1;
105+
        boolean foundDot = false;
106+
107+
        if(command.charAt(pos) == '-') {
108+
            negative = true;
109+
            pos++;
110+
        }
111+
112+
        while(pos < command.length()) {
113+
            char c = command.charAt(pos);
114+
            if(c == '.') {
115+
                if (foundDot)
116+
                    break;
117+
                foundDot = true;
118+
            } else if(c >= '0' && c <= '9') {
119+
                if(foundDot) {
120+
                    fractionPart *= 10;
121+
                    fractionPart += c - '0';
122+
                    fractionSize *= 10;
123+
                } else {
124+
                    integerPart *= 10;
125+
                    integerPart += c - '0';
126+
                }
127+
            } else {
128+
                break;
129+
            }
130+
            pos++;
131+
        }
132+
133+
        float number = (float) integerPart + ((float) fractionPart / (float)fractionSize);
134+
        return negative? new Pair<>(pos, -number): new Pair<>(pos, number);
135+
    }
136+
137+
    private static Pair<Integer, List<Float>> parsePathFloatList(String command, int pos) {
138+
        char c = command.charAt(pos);
139+
        List<Float> res = new ArrayList<>();
140+
        while((c >= '0' && c <= '9') || c == '.' || c == '-') {
141+
            Pair<Integer, Float> f = parsePathFloat(command, pos);
142+
            pos = f.first;
143+
            res.add(f.second);
144+
            if(pos == command.length())
145+
                break;
146+
            if(command.charAt(pos) == ',' || command.charAt(pos) == ' ')
147+
                pos++;
148+
            c = command.charAt(pos);
149+
        }
150+
        return new Pair<>(pos, res);
151+
    }
152+
153+
    private static int parsePathMoveTo(Path path, String command, int pos, boolean relative) throws ParseException {
154+
        Pair<Integer, List<Float>> lst = parsePathFloatList(command, pos);
155+
        if(lst.second.size() != 2)
156+
            throw new ParseException();
157+
        pos = lst.first;
158+
        if(relative)
159+
            path.rMoveTo(lst.second.get(0), lst.second.get(1));
160+
        else
161+
            path.moveTo(lst.second.get(0), lst.second.get(1));
162+
        return pos;
163+
    }
164+
165+
    private static int parsePathCurveTo(Path path, String command, int pos, boolean relative) throws ParseException {
166+
        Pair<Integer, List<Float>> lst = parsePathFloatList(command, pos);
167+
        if(lst.second.size() != 6) {
168+
            Log.e(TAG, "list: " + lst.second.size() + " with " + Arrays.toString(lst.second.toArray()));
169+
            Log.e(TAG, "command: " + command);
170+
            Log.e(TAG, "next pos: " + pos + " with char " + command.charAt(pos));
171+
            throw new ParseException();
172+
        }
173+
        pos = lst.first;
174+
        if(relative)
175+
            path.rCubicTo(lst.second.get(0), lst.second.get(1), lst.second.get(2), lst.second.get(3), lst.second.get(4), lst.second.get(5));
176+
        else
177+
            path.cubicTo(lst.second.get(0), lst.second.get(1), lst.second.get(2), lst.second.get(3), lst.second.get(4), lst.second.get(5));
178+
        return pos;
179+
    }
180+
181+
    private static int parsePathLineTo(Path path, String command, int pos, boolean relative) throws ParseException {
182+
        Pair<Integer, List<Float>> lst = parsePathFloatList(command, pos);
183+
        if(lst.second.size() != 2)
184+
            throw new ParseException();
185+
        pos = lst.first;
186+
        if(relative)
187+
            path.rLineTo(lst.second.get(0), lst.second.get(1));
188+
        else
189+
            path.lineTo(lst.second.get(0), lst.second.get(1));
190+
        return pos;
191+
    }
192+
193+
    private static int parsePathQuadraticTo(Path path, String command, int pos, boolean relative) throws ParseException {
194+
        Pair<Integer, List<Float>> lst = parsePathFloatList(command, pos);
195+
        if(lst.second.size() != 4)
196+
            throw new ParseException();
197+
        pos = lst.first;
198+
        if(relative)
199+
            path.rQuadTo(lst.second.get(0), lst.second.get(1), lst.second.get(2), lst.second.get(3));
200+
        else
201+
            path.quadTo(lst.second.get(0), lst.second.get(1), lst.second.get(2), lst.second.get(3));
202+
        return pos;
203+
    }
204+
205+
    private static void parsePath(Path path, String command) throws ParseException {
206+
        int pos = 0;
207+
        while(pos < command.length()) {
208+
            char com = command.charAt(pos);
209+
            boolean relative = com < 'A' || com > 'Z';
210+
            pos++;
211+
            switch(com) {
212+
                case 'm':
213+
                case 'M':
214+
                    pos = parsePathMoveTo(path, command, pos, relative);
215+
                    break;
216+
                case 'c':
217+
                case 'C':
218+
                case 's':
219+
                case 'S':
220+
                    pos = parsePathCurveTo(path, command, pos, relative);
221+
                    break;
222+
                case 'l':
223+
                case 'L':
224+
                    pos = parsePathLineTo(path, command, pos, relative);
225+
                    break;
226+
                case 'q':
227+
                case 'Q':
228+
                case 't':
229+
                case 'T':
230+
                    pos = parsePathQuadraticTo(path, command, pos, relative);
231+
                    break;
232+
                case 'z':
233+
                case 'Z':
234+
                    path.close();
235+
                case ' ':
236+
                    pos++;
237+
                    break;
238+
                default:
239+
                    throw new ParseException();
240+
            }
241+
        }
242+
    }
243+
94244
    KanjiResult.Stroke getStroke(RandomAccessFile file) throws IOException {
95245
        String command = getHuffmanString(file, commandHuffman);
96246
        String x = getHuffmanString(file, commandHuffman);
97247
        String y = getHuffmanString(file, commandHuffman);
98-
        return new KanjiResult.Stroke(command, x, y);
248+
249+
        Path path = new Path();
250+
251+
        try {
252+
            parsePath(path, command);
253+
        } catch (ParseException e) {
254+
            e.printStackTrace();
255+
            path.reset();
256+
        }
257+
258+
        return new KanjiResult.Stroke(path, 109, Float.parseFloat(x), Float.parseFloat(y));
99259
    }
100260
101261
    KanjiResult getValue(RandomAccessFile file, long pos, String kanji) throws IOException {

app/src/main/java/eu/lepiller/nani/result/KanjiResult.java

11
package eu.lepiller.nani.result;
22
3+
import android.graphics.Path;
4+
35
import java.util.ArrayList;
46
import java.util.List;
57
import java.util.Objects;

2224
    }
2325
2426
    public static class Stroke {
25-
        private final String command, posX, posY;
26-
27-
        public Stroke(String command, String posX, String posY) {
28-
            this.command = command;
29-
            this.posX = posX;
30-
            this.posY = posY;
27+
        private final float numX, numY;
28+
        private final int size;
29+
        private final Path path;
30+
31+
        public Stroke(Path path, int size, float numX, float numY) {
32+
            this.path = path;
33+
            this.size = size;
34+
            this.numX = numX;
35+
            this.numY = numY;
3136
        }
3237
33-
        @Override
34-
        public int hashCode() {
35-
            return Objects.hash(command, posX, posY);
38+
        public Path getPath() {
39+
            return path;
3640
        }
3741
38-
        public String getCommand() {
39-
            return command;
42+
        public float getNumX() {
43+
            return numX;
4044
        }
4145
42-
        public String getPosX() {
43-
            return posX;
46+
        public float getNumY() {
47+
            return numY;
4448
        }
4549
46-
        public String getPosY() {
47-
            return posY;
50+
        public int getSize() {
51+
            return size;
4852
        }
4953
    }
5054

112116
        return elements;
113117
    }
114118
119+
    public List<Stroke> getStrokes() {
120+
        return strokes;
121+
    }
122+
115123
    public List<String> getKun() {
116124
        return kun;
117125
    }

app/src/main/java/eu/lepiller/nani/views/KanjiStrokeView.java unknown status 1

1+
package eu.lepiller.nani.views;
2+
3+
import android.content.Context;
4+
import android.content.res.TypedArray;
5+
import android.graphics.Canvas;
6+
import android.graphics.Color;
7+
import android.graphics.Matrix;
8+
import android.graphics.Paint;
9+
import android.graphics.Path;
10+
import android.graphics.RectF;
11+
import android.util.AttributeSet;
12+
import android.view.View;
13+
14+
import androidx.annotation.Nullable;
15+
16+
import java.util.ArrayList;
17+
import java.util.List;
18+
19+
import eu.lepiller.nani.R;
20+
import eu.lepiller.nani.result.KanjiResult;
21+
22+
public class KanjiStrokeView extends View {
23+
    public enum ColorScheme {
24+
        NOCOLOR, HIGHCONTRAST, SPECTRUM
25+
    }
26+
27+
    private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
28+
    private final Path path = new Path();
29+
    private final Matrix matrix = new Matrix();
30+
    private final RectF rectF = new RectF();
31+
32+
    private ColorScheme scheme = ColorScheme.SPECTRUM;
33+
    private final List<KanjiResult.Stroke> strokes = new ArrayList<>();
34+
    private int defaultColor = Color.BLACK;
35+
    private int size = 64;
36+
37+
    public KanjiStrokeView(Context context) {
38+
        super(context);
39+
        setDefaults();
40+
    }
41+
42+
    public KanjiStrokeView(Context context, @Nullable AttributeSet attrs) {
43+
        super(context, attrs);
44+
        setDefaults();
45+
        setupAttributes(attrs, 0);
46+
    }
47+
48+
    public KanjiStrokeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
49+
        super(context, attrs, defStyleAttr);
50+
        setDefaults();
51+
        setupAttributes(attrs, defStyleAttr);
52+
    }
53+
54+
    private void setDefaults() {
55+
        defaultColor = getContext().getResources().getColor(android.R.color.tab_indicator_text);
56+
    }
57+
58+
    public void setupAttributes(AttributeSet attrs, int defStyleAttr) {
59+
        TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.KanjiStrokeView, defStyleAttr, 0);
60+
        defaultColor = typedArray.getColor(R.styleable.KanjiStrokeView_defaultColor, defaultColor);
61+
        size = typedArray.getDimensionPixelSize(R.styleable.KanjiStrokeView_size, size);
62+
    }
63+
64+
    public void setScheme(ColorScheme scheme) {
65+
        this.scheme = scheme;
66+
    }
67+
68+
    public void setStrokes(List<KanjiResult.Stroke> strokes) {
69+
        this.strokes.clear();
70+
        this.strokes.addAll(strokes);
71+
    }
72+
73+
    private static int getColor(int i, int m, ColorScheme scheme, int defaultColor) {
74+
        if(scheme == ColorScheme.NOCOLOR)
75+
            return defaultColor;
76+
77+
        float angle = 0.618033988749895f;
78+
        float[] hsv = new float[3];
79+
        if(scheme == ColorScheme.SPECTRUM)
80+
            hsv[0] = 360f * ((float)i / m);
81+
        else if(scheme == ColorScheme.HIGHCONTRAST) {
82+
            hsv[0] = 360f * angle * i;
83+
            while(hsv[0] > 360f)
84+
                hsv[0] -= 360f;
85+
        }
86+
        hsv[1] = 0.95f;
87+
        hsv[2] = 0.75f;
88+
89+
        return Color.HSVToColor(hsv);
90+
    }
91+
92+
    @Override
93+
    protected void onDraw(Canvas canvas) {
94+
        int color;
95+
        int i = 0;
96+
        int m = strokes.size();
97+
        paint.setTextSize((float)size/24);
98+
        paint.setStyle(Paint.Style.STROKE);
99+
        for(KanjiResult.Stroke s: strokes) {
100+
            color = getColor(i, m, scheme, defaultColor);
101+
            paint.setColor(color);
102+
103+
            // Draw number
104+
            paint.setStrokeWidth(2);
105+
            canvas.drawText(String.valueOf(i),s.getNumX() / 109 * size, s.getNumY() / 109 * size, paint);
106+
107+
            // Draw stroke
108+
            path.set(s.getPath());
109+
            path.computeBounds(rectF, true);
110+
            matrix.setScale((float)size / s.getSize(), (float)size / s.getSize(), 0, 0);
111+
            path.transform(matrix);
112+
113+
            paint.setStrokeWidth((float)size/32);
114+
            canvas.drawPath(path, paint);
115+
116+
            i++;
117+
        }
118+
    }
119+
120+
    @Override
121+
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
122+
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
123+
        setMeasuredDimension(size, size);
124+
    }
125+
}

app/src/main/res/layout/layout_kanji.xml

22
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
33
    android:layout_width="match_parent"
44
    android:layout_height="wrap_content"
5+
    xmlns:app="http://schemas.android.com/apk/res-auto"
56
    android:orientation="horizontal"
67
    android:layout_marginBottom="16dp">
78

2122
            android:layout_marginRight="8dp"
2223
            android:layout_marginEnd="8dp" />
2324
25+
        <eu.lepiller.nani.views.KanjiStrokeView
26+
            android:layout_width="wrap_content"
27+
            android:layout_height="wrap_content"
28+
            android:id="@+id/kanji_stroke_view"
29+
            android:layout_gravity="center"
30+
            android:layout_marginLeft="8dp"
31+
            android:layout_marginStart="8dp"
32+
            android:layout_marginRight="8dp"
33+
            android:layout_marginEnd="8dp"
34+
            app:size="@dimen/huge_kanji_size" />
35+
2436
        <TextView
2537
            android:layout_width="wrap_content"
2638
            android:layout_height="wrap_content"

app/src/main/res/values/attrs.xml

77
        <attr name="pitch" format="integer" />
88
        <attr name="textSize" format="dimension" />
99
    </declare-styleable>
10+
    <declare-styleable name="KanjiStrokeView">
11+
        <attr name="defaultColor" format="color" />
12+
        <attr name="size" format="dimension" />
13+
    </declare-styleable>
1014
</resources>
1014=
1115=
\ No newline at end of file

app/src/main/res/values/dimens.xml

11
<resources>
22
    <dimen name="fab_margin">16dp</dimen>
33
    <dimen name="title_size">32sp</dimen>
4+
    <dimen name="huge_kanji_size">64sp</dimen>
45
    <dimen name="subtitle_size">24sp</dimen>
56
    <dimen name="normal_size">18sp</dimen>
67
    <dimen name="text_margin">16dp</dimen>