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 |