Add KanjiVG view
app/src/main/java/eu/lepiller/nani/ResultPagerAdapter.java
19 | 19 | import java.util.List; | |
20 | 20 | import java.util.Map; | |
21 | 21 | ||
22 | + | import eu.lepiller.nani.views.KanjiStrokeView; | |
22 | 23 | import eu.lepiller.nani.views.PitchContourView; | |
23 | 24 | import eu.lepiller.nani.views.PitchDiagramView; | |
24 | 25 | import me.weilunli.views.RubyTextView; | |
… | |||
72 | 73 | TextView kanjiView = child_result.findViewById(R.id.kanji_view); | |
73 | 74 | TextView strokesView = child_result.findViewById(R.id.kanji_strokes); | |
74 | 75 | TextView elementsView = child_result.findViewById(R.id.kanji_elements); | |
76 | + | KanjiStrokeView strokeView = child_result.findViewById(R.id.kanji_stroke_view); | |
75 | 77 | LinearLayout senses_view = child_result.findViewById(R.id.sense_view); | |
76 | 78 | TextView onView = child_result.findViewById(R.id.on_reading); | |
77 | 79 | TextView kunView = child_result.findViewById(R.id.kun_reading); | |
… | |||
80 | 82 | kanjiView.setText(result.getKanji()); | |
81 | 83 | strokesView.setText(String.format(context.getResources().getQuantityString(R.plurals.kanji_stroke, result.getStroke()), | |
82 | 84 | result.getStroke())); | |
85 | + | strokeView.setStrokes(result.getStrokes()); | |
86 | + | if(result.getStrokes() == null) | |
87 | + | strokeView.setVisibility(View.GONE); | |
83 | 88 | ||
84 | 89 | elementsView.setText(String.format(context.getResources().getQuantityString(R.plurals.kanji_elements, result.getElements().size()), | |
85 | 90 | getContent(result.getElements()))); |
app/src/main/java/eu/lepiller/nani/dictionary/KanjiVG.java
1 | 1 | package eu.lepiller.nani.dictionary; | |
2 | 2 | ||
3 | + | import android.graphics.Path; | |
3 | 4 | import android.util.Log; | |
5 | + | import android.util.Pair; | |
4 | 6 | ||
5 | 7 | import java.io.File; | |
6 | 8 | import java.io.FileNotFoundException; | |
… | |||
91 | 93 | return null; | |
92 | 94 | } | |
93 | 95 | ||
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 | + | ||
94 | 244 | KanjiResult.Stroke getStroke(RandomAccessFile file) throws IOException { | |
95 | 245 | String command = getHuffmanString(file, commandHuffman); | |
96 | 246 | String x = getHuffmanString(file, commandHuffman); | |
97 | 247 | 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)); | |
99 | 259 | } | |
100 | 260 | ||
101 | 261 | KanjiResult getValue(RandomAccessFile file, long pos, String kanji) throws IOException { |
app/src/main/java/eu/lepiller/nani/result/KanjiResult.java
1 | 1 | package eu.lepiller.nani.result; | |
2 | 2 | ||
3 | + | import android.graphics.Path; | |
4 | + | ||
3 | 5 | import java.util.ArrayList; | |
4 | 6 | import java.util.List; | |
5 | 7 | import java.util.Objects; | |
… | |||
22 | 24 | } | |
23 | 25 | ||
24 | 26 | 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; | |
31 | 36 | } | |
32 | 37 | ||
33 | - | @Override | |
34 | - | public int hashCode() { | |
35 | - | return Objects.hash(command, posX, posY); | |
38 | + | public Path getPath() { | |
39 | + | return path; | |
36 | 40 | } | |
37 | 41 | ||
38 | - | public String getCommand() { | |
39 | - | return command; | |
42 | + | public float getNumX() { | |
43 | + | return numX; | |
40 | 44 | } | |
41 | 45 | ||
42 | - | public String getPosX() { | |
43 | - | return posX; | |
46 | + | public float getNumY() { | |
47 | + | return numY; | |
44 | 48 | } | |
45 | 49 | ||
46 | - | public String getPosY() { | |
47 | - | return posY; | |
50 | + | public int getSize() { | |
51 | + | return size; | |
48 | 52 | } | |
49 | 53 | } | |
50 | 54 | ||
… | |||
112 | 116 | return elements; | |
113 | 117 | } | |
114 | 118 | ||
119 | + | public List<Stroke> getStrokes() { | |
120 | + | return strokes; | |
121 | + | } | |
122 | + | ||
115 | 123 | public List<String> getKun() { | |
116 | 124 | return kun; | |
117 | 125 | } |
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
2 | 2 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
3 | 3 | android:layout_width="match_parent" | |
4 | 4 | android:layout_height="wrap_content" | |
5 | + | xmlns:app="http://schemas.android.com/apk/res-auto" | |
5 | 6 | android:orientation="horizontal" | |
6 | 7 | android:layout_marginBottom="16dp"> | |
7 | 8 | ||
… | |||
21 | 22 | android:layout_marginRight="8dp" | |
22 | 23 | android:layout_marginEnd="8dp" /> | |
23 | 24 | ||
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 | + | ||
24 | 36 | <TextView | |
25 | 37 | android:layout_width="wrap_content" | |
26 | 38 | android:layout_height="wrap_content" |
app/src/main/res/values/attrs.xml
7 | 7 | <attr name="pitch" format="integer" /> | |
8 | 8 | <attr name="textSize" format="dimension" /> | |
9 | 9 | </declare-styleable> | |
10 | + | <declare-styleable name="KanjiStrokeView"> | |
11 | + | <attr name="defaultColor" format="color" /> | |
12 | + | <attr name="size" format="dimension" /> | |
13 | + | </declare-styleable> | |
10 | 14 | </resources> | |
10 | 14 | = | |
11 | 15 | = | \ No newline at end of file |
app/src/main/res/values/dimens.xml
1 | 1 | <resources> | |
2 | 2 | <dimen name="fab_margin">16dp</dimen> | |
3 | 3 | <dimen name="title_size">32sp</dimen> | |
4 | + | <dimen name="huge_kanji_size">64sp</dimen> | |
4 | 5 | <dimen name="subtitle_size">24sp</dimen> | |
5 | 6 | <dimen name="normal_size">18sp</dimen> | |
6 | 7 | <dimen name="text_margin">16dp</dimen> |