├── Furigana
├── app
│ ├── .gitignore
│ ├── src
│ │ ├── main
│ │ │ ├── res
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── values
│ │ │ │ │ ├── colors.xml
│ │ │ │ │ ├── dimens.xml
│ │ │ │ │ ├── styles.xml
│ │ │ │ │ └── strings.xml
│ │ │ │ ├── values-w820dp
│ │ │ │ │ └── dimens.xml
│ │ │ │ └── layout
│ │ │ │ │ └── activity_main.xml
│ │ │ ├── java
│ │ │ │ └── se
│ │ │ │ │ └── fekete
│ │ │ │ │ └── furigana
│ │ │ │ │ └── MainActivity.kt
│ │ │ └── AndroidManifest.xml
│ │ ├── test
│ │ │ └── java
│ │ │ │ └── se
│ │ │ │ └── fekete
│ │ │ │ └── furigana
│ │ │ │ └── ExampleUnitTest.java
│ │ └── androidTest
│ │ │ └── java
│ │ │ └── se
│ │ │ └── fekete
│ │ │ └── furigana
│ │ │ └── ApplicationTest.java
│ ├── proguard-rules.pro
│ └── build.gradle
├── furiganatextview
│ ├── .gitignore
│ ├── src
│ │ ├── main
│ │ │ ├── res
│ │ │ │ └── values
│ │ │ │ │ ├── strings.xml
│ │ │ │ │ └── attrs.xml
│ │ │ ├── AndroidManifest.xml
│ │ │ └── java
│ │ │ │ └── se
│ │ │ │ └── fekete
│ │ │ │ └── furiganatextview
│ │ │ │ ├── utils
│ │ │ │ └── FuriganaUtils.java
│ │ │ │ └── furiganaview
│ │ │ │ ├── LineNormal.kt
│ │ │ │ ├── TextFurigana.kt
│ │ │ │ ├── TextNormal.kt
│ │ │ │ ├── LineFurigana.kt
│ │ │ │ ├── Span.kt
│ │ │ │ ├── QuadraticOptimizer.kt
│ │ │ │ └── FuriganaTextView.kt
│ │ ├── test
│ │ │ └── java
│ │ │ │ └── se
│ │ │ │ └── fekete
│ │ │ │ └── furiganatextview
│ │ │ │ └── ExampleUnitTest.java
│ │ └── androidTest
│ │ │ └── java
│ │ │ └── se
│ │ │ └── fekete
│ │ │ └── furiganatextview
│ │ │ └── ApplicationTest.java
│ ├── proguard-rules.pro
│ └── build.gradle
├── settings.gradle
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── .gitignore
├── build.gradle
└── gradle.properties
├── .gitignore
└── README.md
/Furigana/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/Furigana/furiganatextview/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/Furigana/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':furiganatextview'
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /Furigana/.idea/
2 | /Furigana/.gradle/
3 | /Furigana/.idea/encodings.xml
4 | sh.exe.stackdump
5 |
--------------------------------------------------------------------------------
/Furigana/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fkt3/FuriganaTextView/HEAD/Furigana/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/Furigana/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 |
--------------------------------------------------------------------------------
/Furigana/furiganatextview/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | FuriganaTextView
3 |
4 |
--------------------------------------------------------------------------------
/Furigana/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fkt3/FuriganaTextView/HEAD/Furigana/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/Furigana/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fkt3/FuriganaTextView/HEAD/Furigana/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/Furigana/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fkt3/FuriganaTextView/HEAD/Furigana/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/Furigana/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fkt3/FuriganaTextView/HEAD/Furigana/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/Furigana/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fkt3/FuriganaTextView/HEAD/Furigana/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/Furigana/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/Furigana/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/Furigana/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Oct 13 09:03:40 CEST 2018
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
7 |
--------------------------------------------------------------------------------
/Furigana/furiganatextview/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/Furigana/furiganatextview/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Furigana/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/Furigana/app/src/test/java/se/fekete/furigana/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package se.fekete.furigana;
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 | }
--------------------------------------------------------------------------------
/Furigana/furiganatextview/src/test/java/se/fekete/furiganatextview/ExampleUnitTest.java:
--------------------------------------------------------------------------------
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 | }
--------------------------------------------------------------------------------
/Furigana/app/src/androidTest/java/se/fekete/furigana/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package se.fekete.furigana;
2 |
3 | import android.app.Application;
4 | import android.test.ApplicationTestCase;
5 |
6 | /**
7 | * Testing Fundamentals
8 | */
9 | public class ApplicationTest extends ApplicationTestCase {
10 | public ApplicationTest() {
11 | super(Application.class);
12 | }
13 | }
--------------------------------------------------------------------------------
/Furigana/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Furigana/furiganatextview/src/androidTest/java/se/fekete/furiganatextview/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package se.fekete.furiganatextview;
2 |
3 | import android.app.Application;
4 | import android.test.ApplicationTestCase;
5 |
6 | /**
7 | * Testing Fundamentals
8 | */
9 | public class ApplicationTest extends ApplicationTestCase {
10 | public ApplicationTest() {
11 | super(Application.class);
12 | }
13 | }
--------------------------------------------------------------------------------
/Furigana/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Furigana
3 | 宮崎駿さんは有名です。]]>
4 | 展望台が新しくなる]]>
5 | 展望台が新しくなる]]>
6 |
7 |
--------------------------------------------------------------------------------
/Furigana/app/src/main/java/se/fekete/furigana/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package se.fekete.furigana
2 |
3 | import android.os.Bundle
4 | import android.support.v7.app.AppCompatActivity
5 |
6 | import se.fekete.furiganatextview.furiganaview.FuriganaTextView
7 |
8 | class MainActivity : AppCompatActivity() {
9 |
10 | override fun onCreate(savedInstanceState: Bundle?) {
11 | super.onCreate(savedInstanceState)
12 | setContentView(R.layout.activity_main)
13 |
14 | val furiganaTextView = findViewById(R.id.text_view_furigana)
15 | furiganaTextView?.setFuriganaText("宮崎駿さんは有名です。")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Furigana/app/proguard-rules.pro:
--------------------------------------------------------------------------------
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 | #}
18 |
--------------------------------------------------------------------------------
/Furigana/furiganatextview/proguard-rules.pro:
--------------------------------------------------------------------------------
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 | #}
18 |
--------------------------------------------------------------------------------
/Furigana/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Furigana/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext.kotlin_version = '1.2.71'
5 | repositories {
6 | google()
7 | jcenter()
8 | }
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:3.2.1'
11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
12 |
13 | // NOTE: Do not place your application dependencies here; they belong
14 | // in the individual module build.gradle files
15 | }
16 | }
17 |
18 | allprojects {
19 | repositories {
20 | google()
21 | jcenter()
22 | }
23 | }
24 |
25 | task clean(type: Delete) {
26 | delete rootProject.buildDir
27 | }
28 |
--------------------------------------------------------------------------------
/Furigana/furiganatextview/src/main/java/se/fekete/furiganatextview/utils/FuriganaUtils.java:
--------------------------------------------------------------------------------
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("", "{");
15 | parsed = parsed.replace("", "");
17 |
18 | return parsed.replace("", "}");
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Furigana/furiganatextview/src/main/java/se/fekete/furiganatextview/furiganaview/LineNormal.kt:
--------------------------------------------------------------------------------
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()
10 |
11 | // Elements
12 | fun size(): Int {
13 | return text.size
14 | }
15 |
16 | fun add(text: Vector) {
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 | }
32 |
--------------------------------------------------------------------------------
/Furigana/furiganatextview/build.gradle:
--------------------------------------------------------------------------------
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 | }
30 |
--------------------------------------------------------------------------------
/Furigana/furiganatextview/src/main/java/se/fekete/furiganatextview/furiganaview/TextFurigana.kt:
--------------------------------------------------------------------------------
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 | }
--------------------------------------------------------------------------------
/Furigana/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/Furigana/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | android {
6 | compileSdkVersion 28
7 |
8 | defaultConfig {
9 | minSdkVersion 15
10 | targetSdkVersion 28
11 | versionCode 1
12 | versionName "1.0"
13 | applicationId "se.fekete.furigana"
14 |
15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
16 | }
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | }
24 |
25 | dependencies {
26 | implementation fileTree(dir: 'libs', include: ['*.jar'])
27 | implementation project(':furiganatextview')
28 |
29 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
30 | implementation 'com.android.support:appcompat-v7:28.0.0'
31 |
32 | testImplementation 'junit:junit:4.12'
33 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
34 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
35 | }
36 |
--------------------------------------------------------------------------------
/Furigana/furiganatextview/src/main/java/se/fekete/furiganatextview/furiganaview/TextNormal.kt:
--------------------------------------------------------------------------------
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 {
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 | }
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ⚠️ **FuriganaTextView is not actively maintained**.
2 |
3 | # FuriganaTextView
4 | Custom TextView for Android for rendering Japanese text with furigana.
5 |
6 | [Licensed under Creative Commons BY-SA 3.0](http://creativecommons.org/licenses/by-sa/3.0/)
7 |
8 | Credits to [sh0](https://github.com/sh0/furigana-view) who has written the furigana-view which the FuriganaTextView is built upon.
9 | (This was supposed to be a fork of the original repository)
10 | # Introduction
11 | FuriganaTextView is a Textview for Android that supports rendering of furigana characters above Japanese kanji. The TextView currently supports two xml attributes `app:contains_ruby_tags"` which is a boolean value and tells the FuriganaTextView that the text which is set contains `` tags. The second attribute `app:furigana_text_color` takes a color and can be used to color the furigana separately from the main text.
12 |
13 | # Examples
14 |
15 | ##### Using FuriganaTextView in a Xml layout file.
16 |
17 | ```
18 |
26 | ```
27 |
28 | ##### Using FuriganaTextView in a Kotlin or Java file.
29 | ```
30 | class MainActivity : AppCompatActivity() {
31 | override fun onCreate(savedInstanceState: Bundle?) {
32 | super.onCreate(savedInstanceState)
33 | setContentView(R.layout.activity_main)
34 |
35 | val furiganaTextView = findViewById(R.id.text_view_furigana) as FuriganaTextView?
36 | furiganaTextView!!.setFuriganaText("サンシャイン60の展望台が新しくなる")
37 | }
38 | }
39 | ```
40 |
--------------------------------------------------------------------------------
/Furigana/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
23 |
24 |
33 |
34 |
35 |
43 |
44 |
--------------------------------------------------------------------------------
/Furigana/furiganatextview/src/main/java/se/fekete/furiganatextview/furiganaview/LineFurigana.kt:
--------------------------------------------------------------------------------
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()
10 | private val offsets = Vector()
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 | }
92 |
--------------------------------------------------------------------------------
/Furigana/furiganatextview/src/main/java/se/fekete/furiganatextview/furiganaview/Span.kt:
--------------------------------------------------------------------------------
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()
10 |
11 | // Widths
12 | private val widthChars = Vector()
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) {
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 {
77 | return normal
78 | }
79 |
80 | // Widths
81 | fun widths(): Vector {
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, normalB: Vector) {
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 | }
136 |
--------------------------------------------------------------------------------
/Furigana/furiganatextview/src/main/java/se/fekete/furiganatextview/furiganaview/QuadraticOptimizer.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * FuriganaView widget
3 | * Copyright (C) 2013 sh0
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, 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, 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 | }
159 |
--------------------------------------------------------------------------------
/Furigana/furiganatextview/src/main/java/se/fekete/furiganatextview/furiganaview/FuriganaTextView.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * FuriganaView widget
3 | * Copyright (C) 2013 sh0
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.TextPaint
12 | import android.util.AttributeSet
13 | import android.view.View
14 | import android.widget.TextView
15 | import se.fekete.furiganatextview.R
16 | import java.util.*
17 |
18 | class FuriganaTextView : TextView {
19 |
20 | // Paints
21 | private var textPaintFurigana = TextPaint()
22 | private var textPaintNormal = TextPaint()
23 |
24 | // Sizes
25 | private var lineSize = 0.0f
26 | private var normalHeight = 0.0f
27 | private var furiganaHeight = 0.0f
28 | private var lineMax = 0.0f
29 |
30 | // Spans and lines
31 | private val spans = Vector()
32 | private val normalLines = Vector()
33 | private val furiganaLines = Vector()
34 |
35 | //attributes
36 | private var hasRuby: Boolean = false
37 | private var furiganaTextColor: Int = 0
38 |
39 | // Constructors
40 | constructor(context: Context) : super(context) {
41 | initialize()
42 | }
43 |
44 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
45 | val typedArray = context.obtainStyledAttributes(attrs, R.styleable.FuriganaTextView, 0, 0)
46 | try {
47 | hasRuby = typedArray.getBoolean(R.styleable.FuriganaTextView_contains_ruby_tags, false)
48 | furiganaTextColor = typedArray.getColor(R.styleable.FuriganaTextView_furigana_text_color, 0)
49 | } finally {
50 | typedArray.recycle()
51 | }
52 |
53 | initialize()
54 | }
55 |
56 | private fun initialize() {
57 | val viewText = text
58 | if (viewText.isNotEmpty()) {
59 | setFuriganaText(viewText as String, hasRuby)
60 | }
61 | }
62 |
63 | /**
64 | * The method parseRuby converts kanji enclosed in ruby tags to the
65 | * format which is supported by the textview {Kanji:furigana}
66 |
67 | * @param textWithRuby
68 | * The text string with Kanji enclosed in ruby tags.
69 | */
70 | private fun replaceRuby(textWithRuby: String): String {
71 | var parsed = textWithRuby.replace("", "{")
72 | parsed = parsed.replace("", "")
74 |
75 | return parsed.replace("", "}")
76 | }
77 |
78 | override fun setTextColor(color: Int) {
79 | super.setTextColor(color)
80 | invalidate()
81 | }
82 |
83 | fun setFuriganaText(text: String) {
84 | setFuriganaText(text, hasRuby = false)
85 | }
86 |
87 | fun setFuriganaText(text: String, hasRuby: Boolean) {
88 | super.setText(text)
89 |
90 | var textToDisplay = text
91 | if (this.hasRuby || hasRuby) {
92 | textToDisplay = replaceRuby(text)
93 | }
94 |
95 | setText(paint, textToDisplay, 0, 0)
96 | }
97 |
98 | private fun setText(tp: TextPaint, text: String, markS: Int, markE: Int) {
99 | var mutableText = text
100 | var mutableMarkS = markS
101 | var mutableMarkE = markE
102 |
103 | // Text
104 | textPaintNormal = TextPaint(tp)
105 | textPaintFurigana = TextPaint(tp)
106 | textPaintFurigana.textSize = textPaintFurigana.textSize / 2.0f
107 |
108 | // Line size
109 | normalHeight = textPaintNormal.descent() - textPaintNormal.ascent()
110 | furiganaHeight = textPaintFurigana.descent() - textPaintFurigana.ascent()
111 | lineSize = normalHeight + furiganaHeight
112 |
113 | // Clear spans
114 | spans.clear()
115 |
116 | // Sizes
117 | lineSize = textPaintFurigana.fontSpacing + Math.max(textPaintNormal.fontSpacing, 0f)
118 |
119 | // Spannify text
120 | while (mutableText.isNotEmpty()) {
121 | var idx = mutableText.indexOf('{')
122 | if (idx >= 0) {
123 | // Prefix string
124 | if (idx > 0) {
125 | // Spans
126 | spans.add(Span("", mutableText.substring(0, idx), mutableMarkS, mutableMarkE, textPaintNormal, textPaintFurigana))
127 |
128 | // Remove text
129 | mutableText = mutableText.substring(idx)
130 | mutableMarkS -= idx
131 | mutableMarkE -= idx
132 | }
133 |
134 | // End bracket
135 | idx = mutableText.indexOf('}')
136 | if (idx < 1) {
137 | // Error
138 | break
139 | } else if (idx == 1) {
140 | // Empty bracket
141 | mutableText = mutableText.substring(2)
142 | continue
143 | }
144 |
145 | // Spans
146 | val split = mutableText.substring(1, idx).split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
147 | spans.add(Span(if (split.size > 1) split[1] else "", split[0], mutableMarkS, mutableMarkE, textPaintNormal, textPaintFurigana))
148 |
149 | // Remove text
150 | mutableText = mutableText.substring(idx + 1)
151 | mutableMarkS -= split[0].length
152 | mutableMarkE -= split[0].length
153 |
154 | } else {
155 | // Single span
156 | spans.add(Span("", mutableText, mutableMarkS, mutableMarkE, textPaintNormal, textPaintFurigana))
157 | mutableText = ""
158 | }
159 | }
160 |
161 | // Invalidate view
162 | this.invalidate()
163 | this.requestLayout()
164 | }
165 |
166 | // Size calculation
167 | override fun onMeasure(width_ms: Int, height_ms: Int) {
168 | // Modes
169 | val wmode = View.MeasureSpec.getMode(width_ms)
170 | val hmode = View.MeasureSpec.getMode(height_ms)
171 |
172 | // Dimensions
173 | val wold = View.MeasureSpec.getSize(width_ms)
174 | val hold = View.MeasureSpec.getSize(height_ms)
175 |
176 | if (text.isNotEmpty()) {
177 | // Draw mode
178 | if (wmode == View.MeasureSpec.EXACTLY || wmode == View.MeasureSpec.AT_MOST && wold > 0) {
179 | // Width limited
180 | calculateText(wold.toFloat())
181 | } else {
182 | // Width unlimited
183 | calculateText(-1.0f)
184 | }
185 | }
186 |
187 | // New height
188 | var hnew = Math.round(Math.ceil((lineSize * normalLines.size.toFloat()).toDouble())).toInt()
189 | var wnew = wold
190 | if (wmode != View.MeasureSpec.EXACTLY && normalLines.size <= 1)
191 | wnew = Math.round(Math.ceil(lineMax.toDouble())).toInt()
192 | if (hmode != View.MeasureSpec.UNSPECIFIED && hnew > hold)
193 | hnew = hnew or View.MEASURED_STATE_TOO_SMALL
194 |
195 | // Set result
196 | setMeasuredDimension(wnew, hnew)
197 | }
198 |
199 | private fun calculateText(lineMax: Float) {
200 | // Clear lines
201 | normalLines.clear()
202 | furiganaLines.clear()
203 |
204 | // Sizes
205 | this.lineMax = 0.0f
206 |
207 | // Check if no limits on width
208 | if (lineMax < 0.0) {
209 |
210 | // Create single normal and furigana line
211 | val lineN = LineNormal(textPaintNormal)
212 | val lineF = LineFurigana(this.lineMax, textPaintFurigana)
213 |
214 | // Loop spans
215 | for (span in spans) {
216 | // Text
217 | lineN.add(span.normal())
218 | lineF.add(span.furigana(this.lineMax))
219 |
220 | // Widths update
221 | for (width in span.widths())
222 | this.lineMax += width
223 | }
224 |
225 | // Commit both lines
226 | normalLines.add(lineN)
227 | furiganaLines.add(lineF)
228 |
229 | } else {
230 |
231 | // Lines
232 | var lineX = 0.0f
233 | var lineN = LineNormal(textPaintNormal)
234 | var lineF = LineFurigana(this.lineMax, textPaintFurigana)
235 |
236 | // Initial span
237 | var spanI = 0
238 | var span: Span? = if (spans.isNotEmpty()) spans[spanI] else null
239 |
240 | // Iterate
241 | while (span != null) {
242 | // Start offset
243 | val lineS = lineX
244 |
245 | // Calculate possible line size
246 | val widths = span.widths()
247 | var i = 0
248 | while (i < widths.size) {
249 | if (lineX + widths[i] <= lineMax) {
250 | lineX += widths[i]
251 | } else {
252 | break
253 | }
254 | i++
255 | }
256 |
257 | // Add span to line
258 | if (i >= 0 && i < widths.size) {
259 |
260 | // Span does not fit entirely
261 | if (i > 0) {
262 | // Split half that fits
263 | val normalA = Vector()
264 | val normalB = Vector()
265 | span.split(i, normalA, normalB)
266 | lineN.add(normalA)
267 | span = Span(normalB)
268 | }
269 |
270 | // Add new line with current spans
271 | if (lineN.size() != 0) {
272 | // Add
273 | this.lineMax = if (this.lineMax > lineX) this.lineMax else lineX
274 | normalLines.add(lineN)
275 | furiganaLines.add(lineF)
276 |
277 | // Reset
278 | lineN = LineNormal(textPaintNormal)
279 | lineF = LineFurigana(this.lineMax, textPaintFurigana)
280 | lineX = 0.0f
281 |
282 | // Next span
283 | continue
284 | }
285 |
286 | } else {
287 |
288 | // Span fits entirely
289 | lineN.add(span.normal())
290 | lineF.add(span.furigana(lineS))
291 |
292 | }
293 |
294 | // Next span
295 | span = null
296 | spanI++
297 |
298 | if (spanI < this.spans.size) {
299 | span = this.spans[spanI]
300 | }
301 | }
302 |
303 | // Last span
304 | if (lineN.size() != 0) {
305 | // Add
306 | this.lineMax = if (this.lineMax > lineX) this.lineMax else lineX
307 | normalLines.add(lineN)
308 | furiganaLines.add(lineF)
309 | }
310 | }
311 |
312 | // Calculate furigana
313 | for (line in furiganaLines) {
314 | line.calculate()
315 | }
316 | }
317 |
318 | // Drawing
319 | public override fun onDraw(canvas: Canvas) {
320 |
321 | textPaintNormal.color = currentTextColor
322 |
323 | if (furiganaTextColor != 0) {
324 | textPaintFurigana.color = furiganaTextColor
325 | } else {
326 | textPaintFurigana.color = currentTextColor
327 | }
328 |
329 | // Coordinates
330 | var y = lineSize
331 |
332 | // Loop lines
333 | for (i in normalLines.indices) {
334 | normalLines[i].draw(canvas, y)
335 | furiganaLines[i].draw(canvas, y - normalHeight)
336 | y += lineSize
337 | }
338 | }
339 | }
340 |
--------------------------------------------------------------------------------