├── demo.png ├── README.md └── src ├── QuadraticOptimizer.java └── FuriganaView.java /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sh0/furigana-view/HEAD/demo.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## FuriganaView ## 2 | 3 | * Copyright (C) 2013 sh0 - sh0 (ät) yutani (dot) ee 4 | * [Licensed under Creative Commons BY-SA 3.0](http://creativecommons.org/licenses/by-sa/3.0/) 5 | 6 | ## General ## 7 | 8 | FuriganaView is a widget for Android. It renders text simlarily to TextView, but adds small lines of furigana on top of Japanese kanji. The furigana has to be supplied to the widget within the text. 9 | 10 | Demo: ![furigana-view](http://yutani.ee/furigana-view/demo.png) 11 | 12 | ## Techical ## 13 | 14 | ### FuriganaView class ### 15 | 16 | TextPaint tp = some_text_view.getPaint(); 17 | String text = "{彼女;かのじょ}は{寒気;さむけ}を{防;ふせ}ぐために{厚;あつ}いコートを{着;き}ていた。"; 18 | int mark_s = 11; // highlight 厚い in text (characters 11-13) 19 | int mark_e = 13; 20 | 21 | FuriganaView m_furigana = (FuriganaView) view.findViewById(R.id.furigana); 22 | m_furigana.text_set(tp, text, mark_s, mark_e); 23 | 24 | 25 | ### QuadraticOptimizer ### 26 | 27 | The placement of multiple furigana labels next to each other is a complicated problem. The first label must be inside the FuriganaView box, every consecutive label must not overlap and the last label must also not clip the FuriganaView rendering box. So for n labels there are n+1 constraints. To give every label equal priority of being as close to the kanji as possible, the cost function of offseting a label from kanji should be quadratic. Therefore a quatratic optimizer should be used to get proper label coordinates. 28 | 29 | Optimization problem: 30 | 31 | * minimize f(x) = sum x[i]^2 over i 32 | * obey constraints Ax > b 33 | 34 | Current solver is absolutly minimal and is not what should be really used. No convergence is checked at any point and in some odd cases the results are incorrect (e.g. some furigana might overlap a bit). 35 | 36 | Solver steps: 37 | 38 | * Use penalty method to turn constrained problem into unconstrained optimization. New cost function is phi(x). 39 | * Use Newton optimizer for solving unconstrained problem. This involves taking first and second derivatives of phi(x) using phi\_d1(x) and phi\_d2(x) respectively. Also an inverse of the Hessian matrix needs to be calculated (or equivalently solving a linear equations problem). 40 | * Gauss-Seidel method is used for solving the linear system of equations for the Newton optimizer. 41 | 42 | This solver pretty much worked on the first go so it has not seen any serious debugging (some commented matrix printing lines still exist in code). The matrices are usually very sparse and with low dimensions so it tends to work pretty well on low width displays. 43 | 44 | Ways to improve current solution: 45 | 46 | * Check for convergence in all solver steps 47 | * Use a better solver for the Hessian inverse than Gauss-Seidel method (like Cholesky factorization) 48 | * Go from float to double (unlikely to help much) 49 | 50 | Proper quadratic problem solvers are difficult to implement. A good and compact solver would be great, but is not on schedule to be implemented right now. 51 | -------------------------------------------------------------------------------- /src/QuadraticOptimizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * FuriganaView widget 3 | * Copyright (C) 2013 sh0 4 | * Licensed under Creative Commons BY-SA 3.0 5 | */ 6 | 7 | // Package 8 | package ee.yutani.furiganaview; 9 | 10 | // Constraint optimizer class 11 | public class QuadraticOptimizer 12 | { 13 | // Constants 14 | static final float m_wolfe_gamma = 0.1f; 15 | static final float m_sigma_mul = 10.0f; 16 | static final int m_penalty_runs = 5; 17 | static final int m_newton_runs = 20; 18 | static final int m_gs_runs = 20; 19 | 20 | // Variables 21 | float[][] m_a; 22 | float[] m_b; 23 | 24 | // Constructor 25 | public QuadraticOptimizer(float[][] a, float[] b) 26 | { 27 | // Variables 28 | m_a = a; 29 | m_b = b; 30 | 31 | // Check 32 | assert(m_b.length == m_a.length); 33 | } 34 | 35 | // Calculate 36 | public void calculate(float[] x) 37 | { 38 | // Check if calculation needed 39 | if (phi(1.0f, x) == 0.0f) 40 | return; 41 | 42 | // Calculate 43 | float sigma = 1.0f; 44 | for (int k = 0; k < m_penalty_runs; k++) { 45 | newton_solve(x, sigma); 46 | sigma *= m_sigma_mul; 47 | } 48 | } 49 | 50 | private void newton_solve(float[] x, float sigma) 51 | { 52 | for (int i = 0; i < m_newton_runs; i++) 53 | newton_iteration(x, sigma); 54 | } 55 | 56 | private void newton_iteration(float[] x, float sigma) 57 | { 58 | // Calculate gradient 59 | float[] d = new float[x.length]; 60 | for (int i = 0; i < d.length; i++) 61 | d[i] = phi_d1(i, sigma, x); 62 | 63 | // Calculate Hessian matrix (symmetric) 64 | float[][] h = new float[x.length][x.length]; 65 | for (int i = 0; i < h.length; i++) 66 | for (int j = i; j < h[0].length; j++) 67 | h[i][j] = phi_d2(i, j, sigma, x); 68 | for (int i = 0; i < h.length; i++) 69 | for (int j = 0; j < i; j++) 70 | h[i][j] = h[j][i]; 71 | 72 | /* 73 | // Debug 74 | //Log.w("newton_solve", "<========================================>"); 75 | Log.w("newton_solve", String.format("phi = %f", phi(sigma, x))); 76 | 77 | // Debug 78 | String str = ""; 79 | for (int i = 0; i < d.length; i++) 80 | str += String.format("%.3f ", d[i]); 81 | Log.w("newton_solve", "d = [ " + str + "]"); 82 | 83 | // Debug 84 | for (int i = 0; i < h.length; i++) { 85 | str = ""; 86 | for (int j = 0; j < h[0].length; j++) 87 | str += String.format("%.3f ", h[i][j]); 88 | Log.w("newton_solve", String.format("h[%02d] = [ %s]", i, str)); 89 | } 90 | */ 91 | 92 | // Linear system solver 93 | float p[] = gs_solver(h, d); 94 | 95 | // Iteration 96 | for (int i = 0; i < x.length; i++) 97 | x[i] = x[i] - (m_wolfe_gamma * p[i]); 98 | 99 | /* 100 | // Debug 101 | str = ""; 102 | for (int i = 0; i < x.length; i++) 103 | str += String.format("%.3f ", x[i]); 104 | Log.w("newton_solve", "x = [ " + str + "]"); 105 | */ 106 | } 107 | 108 | // Gauss-Seidel solver 109 | private float[] gs_solver(float[][] a, float[] b) 110 | { 111 | // Initial guess 112 | float p[] = new float[b.length]; 113 | for (int i = 0; i < p.length; i++) 114 | p[i] = 1.0f; 115 | 116 | for (int z = 0; z < m_gs_runs; z++) { 117 | for (int i = 0; i < p.length; i++) { 118 | float s = 0.0f; 119 | for (int j = 0; j < p.length; j++) { 120 | if (i != j) 121 | s += a[i][j] * p[j]; 122 | } 123 | p[i] = (b[i] - s) / a[i][i]; 124 | } 125 | } 126 | 127 | // Result 128 | return p; 129 | } 130 | 131 | // Math 132 | private float dot(float[] a, float[] b) 133 | { 134 | assert(a.length == b.length); 135 | float r = 0.0f; 136 | for (int i = 0; i < a.length; i++) 137 | r += a[i] * b[i]; 138 | return r; 139 | } 140 | 141 | // Cost function f(x) 142 | private float f(float[] x) 143 | { 144 | return dot(x, x); 145 | } 146 | 147 | // Cost function phi(x) 148 | private float phi(float sigma, float[] x) 149 | { 150 | float r = 0.0f; 151 | for (int i = 0; i < x.length; i++) 152 | r += Math.pow(Math.min(0, dot(m_a[i], x) - m_b[i]), 2.0f); 153 | return f(x) + (sigma * r); 154 | } 155 | 156 | private float phi_d1(int n, float sigma, float[] x) 157 | { 158 | float r = 0.0f; 159 | for (int i = 0; i < m_a.length; i++) { 160 | float c = dot(m_a[i], x) - m_b[i]; 161 | if (c < 0) 162 | r += 2.0f * m_a[i][n] * c; 163 | } 164 | return (2.0f * x[n]) + (sigma * r); 165 | } 166 | 167 | private float phi_d2(int n, int m, float sigma, float[] x) 168 | { 169 | float r = 0.0f; 170 | for (int i = 0; i < m_a.length; i++) { 171 | float c = dot(m_a[i], x) - m_b[i]; 172 | if (c < 0) 173 | r += 2.0f * m_a[i][n] * m_a[i][m]; 174 | } 175 | return ((n == m) ? 2.0f : 0.0f) + (sigma * r); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/FuriganaView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * FuriganaView widget 3 | * Copyright (C) 2013 sh0 4 | * Licensed under Creative Commons BY-SA 3.0 5 | */ 6 | 7 | // Package 8 | package ee.yutani.furiganaview; 9 | 10 | // Imports 11 | import java.util.Vector; 12 | 13 | import android.content.Context; 14 | import android.graphics.Canvas; 15 | import android.text.TextPaint; 16 | import android.util.AttributeSet; 17 | import android.view.View; 18 | 19 | // Text view with furigana display 20 | public class FuriganaView extends View 21 | { 22 | private class TextFurigana 23 | { 24 | // Info 25 | private String m_text; 26 | 27 | // Coordinates 28 | float m_offset = 0.0f; 29 | float m_width = 0.0f; 30 | 31 | // Constructor 32 | public TextFurigana(String text) 33 | { 34 | // Info 35 | m_text = text; 36 | 37 | // Coordinates 38 | m_width = m_paint_f.measureText(m_text); 39 | } 40 | 41 | // Info 42 | //private String text() { return m_text; } 43 | 44 | // Coordinates 45 | public float offset_get() { return m_offset; } 46 | public void offset_set(float value) { m_offset = value; } 47 | public float width() { return m_width; } 48 | 49 | // Draw 50 | public void draw(Canvas canvas, float x, float y) 51 | { 52 | x -= m_width / 2.0f; 53 | canvas.drawText(m_text, 0, m_text.length(), x, y, m_paint_f); 54 | } 55 | } 56 | 57 | private class TextNormal 58 | { 59 | // Info 60 | private String m_text; 61 | private boolean m_is_marked; 62 | 63 | // Widths 64 | private float m_width_total; 65 | private float[] m_width_chars; 66 | 67 | // Constructor 68 | public TextNormal(String text, boolean is_marked) 69 | { 70 | // Info 71 | m_text = text; 72 | m_is_marked = is_marked; 73 | 74 | // Character widths 75 | m_width_chars = new float[m_text.length()]; 76 | if (m_is_marked) { 77 | m_paint_k_mark.getTextWidths(m_text, m_width_chars); 78 | } else { 79 | m_paint_k_norm.getTextWidths(m_text, m_width_chars); 80 | } 81 | 82 | // Total width 83 | m_width_total = 0.0f; 84 | for (float v : m_width_chars) 85 | m_width_total += v; 86 | } 87 | 88 | // Info 89 | public int length() { return m_text.length(); } 90 | 91 | // Widths 92 | public float[] width_chars() { return m_width_chars; } 93 | 94 | // Split 95 | public TextNormal[] split(int offset) 96 | { 97 | return new TextNormal[]{ 98 | new TextNormal(m_text.substring(0, offset), m_is_marked), 99 | new TextNormal(m_text.substring(offset), m_is_marked) 100 | }; 101 | } 102 | 103 | // Draw 104 | public float draw(Canvas canvas, float x, float y) 105 | { 106 | if (m_is_marked) { 107 | canvas.drawText(m_text, 0, m_text.length(), x, y, m_paint_k_mark); 108 | } else { 109 | canvas.drawText(m_text, 0, m_text.length(), x, y, m_paint_k_norm); 110 | } 111 | return m_width_total; 112 | } 113 | } 114 | 115 | private class LineFurigana 116 | { 117 | // Text 118 | private Vector m_text = new Vector(); 119 | private Vector m_offset = new Vector(); 120 | 121 | // Add 122 | public void add(TextFurigana text) 123 | { 124 | if (text != null) 125 | m_text.add(text); 126 | } 127 | 128 | // Calculate 129 | public void calculate() 130 | { 131 | // Check size 132 | if (m_text.size() == 0) 133 | return; 134 | 135 | /* 136 | // Debug 137 | String str = ""; 138 | for (TextFurigana text : m_text) 139 | str += "'" + text.text() + "' "; 140 | */ 141 | 142 | // r[] - ideal offsets 143 | float[] r = new float[m_text.size()]; 144 | for (int i = 0; i < m_text.size(); i++) 145 | r[i] = m_text.get(i).offset_get(); 146 | 147 | // a[] - constraint matrix 148 | float[][] a = new float[m_text.size() + 1][m_text.size()]; 149 | for (int i = 0; i < a.length; i++) 150 | for (int j = 0; j < a[0].length; j++) 151 | a[i][j] = 0.0f; 152 | a[0][0] = 1.0f; 153 | for (int i = 1; i < a.length - 2; i++) { 154 | a[i][i - 1] = -1.0f; 155 | a[i][i] = 1.0f; 156 | } 157 | a[a.length - 1][a[0].length - 1] = -1.0f; 158 | 159 | // b[] - constraint vector 160 | float[] b = new float[m_text.size() + 1]; 161 | b[0] = -r[0] + (0.5f * m_text.get(0).width()); 162 | for (int i = 1; i < b.length - 2; i++) 163 | b[i] = (0.5f * (m_text.get(i).width() + m_text.get(i - 1).width())) + (r[i - 1] - r[i]); 164 | b[b.length - 1] = -m_linemax + r[r.length -1] + (0.5f * m_text.get(m_text.size() - 1).width()); 165 | 166 | // Calculate constraint optimization 167 | float[] x = new float[m_text.size()]; 168 | for (int i = 0; i < x.length; i++) 169 | x[i] = 0.0f; 170 | QuadraticOptimizer co = new QuadraticOptimizer(a, b); 171 | co.calculate(x); 172 | for (int i = 0; i < x.length; i++) 173 | m_offset.add(x[i] + r[i]); 174 | } 175 | 176 | // Draw 177 | public void draw(Canvas canvas, float y) 178 | { 179 | y -= m_paint_f.descent(); 180 | if (m_offset.size() == m_text.size()) { 181 | // Render with fixed offsets 182 | for (int i = 0; i < m_offset.size(); i++) 183 | m_text.get(i).draw(canvas, m_offset.get(i), y); 184 | } else { 185 | // Render with original offsets 186 | for (TextFurigana text : m_text) 187 | text.draw(canvas, text.offset_get(), y); 188 | } 189 | } 190 | } 191 | 192 | private class LineNormal 193 | { 194 | // Text 195 | private Vector m_text = new Vector(); 196 | 197 | // Elements 198 | public int size() { return m_text.size(); } 199 | public void add(Vector text) { m_text.addAll(text); } 200 | 201 | // Draw 202 | public void draw(Canvas canvas, float y) 203 | { 204 | y -= m_paint_k_norm.descent(); 205 | float x = 0.0f; 206 | for (TextNormal text : m_text) 207 | x += text.draw(canvas, x, y); 208 | } 209 | } 210 | 211 | private class Span 212 | { 213 | // Text 214 | private TextFurigana m_furigana = null; 215 | private Vector m_normal = new Vector(); 216 | 217 | // Widths 218 | private Vector m_width_chars = new Vector(); 219 | private float m_width_total = 0.0f; 220 | 221 | // Constructors 222 | public Span(String text_f, String text_k, int mark_s, int mark_e) 223 | { 224 | // Furigana text 225 | if (text_f.length() > 0) 226 | m_furigana = new TextFurigana(text_f); 227 | 228 | // Normal text 229 | if (mark_s < text_k.length() && mark_e > 0 && mark_s < mark_e) { 230 | 231 | // Fix marked bounds 232 | mark_s = Math.max(0, mark_s); 233 | mark_e = Math.min(text_k.length(), mark_e); 234 | 235 | // Prefix 236 | if (mark_s > 0) 237 | m_normal.add(new TextNormal(text_k.substring(0, mark_s), false)); 238 | 239 | // Marked 240 | if (mark_e > mark_s) 241 | m_normal.add(new TextNormal(text_k.substring(mark_s, mark_e), true)); 242 | 243 | // Postfix 244 | if (mark_e < text_k.length()) 245 | m_normal.add(new TextNormal(text_k.substring(mark_e), false)); 246 | 247 | } else { 248 | 249 | // Non marked 250 | m_normal.add(new TextNormal(text_k, false)); 251 | 252 | } 253 | 254 | // Widths 255 | widths_calculate(); 256 | } 257 | 258 | public Span(Vector normal) 259 | { 260 | // Only normal text 261 | m_normal = normal; 262 | 263 | // Widths 264 | widths_calculate(); 265 | } 266 | 267 | // Text 268 | public TextFurigana furigana(float x) { 269 | if (m_furigana == null) 270 | return null; 271 | m_furigana.offset_set(x + (m_width_total / 2.0f)); 272 | return m_furigana; 273 | } 274 | public Vector normal() { return m_normal; } 275 | 276 | // Widths 277 | public Vector widths() { return m_width_chars; } 278 | private void widths_calculate() 279 | { 280 | // Chars 281 | if (m_furigana == null) { 282 | for (TextNormal normal : m_normal) 283 | for (float v : normal.width_chars()) 284 | m_width_chars.add(v); 285 | } else { 286 | float sum = 0.0f; 287 | for (TextNormal normal : m_normal) 288 | for (float v : normal.width_chars()) 289 | sum += v; 290 | m_width_chars.add(sum); 291 | } 292 | 293 | // Total 294 | m_width_total = 0.0f; 295 | for (float v : m_width_chars) 296 | m_width_total += v; 297 | } 298 | 299 | // Split 300 | public void split(int offset, Vector normal_a, Vector normal_b) 301 | { 302 | // Check if no furigana 303 | assert(m_furigana == null); 304 | 305 | // Split normal list 306 | for (TextNormal cur : m_normal) { 307 | if (offset <= 0) { 308 | normal_b.add(cur); 309 | } else if (offset >= cur.length()) { 310 | normal_a.add(cur); 311 | } else { 312 | TextNormal[] split = cur.split(offset); 313 | normal_a.add(split[0]); 314 | normal_b.add(split[1]); 315 | } 316 | offset -= cur.length(); 317 | } 318 | } 319 | } 320 | 321 | // Paints 322 | private TextPaint m_paint_f = new TextPaint(); 323 | private TextPaint m_paint_k_norm = new TextPaint(); 324 | private TextPaint m_paint_k_mark = new TextPaint(); 325 | 326 | // Sizes 327 | private float m_linesize = 0.0f; 328 | private float m_height_n = 0.0f; 329 | private float m_height_f = 0.0f; 330 | private float m_linemax = 0.0f; 331 | 332 | // Spans and lines 333 | private Vector m_span = new Vector(); 334 | private Vector m_line_n = new Vector(); 335 | private Vector m_line_f = new Vector(); 336 | 337 | // Constructors 338 | public FuriganaView(Context context) { super(context); } 339 | public FuriganaView(Context context, AttributeSet attrs) { super(context, attrs); } 340 | public FuriganaView(Context context, AttributeSet attrs, int style) { super(context, attrs, style); } 341 | 342 | // Text functions 343 | public void text_set(TextPaint tp, String text, int mark_s, int mark_e) 344 | { 345 | // Text 346 | m_paint_k_norm = new TextPaint(tp); 347 | m_paint_k_mark = new TextPaint(tp); 348 | m_paint_k_mark.setFakeBoldText(true); 349 | m_paint_f = new TextPaint(tp); 350 | m_paint_f.setTextSize(m_paint_f.getTextSize() / 2.0f); 351 | 352 | // Linesize 353 | m_height_n = m_paint_k_norm.descent() - m_paint_k_norm.ascent(); 354 | m_height_f = m_paint_f.descent() - m_paint_f.ascent(); 355 | m_linesize = m_height_n + m_height_f; 356 | 357 | // Clear spans 358 | m_span.clear(); 359 | 360 | // Sizes 361 | m_linesize = m_paint_f.getFontSpacing() + Math.max(m_paint_k_norm.getFontSpacing(), m_paint_k_mark.getFontSpacing()); 362 | 363 | // Spannify text 364 | while (text.length() > 0) { 365 | int idx = text.indexOf('{'); 366 | if (idx >= 0) { 367 | // Prefix string 368 | if (idx > 0) { 369 | // Spans 370 | m_span.add(new Span("", text.substring(0, idx), mark_s, mark_e)); 371 | 372 | // Remove text 373 | text = text.substring(idx); 374 | mark_s -= idx; 375 | mark_e -= idx; 376 | } 377 | 378 | // End bracket 379 | idx = text.indexOf('}'); 380 | if (idx < 1) { 381 | // Error 382 | text = ""; 383 | break; 384 | } else if (idx == 1) { 385 | // Empty bracket 386 | text = text.substring(2); 387 | continue; 388 | } 389 | 390 | // Spans 391 | String[] split = text.substring(1, idx).split(";"); 392 | m_span.add(new Span(((split.length > 1) ? split[1] : ""), split[0], mark_s, mark_e)); 393 | 394 | // Remove text 395 | text = text.substring(idx + 1); 396 | mark_s -= split[0].length(); 397 | mark_e -= split[0].length(); 398 | 399 | } else { 400 | // Single span 401 | m_span.add(new Span("", text, mark_s, mark_e)); 402 | text = ""; 403 | } 404 | } 405 | 406 | // Invalidate view 407 | this.invalidate(); 408 | this.requestLayout(); 409 | } 410 | 411 | // Size calculation 412 | @Override protected void onMeasure(int width_ms, int height_ms) 413 | { 414 | // Modes 415 | int wmode = View.MeasureSpec.getMode(width_ms); 416 | int hmode = View.MeasureSpec.getMode(height_ms); 417 | 418 | // Dimensions 419 | int wold = View.MeasureSpec.getSize(width_ms); 420 | int hold = View.MeasureSpec.getSize(height_ms); 421 | 422 | // Draw mode 423 | if (wmode == View.MeasureSpec.EXACTLY || wmode == View.MeasureSpec.AT_MOST && wold > 0) { 424 | // Width limited 425 | text_calculate(wold); 426 | } else { 427 | // Width unlimited 428 | text_calculate(-1.0f); 429 | } 430 | 431 | // New height 432 | int hnew = (int)Math.round(Math.ceil(m_linesize * (float)m_line_n.size())); 433 | int wnew = wold; 434 | if (wmode != View.MeasureSpec.EXACTLY && m_line_n.size() <= 1) 435 | wnew = (int)Math.round(Math.ceil(m_linemax)); 436 | if (hmode != View.MeasureSpec.UNSPECIFIED && hnew > hold) 437 | hnew |= MEASURED_STATE_TOO_SMALL; 438 | 439 | // Set result 440 | setMeasuredDimension(wnew, hnew); 441 | } 442 | 443 | private void text_calculate(float line_max) 444 | { 445 | // Clear lines 446 | m_line_n.clear(); 447 | m_line_f.clear(); 448 | 449 | // Sizes 450 | m_linemax = 0.0f; 451 | 452 | // Check if no limits on width 453 | if (line_max < 0.0) { 454 | 455 | // Create single normal and furigana line 456 | LineNormal line_n = new LineNormal(); 457 | LineFurigana line_f = new LineFurigana(); 458 | 459 | // Loop spans 460 | for (Span span : m_span) { 461 | // Text 462 | line_n.add(span.normal()); 463 | line_f.add(span.furigana(m_linemax)); 464 | 465 | // Widths update 466 | for (float width : span.widths()) 467 | m_linemax += width; 468 | } 469 | 470 | // Commit both lines 471 | m_line_n.add(line_n); 472 | m_line_f.add(line_f); 473 | 474 | } else { 475 | 476 | // Lines 477 | float line_x = 0.0f; 478 | LineNormal line_n = new LineNormal(); 479 | LineFurigana line_f = new LineFurigana(); 480 | 481 | // Initial span 482 | int span_i = 0; 483 | Span span = m_span.get(span_i); 484 | 485 | // Iterate 486 | while (span != null) { 487 | // Start offset 488 | float line_s = line_x; 489 | 490 | // Calculate possible line size 491 | Vector widths = span.widths(); 492 | int i = 0; 493 | for (i = 0; i < widths.size(); i++) { 494 | if (line_x + widths.get(i) <= line_max) 495 | line_x += widths.get(i); 496 | else 497 | break; 498 | } 499 | 500 | // Add span to line 501 | if (i >= 0 && i < widths.size()) { 502 | 503 | // Span does not fit entirely 504 | if (i > 0) { 505 | // Split half that fits 506 | Vector normal_a = new Vector(); 507 | Vector normal_b = new Vector(); 508 | span.split(i, normal_a, normal_b); 509 | line_n.add(normal_a); 510 | span = new Span(normal_b); 511 | } 512 | 513 | // Add new line with current spans 514 | if (line_n.size() != 0) { 515 | // Add 516 | m_linemax = (m_linemax > line_x ? m_linemax : line_x); 517 | m_line_n.add(line_n); 518 | m_line_f.add(line_f); 519 | 520 | // Reset 521 | line_n = new LineNormal(); 522 | line_f = new LineFurigana(); 523 | line_x = 0.0f; 524 | 525 | // Next span 526 | continue; 527 | } 528 | 529 | } else { 530 | 531 | // Span fits entirely 532 | line_n.add(span.normal()); 533 | line_f.add(span.furigana(line_s)); 534 | 535 | } 536 | 537 | // Next span 538 | span = null; 539 | span_i++; 540 | if (span_i < m_span.size()) 541 | span = m_span.get(span_i); 542 | } 543 | 544 | // Last span 545 | if (line_n.size() != 0) { 546 | // Add 547 | m_linemax = (m_linemax > line_x ? m_linemax : line_x); 548 | m_line_n.add(line_n); 549 | m_line_f.add(line_f); 550 | } 551 | } 552 | 553 | // Calculate furigana 554 | for (LineFurigana line : m_line_f) 555 | line.calculate(); 556 | } 557 | 558 | // Drawing 559 | @Override public void onDraw(Canvas canvas) 560 | { 561 | /* 562 | // Debug background 563 | Paint paint = new Paint(); 564 | paint.setARGB(0x30, 0, 0, 0xff); 565 | Rect rect = new Rect(); 566 | canvas.getClipBounds(rect); 567 | canvas.drawRect(rect, paint); 568 | */ 569 | 570 | // Check 571 | assert(m_line_n.size() == m_line_f.size()); 572 | 573 | // Coordinates 574 | float y = m_linesize; 575 | 576 | // Loop lines 577 | for (int i = 0; i < m_line_n.size(); i++) { 578 | m_line_n.get(i).draw(canvas, y); 579 | m_line_f.get(i).draw(canvas, y - m_height_n); 580 | y += m_linesize; 581 | } 582 | } 583 | } 584 | --------------------------------------------------------------------------------