├── 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("", ";"); 16 | 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("", ";") 73 | 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 | --------------------------------------------------------------------------------