Add furigana
.idea/gradle.xml
| 3 | 3 | <component name="GradleSettings"> | |
| 4 | 4 | <option name="linkedExternalProjectsSettings"> | |
| 5 | 5 | <GradleProjectSettings> | |
| 6 | - | <compositeConfiguration> | |
| 7 | - | <compositeBuild compositeDefinitionSource="SCRIPT" /> | |
| 8 | - | </compositeConfiguration> | |
| 9 | 6 | <option name="distributionType" value="DEFAULT_WRAPPED" /> | |
| 10 | 7 | <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> | |
| 11 | 15 | <option name="resolveModulePerSourceSet" value="false" /> | |
| 12 | 16 | </GradleProjectSettings> | |
| 13 | 17 | </option> |
app/build.gradle
| 26 | 26 | testImplementation 'junit:junit:4.12' | |
| 27 | 27 | androidTestImplementation 'com.android.support.test:runner:1.0.2' | |
| 28 | 28 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' | |
| 29 | + | compile project(path: ':furiganatextview') | |
| 29 | 30 | } |
app/src/main/java/eu/lepiller/nani/MainActivity.java
| 16 | 16 | ||
| 17 | 17 | import eu.lepiller.nani.dictionary.DictionaryFactory; | |
| 18 | 18 | import eu.lepiller.nani.result.Result; | |
| 19 | + | import se.fekete.furiganatextview.furiganaview.FuriganaTextView; | |
| 19 | 20 | ||
| 20 | 21 | public class MainActivity extends AppCompatActivity { | |
| 21 | 22 | ||
… | |||
| 44 | 45 | break; | |
| 45 | 46 | View child_result = getLayoutInflater().inflate(R.layout.layout_result, result_view, false); | |
| 46 | 47 | ||
| 47 | - | TextView kanji_view = child_result.findViewById(R.id.kanji_view); | |
| 48 | + | FuriganaTextView kanji_view = child_result.findViewById(R.id.kanji_view); | |
| 48 | 49 | LinearLayout senses_view = child_result.findViewById(R.id.sense_view); | |
| 49 | 50 | TextView additional_info = child_result.findViewById(R.id.additional_info_view); | |
| 50 | 51 | ||
| 51 | 52 | // Populate the data into the template view using the data object | |
| 52 | - | kanji_view.setText(result.getKanji()); | |
| 53 | + | kanji_view.setFuriganaText(result.getKanjiFurigana()); | |
| 53 | 54 | ||
| 54 | 55 | StringBuilder additional = new StringBuilder(); | |
| 55 | 56 | boolean separator = false; | |
app/src/main/java/eu/lepiller/nani/result/Result.java
| 1 | 1 | package eu.lepiller.nani.result; | |
| 2 | 2 | ||
| 3 | + | import android.util.Log; | |
| 4 | + | ||
| 3 | 5 | 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; | |
| 4 | 10 | ||
| 5 | 11 | public class Result { | |
| 6 | 12 | public static class Source { | |
… | |||
| 59 | 65 | } | |
| 60 | 66 | ||
| 61 | 67 | public String getKanji() { | |
| 62 | - | String k = ""; | |
| 68 | + | String k = getReading(); | |
| 63 | 69 | if(kanjis.size() > 0) | |
| 64 | 70 | k = kanjis.get(0); | |
| 65 | 71 | return k; | |
… | |||
| 76 | 82 | public ArrayList<Reading> getReadings() { | |
| 77 | 83 | return readings; | |
| 78 | 84 | } | |
| 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 | + | } | |
| 79 | 169 | } | |
app/src/main/res/layout/layout_result.xml
| 1 | 1 | <?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" | |
| 4 | 3 | android:layout_width="match_parent" | |
| 5 | 4 | android:layout_height="wrap_content" | |
| 5 | + | xmlns:app="http://schemas.android.com/apk/res-auto" | |
| 6 | 6 | android:layout_marginBottom="32dp"> | |
| 7 | 7 | ||
| 8 | - | <TextView | |
| 8 | + | <se.fekete.furiganatextview.furiganaview.FuriganaTextView | |
| 9 | 9 | android:id="@+id/kanji_view" | |
| 10 | - | android:layout_width="96dp" | |
| 10 | + | android:layout_width="wrap_content" | |
| 11 | 11 | android:layout_height="wrap_content" | |
| 12 | 12 | android:layout_gravity="center" | |
| 13 | 13 | 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" | |
| 15 | 17 | android:textIsSelectable="true" | |
| 18 | + | app:contains_ruby_tags="true" | |
| 16 | 19 | android:textSize="@dimen/title_size" /> | |
| 17 | 20 | ||
| 18 | 21 | <LinearLayout |
app/src/main/res/values/strings.xml
| 11 | 11 | <string name="download">Download</string> | |
| 12 | 12 | <string name="remove">Remove</string> | |
| 13 | 13 | <string name="icon_description">Icon</string> | |
| 14 | + | <string name="kanji_description">Writing</string> | |
| 14 | 15 | ||
| 15 | 16 | <!-- Dictionnary descriptions --> | |
| 16 | 17 | <string name="dico_jmdict_example">Japanese/English dictionary for test purposes. Do not use.</string> |
build.gradle
| 1 | 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. | |
| 2 | 2 | ||
| 3 | 3 | buildscript { | |
| 4 | + | ext.kotlin_version = '1.3.20' | |
| 4 | 5 | repositories { | |
| 5 | 6 | google() | |
| 6 | 7 | jcenter() | |
| 7 | 8 | ||
| 8 | 9 | } | |
| 9 | 10 | 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 | + | ||
| 12 | 14 | // NOTE: Do not place your application dependencies here; they belong | |
| 13 | 15 | // in the individual module build.gradle files | |
| 14 | 16 | } |
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 | < | ||
| 0 | 14 | < | \ 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 | < | ||
| 0 | 34 | < | \ 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 | < | ||
| 0 | 8 | < | \ 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 | < | ||
| 0 | 18 | < | \ 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 | < | ||
| 0 | 16 | < | \ No newline at end of file |
settings.gradle
| 1 | - | include ':app' | |
| 1 | + | include ':app', ':furiganatextview' | |
| 1 | < | ||
| 0 | 2 | < | \ No newline at end of file |