Add furigana

Julien LepillerMon Apr 15 20:09:48+0200 2019

9b9ed2f

Add furigana

.idea/gradle.xml

33
  <component name="GradleSettings">
44
    <option name="linkedExternalProjectsSettings">
55
      <GradleProjectSettings>
6-
        <compositeConfiguration>
7-
          <compositeBuild compositeDefinitionSource="SCRIPT" />
8-
        </compositeConfiguration>
96
        <option name="distributionType" value="DEFAULT_WRAPPED" />
107
        <option name="externalProjectPath" value="$PROJECT_DIR$" />
8+
        <option name="modules">
9+
          <set>
10+
            <option value="$PROJECT_DIR$" />
11+
            <option value="$PROJECT_DIR$/app" />
12+
            <option value="$PROJECT_DIR$/furiganatextview" />
13+
          </set>
14+
        </option>
1115
        <option name="resolveModulePerSourceSet" value="false" />
1216
      </GradleProjectSettings>
1317
    </option>

app/build.gradle

2626
    testImplementation 'junit:junit:4.12'
2727
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
2828
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
29+
    compile project(path: ':furiganatextview')
2930
}

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

1616
1717
import eu.lepiller.nani.dictionary.DictionaryFactory;
1818
import eu.lepiller.nani.result.Result;
19+
import se.fekete.furiganatextview.furiganaview.FuriganaTextView;
1920
2021
public class MainActivity extends AppCompatActivity {
2122

4445
                        break;
4546
                    View child_result = getLayoutInflater().inflate(R.layout.layout_result, result_view, false);
4647
47-
                    TextView kanji_view = child_result.findViewById(R.id.kanji_view);
48+
                    FuriganaTextView kanji_view = child_result.findViewById(R.id.kanji_view);
4849
                    LinearLayout senses_view = child_result.findViewById(R.id.sense_view);
4950
                    TextView additional_info = child_result.findViewById(R.id.additional_info_view);
5051
5152
                    // Populate the data into the template view using the data object
52-
                    kanji_view.setText(result.getKanji());
53+
                    kanji_view.setFuriganaText(result.getKanjiFurigana());
5354
5455
                    StringBuilder additional = new StringBuilder();
5556
                    boolean separator = false;

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

11
package eu.lepiller.nani.result;
22
3+
import android.util.Log;
4+
35
import java.util.ArrayList;
6+
import java.util.regex.Matcher;
7+
import java.util.regex.Pattern;
8+
9+
import static java.lang.Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS;
410
511
public class Result {
612
    public static class Source {

5965
    }
6066
6167
    public String getKanji() {
62-
        String k = "";
68+
        String k = getReading();
6369
        if(kanjis.size() > 0)
6470
            k = kanjis.get(0);
6571
        return k;

7682
    public ArrayList<Reading> getReadings() {
7783
        return readings;
7884
    }
85+
86+
    public String getReading() {
87+
        String reading = "";
88+
        if(readings.size() > 0) {
89+
            ArrayList<String> rs = readings.get(0).readings;
90+
            if(rs.size() > 0)
91+
                reading = rs.get(0);
92+
        }
93+
        return reading;
94+
    }
95+
96+
    public String getKanjiFurigana() {
97+
        String txt = getKanji();
98+
        String reading = getReading();
99+
        Log.d("RESULT", "reading: " + reading);
100+
101+
        // split the text into kanji / not kanji portions
102+
        ArrayList<String> portions = new ArrayList<>();
103+
104+
        StringBuilder current = new StringBuilder();
105+
        Character.UnicodeBlock b = CJK_UNIFIED_IDEOGRAPHS;
106+
107+
        for(int i=0; i<txt.length(); i++) {
108+
            Character.UnicodeBlock b2 = Character.UnicodeBlock.of(txt.charAt(i));
109+
            if(b == b2) {
110+
                current.append(txt.charAt(i));
111+
            } else {
112+
                String s = current.toString();
113+
                if(!s.isEmpty())
114+
                    portions.add(s);
115+
                current = new StringBuilder();
116+
                current.append(txt.charAt(i));
117+
            }
118+
119+
            b = b2;
120+
        }
121+
        String str = current.toString();
122+
        if(!str.isEmpty())
123+
            portions.add(str);
124+
125+
        // Create a regexp to match kanji places
126+
        current = new StringBuilder();
127+
        current.append("^");
128+
        for(String s: portions) {
129+
            if(Character.UnicodeBlock.of(s.charAt(0)) == CJK_UNIFIED_IDEOGRAPHS) {
130+
                current.append("(.*)");
131+
            } else {
132+
                current.append(s);
133+
            }
134+
        }
135+
        current.append("$");
136+
137+
        Log.d("RESULT", "regex: " + current.toString());
138+
139+
        Pattern p = Pattern.compile(current.toString());
140+
        Matcher m = p.matcher(reading);
141+
142+
        if(!m.matches()) {
143+
            Log.d("RESULT", "Finaly: " + txt);
144+
            return txt;
145+
        }
146+
147+
        // We have a match!
148+
149+
        Log.d("RESULT", "matched");
150+
151+
        current = new StringBuilder();
152+
        int group = 1;
153+
        for(String s: portions) {
154+
            if(Character.UnicodeBlock.of(s.charAt(0)) == CJK_UNIFIED_IDEOGRAPHS) {
155+
                current.append("<ruby>");
156+
                current.append(s);
157+
                current.append("<rt>");
158+
                current.append(m.group(group));
159+
                current.append("</rt>");
160+
                current.append("</ruby>");
161+
                group++;
162+
            } else {
163+
                current.append(s);
164+
            }
165+
        }
166+
        Log.d("RESULT", "Finaly: " + current.toString());
167+
        return current.toString();
168+
    }
79169
}

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

11
<?xml version="1.0" encoding="utf-8"?>
2-
<LinearLayout
3-
    xmlns:android="http://schemas.android.com/apk/res/android"
2+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
43
    android:layout_width="match_parent"
54
    android:layout_height="wrap_content"
5+
    xmlns:app="http://schemas.android.com/apk/res-auto"
66
    android:layout_marginBottom="32dp">
77
8-
    <TextView
8+
    <se.fekete.furiganatextview.furiganaview.FuriganaTextView
99
        android:id="@+id/kanji_view"
10-
        android:layout_width="96dp"
10+
        android:layout_width="wrap_content"
1111
        android:layout_height="wrap_content"
1212
        android:layout_gravity="center"
1313
        android:layout_weight="0"
14-
        android:contentDescription="@string/icon_description"
14+
        android:layout_marginLeft="8dp"
15+
        android:layout_marginStart="8dp"
16+
        android:contentDescription="@string/kanji_description"
1517
        android:textIsSelectable="true"
18+
        app:contains_ruby_tags="true"
1619
        android:textSize="@dimen/title_size" />
1720
1821
    <LinearLayout

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

1111
    <string name="download">Download</string>
1212
    <string name="remove">Remove</string>
1313
    <string name="icon_description">Icon</string>
14+
    <string name="kanji_description">Writing</string>
1415
1516
    <!-- Dictionnary descriptions -->
1617
    <string name="dico_jmdict_example">Japanese/English dictionary for test purposes. Do not use.</string>

build.gradle

11
// Top-level build file where you can add configuration options common to all sub-projects/modules.
22
33
buildscript {
4+
    ext.kotlin_version = '1.3.20'
45
    repositories {
56
        google()
67
        jcenter()
78
        
89
    }
910
    dependencies {
10-
        classpath 'com.android.tools.build:gradle:3.3.1'
11-
        
11+
        classpath 'com.android.tools.build:gradle:3.3.2'
12+
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
13+
1214
        // NOTE: Do not place your application dependencies here; they belong
1315
        // in the individual module build.gradle files
1416
    }

furiganatextview/.gitignore unknown status 1

1+
/build

furiganatextview/build.gradle unknown status 1

1+
apply plugin: 'com.android.library'
2+
apply plugin: 'kotlin-android'
3+
4+
android {
5+
    compileSdkVersion 28
6+
7+
    defaultConfig {
8+
        minSdkVersion 15
9+
        targetSdkVersion 28
10+
        versionCode 1
11+
        versionName "1.0"
12+
    }
13+
    buildTypes {
14+
        release {
15+
            minifyEnabled false
16+
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
17+
        }
18+
    }
19+
}
20+
21+
dependencies {
22+
    implementation fileTree(dir: 'libs', include: ['*.jar'])
23+
    testImplementation 'junit:junit:4.12'
24+
    implementation 'com.android.support:appcompat-v7:28.0.0'
25+
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
26+
}
27+
repositories {
28+
    mavenCentral()
29+
}

furiganatextview/proguard-rules.pro unknown status 1

1+
# Add project specific ProGuard rules here.
2+
# By default, the flags in this file are appended to flags specified
3+
# in /Users/lorand/Android_SDK/sdk/tools/proguard/proguard-android.txt
4+
# You can edit the include path and order by changing the proguardFiles
5+
# directive in build.gradle.
6+
#
7+
# For more details, see
8+
#   http://developer.android.com/guide/developing/tools/proguard.html
9+
10+
# Add any project specific keep options here:
11+
12+
# If your project uses WebView with JS, uncomment the following
13+
# and specify the fully qualified class name to the JavaScript interface
14+
# class:
15+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16+
#   public *;
17+
#}

furiganatextview/src/androidTest/java/eu/lepiller/furiganatextview/ExampleInstrumentedTest.java unknown status 1

1+
package eu.lepiller.furiganatextview;
2+
3+
import android.content.Context;
4+
import android.support.test.InstrumentationRegistry;
5+
import android.support.test.runner.AndroidJUnit4;
6+
7+
import org.junit.Test;
8+
import org.junit.runner.RunWith;
9+
10+
import static org.junit.Assert.*;
11+
12+
/**
13+
 * Instrumented test, which will execute on an Android device.
14+
 *
15+
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
16+
 */
17+
@RunWith(AndroidJUnit4.class)
18+
public class ExampleInstrumentedTest {
19+
    @Test
20+
    public void useAppContext() {
21+
        // Context of the app under test.
22+
        Context appContext = InstrumentationRegistry.getTargetContext();
23+
24+
        assertEquals("eu.lepiller.furiganatextview.test", appContext.getPackageName());
25+
    }
26+
}

furiganatextview/src/androidTest/java/se/fekete/furiganatextview/ApplicationTest.java unknown status 1

1+
package se.fekete.furiganatextview;
2+
3+
import android.app.Application;
4+
import android.test.ApplicationTestCase;
5+
6+
/**
7+
 * <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
8+
 */
9+
public class ApplicationTest extends ApplicationTestCase<Application> {
10+
    public ApplicationTest() {
11+
        super(Application.class);
12+
    }
13+
}
13<
014<
\ No newline at end of file

furiganatextview/src/main/AndroidManifest.xml unknown status 1

1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
    package="se.fekete.furiganatextview">
3+
4+
    <application android:allowBackup="true"
5+
        android:label="@string/app_name"
6+
        android:supportsRtl="true"
7+
>
8+
9+
    </application>
10+
11+
</manifest>

furiganatextview/src/main/java/se/fekete/furiganatextview/furiganaview/FuriganaTextView.kt unknown status 1

1+
/*
2+
 * FuriganaView widget
3+
 * Copyright (C) 2013 sh0 <sh0@yutani.ee>
4+
 * Licensed under Creative Commons BY-SA 3.0
5+
 */
6+
7+
package se.fekete.furiganatextview.furiganaview
8+
9+
import android.content.Context
10+
import android.graphics.Canvas
11+
import android.text.SpannableString
12+
import android.text.TextPaint
13+
import android.util.AttributeSet
14+
import android.view.View
15+
import android.widget.TextView
16+
import se.fekete.furiganatextview.R
17+
import java.util.*
18+
19+
class FuriganaTextView : TextView {
20+
21+
    // Paints
22+
    private var textPaintFurigana = TextPaint()
23+
    private var textPaintNormal = TextPaint()
24+
25+
    // Sizes
26+
    private var lineSize = 0.0f
27+
    private var normalHeight = 0.0f
28+
    private var furiganaHeight = 0.0f
29+
    private var lineMax = 0.0f
30+
31+
    // Spans and lines
32+
    private val spans = Vector<Span>()
33+
    private val normalLines = Vector<LineNormal>()
34+
    private val furiganaLines = Vector<LineFurigana>()
35+
36+
    //attributes
37+
    private var hasRuby: Boolean = false
38+
    private var furiganaTextColor: Int = 0
39+
40+
    // Constructors
41+
    constructor(context: Context) : super(context) {
42+
        initialize()
43+
    }
44+
45+
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
46+
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.FuriganaTextView, 0, 0)
47+
        try {
48+
            hasRuby = typedArray.getBoolean(R.styleable.FuriganaTextView_contains_ruby_tags, false)
49+
            furiganaTextColor = typedArray.getColor(R.styleable.FuriganaTextView_furigana_text_color, 0)
50+
        } finally {
51+
            typedArray.recycle()
52+
        }
53+
54+
        initialize()
55+
    }
56+
57+
    private fun initialize() {
58+
        val viewText = text
59+
        if (viewText.isNotEmpty()) {
60+
            if (viewText is String)
61+
                setFuriganaText(viewText, hasRuby)
62+
            else
63+
                setFuriganaText((viewText as SpannableString).toString(), hasRuby)
64+
        }
65+
    }
66+
67+
    /**
68+
     * The method parseRuby converts kanji enclosed in ruby tags to the
69+
     * format which is supported by the textview {Kanji:furigana}
70+
71+
     * @param textWithRuby
72+
     * The text string with Kanji enclosed in ruby tags.
73+
     */
74+
    private fun replaceRuby(textWithRuby: String): String {
75+
        var parsed = textWithRuby.replace("<ruby>", "{")
76+
        parsed = parsed.replace("<rt>", ";")
77+
        parsed = parsed.replace("</rt>", "")
78+
79+
        return parsed.replace("</ruby>", "}")
80+
    }
81+
82+
    override fun setTextColor(color: Int) {
83+
        super.setTextColor(color)
84+
        invalidate()
85+
    }
86+
87+
    fun setFuriganaText(text: String) {
88+
        setFuriganaText(text, hasRuby = false)
89+
    }
90+
91+
    fun setFuriganaText(text: String, hasRuby: Boolean) {
92+
        super.setText(text)
93+
94+
        var textToDisplay = text
95+
        if (this.hasRuby || hasRuby) {
96+
            textToDisplay = replaceRuby(text)
97+
        }
98+
99+
        setText(paint, textToDisplay, 0, 0)
100+
    }
101+
102+
    private fun setText(tp: TextPaint, text: String, markS: Int, markE: Int) {
103+
        var mutableText = text
104+
        var mutableMarkS = markS
105+
        var mutableMarkE = markE
106+
107+
        // Text
108+
        textPaintNormal = TextPaint(tp)
109+
        textPaintFurigana = TextPaint(tp)
110+
        textPaintFurigana.textSize = textPaintFurigana.textSize / 2.0f
111+
112+
        // Line size
113+
        normalHeight = textPaintNormal.descent() - textPaintNormal.ascent()
114+
        furiganaHeight = textPaintFurigana.descent() - textPaintFurigana.ascent()
115+
        lineSize = normalHeight + furiganaHeight
116+
117+
        // Clear spans
118+
        spans.clear()
119+
120+
        // Sizes
121+
        lineSize = textPaintFurigana.fontSpacing + Math.max(textPaintNormal.fontSpacing, 0f)
122+
123+
        // Spannify text
124+
        while (mutableText.isNotEmpty()) {
125+
            var idx = mutableText.indexOf('{')
126+
            if (idx >= 0) {
127+
                // Prefix string
128+
                if (idx > 0) {
129+
                    // Spans
130+
                    spans.add(Span("", mutableText.substring(0, idx), mutableMarkS, mutableMarkE, textPaintNormal, textPaintFurigana))
131+
132+
                    // Remove text
133+
                    mutableText = mutableText.substring(idx)
134+
                    mutableMarkS -= idx
135+
                    mutableMarkE -= idx
136+
                }
137+
138+
                // End bracket
139+
                idx = mutableText.indexOf('}')
140+
                if (idx < 1) {
141+
                    // Error
142+
                    break
143+
                } else if (idx == 1) {
144+
                    // Empty bracket
145+
                    mutableText = mutableText.substring(2)
146+
                    continue
147+
                }
148+
149+
                // Spans
150+
                val split = mutableText.substring(1, idx).split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
151+
                spans.add(Span(if (split.size > 1) split[1] else "", split[0], mutableMarkS, mutableMarkE, textPaintNormal, textPaintFurigana))
152+
153+
                // Remove text
154+
                mutableText = mutableText.substring(idx + 1)
155+
                mutableMarkS -= split[0].length
156+
                mutableMarkE -= split[0].length
157+
158+
            } else {
159+
                // Single span
160+
                spans.add(Span("", mutableText, mutableMarkS, mutableMarkE, textPaintNormal, textPaintFurigana))
161+
                mutableText = ""
162+
            }
163+
        }
164+
165+
        // Invalidate view
166+
        this.invalidate()
167+
        this.requestLayout()
168+
    }
169+
170+
    // Size calculation
171+
    override fun onMeasure(width_ms: Int, height_ms: Int) {
172+
        // Modes
173+
        val wmode = View.MeasureSpec.getMode(width_ms)
174+
        val hmode = View.MeasureSpec.getMode(height_ms)
175+
176+
        // Dimensions
177+
        val wold = View.MeasureSpec.getSize(width_ms)
178+
        val hold = View.MeasureSpec.getSize(height_ms)
179+
180+
        if (text.isNotEmpty()) {
181+
            // Draw mode
182+
            if (wmode == View.MeasureSpec.EXACTLY || wmode == View.MeasureSpec.AT_MOST && wold > 0) {
183+
                // Width limited
184+
                calculateText(wold.toFloat())
185+
            } else {
186+
                // Width unlimited
187+
                calculateText(-1.0f)
188+
            }
189+
        }
190+
191+
        // New height
192+
        var hnew = Math.round(Math.ceil((lineSize * normalLines.size.toFloat()).toDouble())).toInt()
193+
        var wnew = wold
194+
        if (wmode != View.MeasureSpec.EXACTLY && normalLines.size <= 1)
195+
            wnew = Math.round(Math.ceil(lineMax.toDouble())).toInt()
196+
        if (hmode != View.MeasureSpec.UNSPECIFIED && hnew > hold)
197+
            hnew = hnew or View.MEASURED_STATE_TOO_SMALL
198+
199+
        // Set result
200+
        setMeasuredDimension(wnew, hnew)
201+
    }
202+
203+
    private fun calculateText(lineMax: Float) {
204+
        // Clear lines
205+
        normalLines.clear()
206+
        furiganaLines.clear()
207+
208+
        // Sizes
209+
        this.lineMax = 0.0f
210+
211+
        // Check if no limits on width
212+
        if (lineMax < 0.0) {
213+
214+
            // Create single normal and furigana line
215+
            val lineN = LineNormal(textPaintNormal)
216+
            val lineF = LineFurigana(this.lineMax, textPaintFurigana)
217+
218+
            // Loop spans
219+
            for (span in spans) {
220+
                // Text
221+
                lineN.add(span.normal())
222+
                lineF.add(span.furigana(this.lineMax))
223+
224+
                // Widths update
225+
                for (width in span.widths())
226+
                    this.lineMax += width
227+
            }
228+
229+
            // Commit both lines
230+
            normalLines.add(lineN)
231+
            furiganaLines.add(lineF)
232+
233+
        } else {
234+
235+
            // Lines
236+
            var lineX = 0.0f
237+
            var lineN = LineNormal(textPaintNormal)
238+
            var lineF = LineFurigana(this.lineMax, textPaintFurigana)
239+
240+
            // Initial span
241+
            var spanI = 0
242+
            var span: Span? = if (spans.isNotEmpty()) spans[spanI] else null
243+
244+
            // Iterate
245+
            while (span != null) {
246+
                // Start offset
247+
                val lineS = lineX
248+
249+
                // Calculate possible line size
250+
                val widths = span.widths()
251+
                var i = 0
252+
                while (i < widths.size) {
253+
                    if (lineX + widths[i] <= lineMax) {
254+
                        lineX += widths[i]
255+
                    } else {
256+
                        break
257+
                    }
258+
                    i++
259+
                }
260+
261+
                // Add span to line
262+
                if (i >= 0 && i < widths.size) {
263+
264+
                    // Span does not fit entirely
265+
                    if (i > 0) {
266+
                        // Split half that fits
267+
                        val normalA = Vector<TextNormal>()
268+
                        val normalB = Vector<TextNormal>()
269+
                        span.split(i, normalA, normalB)
270+
                        lineN.add(normalA)
271+
                        span = Span(normalB)
272+
                    }
273+
274+
                    // Add new line with current spans
275+
                    if (lineN.size() != 0) {
276+
                        // Add
277+
                        this.lineMax = if (this.lineMax > lineX) this.lineMax else lineX
278+
                        normalLines.add(lineN)
279+
                        furiganaLines.add(lineF)
280+
281+
                        // Reset
282+
                        lineN = LineNormal(textPaintNormal)
283+
                        lineF = LineFurigana(this.lineMax, textPaintFurigana)
284+
                        lineX = 0.0f
285+
286+
                        // Next span
287+
                        continue
288+
                    }
289+
290+
                } else {
291+
292+
                    // Span fits entirely
293+
                    lineN.add(span.normal())
294+
                    lineF.add(span.furigana(lineS))
295+
296+
                }
297+
298+
                // Next span
299+
                span = null
300+
                spanI++
301+
302+
                if (spanI < this.spans.size) {
303+
                    span = this.spans[spanI]
304+
                }
305+
            }
306+
307+
            // Last span
308+
            if (lineN.size() != 0) {
309+
                // Add
310+
                this.lineMax = if (this.lineMax > lineX) this.lineMax else lineX
311+
                normalLines.add(lineN)
312+
                furiganaLines.add(lineF)
313+
            }
314+
        }
315+
316+
        // Calculate furigana
317+
        for (line in furiganaLines) {
318+
            line.calculate()
319+
        }
320+
    }
321+
322+
    // Drawing
323+
    public override fun onDraw(canvas: Canvas) {
324+
325+
        textPaintNormal.color = currentTextColor
326+
327+
        if (furiganaTextColor != 0) {
328+
            textPaintFurigana.color = furiganaTextColor
329+
        } else {
330+
            textPaintFurigana.color = currentTextColor
331+
        }
332+
333+
        // Coordinates
334+
        var y = lineSize
335+
336+
        // Loop lines
337+
        for (i in normalLines.indices) {
338+
            normalLines[i].draw(canvas, y)
339+
            furiganaLines[i].draw(canvas, y - normalHeight)
340+
            y += lineSize
341+
        }
342+
    }
343+
}

furiganatextview/src/main/java/se/fekete/furiganatextview/furiganaview/LineFurigana.kt unknown status 1

1+
package se.fekete.furiganatextview.furiganaview
2+
3+
import android.graphics.Canvas
4+
import android.graphics.Paint
5+
import java.util.*
6+
7+
class LineFurigana(private val lineMax: Float, private val paint: Paint) {
8+
    // Text
9+
    private val texts = Vector<TextFurigana>()
10+
    private val offsets = Vector<Float>()
11+
12+
    // Add
13+
    fun add(text: TextFurigana?) {
14+
        if (text != null) {
15+
            this.texts.add(text)
16+
        }
17+
    }
18+
19+
    // Calculate
20+
    fun calculate() {
21+
        // Check size
22+
        if (texts.size == 0) {
23+
            return
24+
        }
25+
26+
        val r = FloatArray(texts.size)
27+
28+
        for (i in texts.indices) {
29+
            r[i] = texts[i].getOffset()
30+
        }
31+
32+
        // a[] - constraint matrix
33+
        val a = Array(texts.size + 1) { FloatArray(texts.size) }
34+
35+
        for (i in a.indices) {
36+
            for (j in 0 until a[0].size) {
37+
                a[i][j] = 0.0f
38+
            }
39+
        }
40+
41+
        a[0][0] = 1.0f
42+
43+
        for (i in 1 until a.size - 2) {
44+
            a[i][i - 1] = -1.0f
45+
            a[i][i] = 1.0f
46+
        }
47+
48+
        a[a.size - 1][a[0].size - 1] = -1.0f
49+
50+
        // b[] - constraint vector
51+
        val b = FloatArray(texts.size + 1)
52+
        b[0] = -r[0] + 0.5f * texts[0].width()
53+
54+
        for (i in 1 until b.size - 2) {
55+
            b[i] = 0.5f * (texts[i].width() + texts[i - 1].width()) + (r[i - 1] - r[i])
56+
        }
57+
58+
        b[b.size - 1] = -lineMax + r[r.size - 1] + 0.5f * texts[texts.size - 1].width()
59+
60+
        // Calculate constraint optimization
61+
        val x = FloatArray(texts.size)
62+
        for (i in x.indices) {
63+
            x[i] = 0.0f
64+
        }
65+
66+
        val co = QuadraticOptimizer(a, b)
67+
        co.calculate(x)
68+
69+
        for (i in x.indices) {
70+
            offsets.add(x[i] + r[i])
71+
        }
72+
    }
73+
74+
    // Draw
75+
    fun draw(canvas: Canvas, y: Float) {
76+
        var mutableY = y
77+
        mutableY -= paint.descent()
78+
79+
        if (offsets.size == texts.size) {
80+
            // Render with fixed offsets
81+
            for (i in offsets.indices) {
82+
                texts[i].draw(canvas, offsets[i], mutableY)
83+
            }
84+
        } else {
85+
            // Render with original offsets
86+
            for (text in texts) {
87+
                text.draw(canvas, text.getOffset(), mutableY)
88+
            }
89+
        }
90+
    }
91+
}

furiganatextview/src/main/java/se/fekete/furiganatextview/furiganaview/LineNormal.kt unknown status 1

1+
package se.fekete.furiganatextview.furiganaview
2+
3+
import android.graphics.Canvas
4+
import android.graphics.Paint
5+
import java.util.*
6+
7+
class LineNormal(val paint: Paint) {
8+
    // Text
9+
    private val text = Vector<TextNormal>()
10+
11+
    // Elements
12+
    fun size(): Int {
13+
        return text.size
14+
    }
15+
16+
    fun add(text: Vector<TextNormal>) {
17+
        this.text.addAll(text)
18+
    }
19+
20+
    // Draw
21+
    fun draw(canvas: Canvas, y: Float) {
22+
        var mutableY = y
23+
        mutableY -= paint.descent()
24+
25+
        var x = 0.0f
26+
27+
        for (text in text) {
28+
            x += text.draw(canvas, x, mutableY)
29+
        }
30+
    }
31+
}

furiganatextview/src/main/java/se/fekete/furiganatextview/furiganaview/QuadraticOptimizer.kt unknown status 1

1+
/*
2+
 * FuriganaView widget
3+
 * Copyright (C) 2013 sh0 <sh0@yutani.ee>
4+
 * Licensed under Creative Commons BY-SA 3.0
5+
 */
6+
7+
package se.fekete.furiganatextview.furiganaview
8+
9+
// Constraint optimizer class
10+
class QuadraticOptimizer(private var a: Array<FloatArray>, private var b: FloatArray) {
11+
12+
    // Calculate
13+
    fun calculate(x: FloatArray) {
14+
        // Check if calculation needed
15+
        if (phi(1.0f, x) == 0.0f) {
16+
            return
17+
        }
18+
19+
        // Calculate
20+
        var sigma = 1.0f
21+
22+
        for (k in 0 until penaltyRuns) {
23+
            newtonSolve(x, sigma)
24+
            sigma *= sigmaMul
25+
        }
26+
    }
27+
28+
    private fun newtonSolve(x: FloatArray, sigma: Float) {
29+
        for (i in 0 until newtonRuns) {
30+
            newtonIteration(x, sigma)
31+
        }
32+
    }
33+
34+
    private fun newtonIteration(x: FloatArray, sigma: Float) {
35+
        // Calculate gradient
36+
        val d = FloatArray(x.size)
37+
38+
        for (i in d.indices) {
39+
            d[i] = phiD1(i, sigma, x)
40+
        }
41+
42+
        // Calculate Hessian matrix (symmetric)
43+
        val h = Array(x.size) { FloatArray(x.size) }
44+
        for (i in h.indices) {
45+
46+
            for (j in i until h[0].size) {
47+
                h[i][j] = phiD2(i, j, sigma, x)
48+
            }
49+
        }
50+
        for (i in h.indices) {
51+
            for (j in 0 until i) {
52+
                h[i][j] = h[j][i]
53+
            }
54+
        }
55+
56+
        // Linear system solver
57+
        val p = gsSolver(h, d)
58+
59+
        // Iteration
60+
        for (i in x.indices) {
61+
            x[i] = x[i] - wolfeGamma * p[i]
62+
        }
63+
64+
    }
65+
66+
    // Gauss-Seidel solver
67+
    private fun gsSolver(a: Array<FloatArray>, b: FloatArray): FloatArray {
68+
        // Initial guess
69+
        val p = FloatArray(b.size)
70+
71+
        for (i in p.indices) {
72+
            p[i] = 1.0f
73+
        }
74+
75+
        for (z in 0 until gsRuns) {
76+
77+
            for (i in p.indices) {
78+
                var s = 0.0f
79+
80+
                for (j in p.indices) {
81+
                    if (i != j) {
82+
                        s += a[i][j] * p[j]
83+
                    }
84+
                }
85+
86+
                p[i] = (b[i] - s) / a[i][i]
87+
            }
88+
        }
89+
90+
        // Result
91+
        return p
92+
    }
93+
94+
    // Math
95+
    private fun dot(a: FloatArray, b: FloatArray): Float {
96+
        assert(a.size == b.size)
97+
98+
        var r = 0.0f
99+
100+
        for (i in a.indices) {
101+
            r += a[i] * b[i]
102+
        }
103+
104+
        return r
105+
    }
106+
107+
    // Cost function f(x)
108+
    private fun f(x: FloatArray): Float {
109+
        return dot(x, x)
110+
    }
111+
112+
    // Cost function phi(x)
113+
    private fun phi(sigma: Float, x: FloatArray): Float {
114+
        var r = 0.0f
115+
116+
        for (i in x.indices) {
117+
            r += Math.pow(Math.min(0f, dot(a[i], x) - b[i]).toDouble(), 2.0).toFloat()
118+
        }
119+
120+
        return f(x) + sigma * r
121+
    }
122+
123+
    private fun phiD1(n: Int, sigma: Float, x: FloatArray): Float {
124+
        var r = 0.0f
125+
126+
        for (i in a.indices) {
127+
            val c = dot(a[i], x) - b[i]
128+
129+
            if (c < 0) {
130+
                r += 2.0f * a[i][n] * c
131+
            }
132+
        }
133+
134+
        return 2.0f * x[n] + sigma * r
135+
    }
136+
137+
    private fun phiD2(n: Int, m: Int, sigma: Float, x: FloatArray): Float {
138+
        var r = 0.0f
139+
140+
        for (i in a.indices) {
141+
            val c = dot(a[i], x) - b[i]
142+
            if (c < 0) {
143+
                r += 2.0f * a[i][n] * a[i][m]
144+
            }
145+
        }
146+
147+
        return (if (n == m) 2.0f else 0.0f) + sigma * r
148+
    }
149+
150+
    companion object {
151+
        // Constants
152+
        internal const val wolfeGamma = 0.1f
153+
        internal const val sigmaMul = 10.0f
154+
        internal const val penaltyRuns = 5
155+
        internal const val newtonRuns = 20
156+
        internal const val gsRuns = 20
157+
    }
158+
}

furiganatextview/src/main/java/se/fekete/furiganatextview/furiganaview/Span.kt unknown status 1

1+
package se.fekete.furiganatextview.furiganaview
2+
3+
import android.graphics.Paint
4+
import java.util.*
5+
6+
internal class Span {
7+
    // Text
8+
    private var furigana: TextFurigana? = null
9+
    private var normal = Vector<TextNormal>()
10+
11+
    // Widths
12+
    private val widthChars = Vector<Float>()
13+
    private var widthTotal = 0.0f
14+
15+
    // Constructors
16+
    constructor(textF: String, textK: String, markS: Int, markE: Int, paint: Paint, paintF: Paint) {
17+
18+
        var mutableMarkS = markS
19+
        var mutableMarkE = markE
20+
21+
        // Furigana text
22+
        if (textF.isNotEmpty()) {
23+
            furigana = TextFurigana(textF, paintF)
24+
        }
25+
26+
        // Normal text
27+
        if (mutableMarkS < textK.length && mutableMarkE > 0 && mutableMarkS < mutableMarkE) {
28+
29+
            // Fix marked bounds
30+
            mutableMarkS = Math.max(0, mutableMarkS)
31+
            mutableMarkE = Math.min(textK.length, mutableMarkE)
32+
33+
            // Prefix
34+
            if (mutableMarkS > 0) {
35+
                normal.add(TextNormal(textK.substring(0, mutableMarkS), paint))
36+
            }
37+
38+
            // Marked
39+
            if (mutableMarkE > mutableMarkS) {
40+
                normal.add(TextNormal(textK.substring(mutableMarkS, mutableMarkE), paint))
41+
            }
42+
43+
            // Postfix
44+
            if (mutableMarkE < textK.length) {
45+
                normal.add(TextNormal(textK.substring(mutableMarkE), paint))
46+
            }
47+
48+
        } else {
49+
            // Non marked
50+
            normal.add(TextNormal(textK, paint))
51+
        }
52+
53+
        // Widths
54+
        calculateWidths()
55+
    }
56+
57+
    constructor(normal: Vector<TextNormal>) {
58+
        // Only normal text
59+
        this.normal = normal
60+
61+
        // Widths
62+
        calculateWidths()
63+
    }
64+
65+
    // Text
66+
    fun furigana(x: Float): TextFurigana? {
67+
        if (furigana == null) {
68+
            return null
69+
        }
70+
71+
        furigana?.setOffset(x + widthTotal / 2.0f)
72+
73+
        return furigana
74+
    }
75+
76+
    fun normal(): Vector<TextNormal> {
77+
        return normal
78+
    }
79+
80+
    // Widths
81+
    fun widths(): Vector<Float> {
82+
        return widthChars
83+
    }
84+
85+
    private fun calculateWidths() {
86+
        // Chars
87+
        if (furigana == null) {
88+
            for (normal in normal) {
89+
                for (v in normal.charsWidth()) {
90+
                    widthChars.add(v)
91+
                }
92+
            }
93+
        } else {
94+
            var sum = 0.0f
95+
96+
            for (normal in normal) {
97+
                for (v in normal.charsWidth()) {
98+
                    sum += v
99+
                }
100+
            }
101+
            widthChars.add(sum)
102+
        }
103+
104+
        // Total
105+
        widthTotal = 0.0f
106+
107+
        for (v in widthChars) {
108+
            widthTotal += v
109+
        }
110+
    }
111+
112+
    // Split
113+
    fun split(offset: Int, normalA: Vector<TextNormal>, normalB: Vector<TextNormal>) {
114+
        var mutableOffset = offset
115+
116+
        // Check if no furigana
117+
        if (furigana == null) {
118+
            return
119+
        }
120+
121+
        // Split normal list
122+
        for (cur in normal) {
123+
            when {
124+
                mutableOffset <= 0 -> normalB.add(cur)
125+
                mutableOffset >= cur.length() -> normalA.add(cur)
126+
                else -> {
127+
                    val split = cur.split(mutableOffset)
128+
                    normalA.add(split[0])
129+
                    normalB.add(split[1])
130+
                }
131+
            }
132+
            mutableOffset -= cur.length()
133+
        }
134+
    }
135+
}

furiganatextview/src/main/java/se/fekete/furiganatextview/furiganaview/TextFurigana.kt unknown status 1

1+
package se.fekete.furiganatextview.furiganaview
2+
3+
import android.graphics.Canvas
4+
import android.graphics.Paint
5+
6+
class TextFurigana(private val text: String, private val paintF: Paint) {
7+
8+
    // Coordinates
9+
    private var offset = 0.0f
10+
    private var width = 0.0f
11+
12+
    init {
13+
        width = paintF.measureText(text)
14+
    }
15+
16+
    fun getOffset(): Float {
17+
        return offset
18+
    }
19+
20+
    fun setOffset(value: Float) {
21+
        offset = value
22+
    }
23+
24+
    fun width(): Float {
25+
        return width
26+
    }
27+
28+
    fun draw(canvas: Canvas, x: Float, y: Float) {
29+
        var mutableX = x
30+
        mutableX -= width / 2.0f
31+
        canvas.drawText(text, 0, text.length, mutableX, y, paintF)
32+
    }
33+
}
33<
034<
\ No newline at end of file

furiganatextview/src/main/java/se/fekete/furiganatextview/furiganaview/TextNormal.kt unknown status 1

1+
package se.fekete.furiganatextview.furiganaview
2+
3+
import android.graphics.Canvas
4+
import android.graphics.Paint
5+
6+
class TextNormal(private val text: String, private val paint: Paint) {
7+
8+
    private var totalWidth: Float = 0.toFloat()
9+
    private val charsWidth: FloatArray = FloatArray(text.length)
10+
11+
    init {
12+
        paint.getTextWidths(text, charsWidth)
13+
14+
        // Total width
15+
        totalWidth = 0.0f
16+
        for (v in charsWidth)
17+
            totalWidth += v
18+
    }
19+
20+
    // Info
21+
    fun length(): Int {
22+
        return text.length
23+
    }
24+
25+
    // Widths
26+
    fun charsWidth(): FloatArray {
27+
        return charsWidth
28+
    }
29+
30+
    // Split
31+
    fun split(offset: Int): Array<TextNormal> {
32+
        return arrayOf(TextNormal(text.substring(0, offset), paint), TextNormal(text.substring(offset), paint))
33+
    }
34+
35+
    // Draw
36+
    fun draw(canvas: Canvas, x: Float, y: Float): Float {
37+
        canvas.drawText(text, 0, text.length, x, y, paint)
38+
        return totalWidth
39+
    }
40+
}

furiganatextview/src/main/java/se/fekete/furiganatextview/utils/FuriganaUtils.java unknown status 1

1+
package se.fekete.furiganatextview.utils;
2+
3+
4+
@Deprecated
5+
public class FuriganaUtils {
6+
    /**
7+
     * The method parseRuby converts kanji enclosed in ruby tags to the
8+
     * format which is supported by the textview {Kanji:furigana}
9+
     *
10+
     * @param textWithRuby
11+
     * @deprecated Use the set{@link se.fekete.furiganatextview.furiganaview.FuriganaTextView}
12+
     */
13+
    public static String parseRuby(String textWithRuby) {
14+
        String parsed = textWithRuby.replace("<ruby>", "{");
15+
        parsed = parsed.replace("<rt>", ";");
16+
        parsed = parsed.replace("</rt>", "");
17+
18+
        return parsed.replace("</ruby>", "}");
19+
    }
20+
}

furiganatextview/src/main/res/values/attrs.xml unknown status 1

1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
    <declare-styleable name="FuriganaTextView">
4+
        <attr name="contains_ruby_tags" format="boolean"/>
5+
        <attr name="furigana_text_color" format="color"/>
6+
    </declare-styleable>
7+
</resources>
7<
08<
\ No newline at end of file

furiganatextview/src/main/res/values/strings.xml unknown status 1

1+
<resources>
2+
    <string name="app_name">FuriganaTextView</string>
3+
</resources>

furiganatextview/src/test/java/eu/lepiller/furiganatextview/ExampleUnitTest.java unknown status 1

1+
package eu.lepiller.furiganatextview;
2+
3+
import org.junit.Test;
4+
5+
import static org.junit.Assert.*;
6+
7+
/**
8+
 * Example local unit test, which will execute on the development machine (host).
9+
 *
10+
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
11+
 */
12+
public class ExampleUnitTest {
13+
    @Test
14+
    public void addition_isCorrect() {
15+
        assertEquals(4, 2 + 2);
16+
    }
17+
}
17<
018<
\ No newline at end of file

furiganatextview/src/test/java/se/fekete/furiganatextview/ExampleUnitTest.java unknown status 1

1+
package se.fekete.furiganatextview;
2+
3+
import org.junit.Test;
4+
5+
import static org.junit.Assert.assertEquals;
6+
7+
/**
8+
 * To work on unit tests, switch the Test Artifact in the Build Variants view.
9+
 */
10+
public class ExampleUnitTest {
11+
    @Test
12+
    public void addition_isCorrect() {
13+
        assertEquals(4, 2 + 2);
14+
    }
15+
}
15<
016<
\ No newline at end of file

settings.gradle

1-
include ':app'
1+
include ':app', ':furiganatextview'
1<
02<
\ No newline at end of file