Change furiganatextview to a better version.
.idea/gradle.xml
11 | 11 | <set> | |
12 | 12 | <option value="$PROJECT_DIR$" /> | |
13 | 13 | <option value="$PROJECT_DIR$/app" /> | |
14 | - | <option value="$PROJECT_DIR$/furiganatextview" /> | |
14 | + | <option value="$PROJECT_DIR$/rubytextview" /> | |
15 | 15 | </set> | |
16 | 16 | </option> | |
17 | 17 | <option name="resolveModulePerSourceSet" value="false" /> | |
18 | 18 | </GradleProjectSettings> | |
19 | 19 | </option> | |
20 | 20 | </component> | |
21 | - | </project> | |
21 | + | </project> | |
21 | < | ||
0 | 22 | < | \ No newline at end of file |
app/build.gradle
29 | 29 | implementation 'com.andree-surya:moji4j:1.0.0' | |
30 | 30 | implementation 'com.google.android:flexbox:2.0.1' | |
31 | 31 | implementation 'com.google.android.material:material:1.4.0' | |
32 | - | implementation project(path: ':furiganatextview') | |
32 | + | implementation project(path: ':rubytextview') | |
33 | 33 | testImplementation 'junit:junit:4.12' | |
34 | 34 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' | |
35 | 35 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' |
app/src/main/java/eu/lepiller/nani/ResultPagerAdapter.java
2 | 2 | ||
3 | 3 | import android.content.Context; | |
4 | 4 | import android.content.Intent; | |
5 | - | import android.os.Build; | |
6 | 5 | import android.text.Html; | |
7 | 6 | import android.util.Log; | |
8 | - | import android.view.Gravity; | |
9 | 7 | import android.view.LayoutInflater; | |
10 | 8 | import android.view.View; | |
11 | 9 | import android.view.ViewGroup; | |
… | |||
15 | 13 | import androidx.annotation.NonNull; | |
16 | 14 | import androidx.recyclerview.widget.RecyclerView; | |
17 | 15 | ||
18 | - | import org.w3c.dom.Text; | |
19 | - | ||
20 | 16 | import java.util.ArrayList; | |
21 | 17 | import java.util.HashMap; | |
22 | 18 | import java.util.List; | |
23 | 19 | import java.util.Map; | |
24 | 20 | ||
21 | + | import eu.lepiller.views.RubyTextView; | |
25 | 22 | import eu.lepiller.nani.result.KanjiResult; | |
26 | 23 | import eu.lepiller.nani.result.Result; | |
27 | - | import se.fekete.furiganatextview.furiganaview.FuriganaTextView; | |
28 | 24 | ||
29 | 25 | public class ResultPagerAdapter extends RecyclerView.Adapter<ResultPagerAdapter.ViewHolder> { | |
30 | 26 | static List<Result> results = new ArrayList<>(); | |
… | |||
133 | 129 | for(Result result: results) { | |
134 | 130 | View child_result = LayoutInflater.from(context).inflate(R.layout.layout_result, result_view, false); | |
135 | 131 | ||
136 | - | FuriganaTextView kanji_view = child_result.findViewById(R.id.kanji_view); | |
132 | + | RubyTextView kanji_view = child_result.findViewById(R.id.kanji_view); | |
137 | 133 | TextView reading_view = child_result.findViewById(R.id.reading_view); | |
138 | 134 | LinearLayout senses_view = child_result.findViewById(R.id.sense_view); | |
139 | 135 | TextView additional_info = child_result.findViewById(R.id.additional_info_view); | |
… | |||
141 | 137 | ||
142 | 138 | // Populate the data into the template view using the data object | |
143 | 139 | if(readingStyle.compareTo("furigana") == 0) { | |
144 | - | kanji_view.setFuriganaText(result.getKanjiFurigana()); | |
140 | + | kanji_view.setCombinedText(result.getKanjiFurigana()); | |
145 | 141 | } else { | |
146 | - | kanji_view.setFuriganaText(result.getKanji()); | |
142 | + | kanji_view.setCombinedText(result.getKanji()); | |
147 | 143 | reading_view.setVisibility(View.VISIBLE); | |
148 | 144 | reading_view.setText(readingStyle.compareTo("kana") == 0? result.getReading(): result.getRomajiReading()); | |
149 | 145 | } |
app/src/main/java/eu/lepiller/nani/result/Result.java
191 | 191 | int group = 1; | |
192 | 192 | for(String s: portions) { | |
193 | 193 | if(Character.UnicodeBlock.of(s.charAt(0)) == CJK_UNIFIED_IDEOGRAPHS) { | |
194 | - | current.append("<ruby>"); | |
195 | 194 | current.append(s); | |
196 | - | current.append("<rt>"); | |
195 | + | current.append("|"); | |
197 | 196 | current.append(m.group(group)); | |
198 | - | current.append("</rt>"); | |
199 | - | current.append("</ruby>"); | |
197 | + | current.append(" "); | |
200 | 198 | group++; | |
201 | 199 | } else { | |
202 | 200 | current.append(s); | |
201 | + | current.append(" "); | |
203 | 202 | } | |
204 | 203 | } | |
205 | 204 | Log.v("RESULT", "Finaly: " + current.toString()); |
app/src/main/res/layout/layout_result.xml
18 | 18 | android:layout_height="wrap_content" | |
19 | 19 | android:layout_gravity="center" | |
20 | 20 | android:orientation="vertical"> | |
21 | - | <se.fekete.furiganatextview.furiganaview.FuriganaTextView | |
21 | + | <me.weilunli.views.RubyTextView | |
22 | 22 | android:id="@+id/kanji_view" | |
23 | - | android:layout_width="match_parent" | |
23 | + | android:layout_width="wrap_content" | |
24 | 24 | android:layout_height="wrap_content" | |
25 | 25 | android:layout_marginLeft="8dp" | |
26 | 26 | android:layout_marginStart="8dp" | |
… | |||
29 | 29 | android:textAlignment="center" | |
30 | 30 | android:contentDescription="@string/kanji_description" | |
31 | 31 | android:textIsSelectable="true" | |
32 | - | app:contains_ruby_tags="true" | |
33 | 32 | android:textSize="@dimen/title_size" | |
33 | + | app:rubyTextSize="@dimen/normal_size" | |
34 | 34 | android:gravity="center_horizontal" /> | |
35 | 35 | ||
36 | 36 | <TextView | |
… | |||
46 | 46 | android:contentDescription="@string/kanji_description" | |
47 | 47 | android:textIsSelectable="true" | |
48 | 48 | android:gravity="center_horizontal" | |
49 | + | android:minWidth="@dimen/title_size" | |
49 | 50 | android:visibility="gone" /> | |
50 | 51 | </LinearLayout> | |
51 | 52 |
app/src/main/res/values/dimens.xml
2 | 2 | <dimen name="fab_margin">16dp</dimen> | |
3 | 3 | <dimen name="title_size">32sp</dimen> | |
4 | 4 | <dimen name="subtitle_size">24sp</dimen> | |
5 | + | <dimen name="normal_size">18sp</dimen> | |
5 | 6 | <dimen name="text_margin">16dp</dimen> | |
6 | 7 | </resources> |
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.72' | |
5 | 4 | repositories { | |
6 | 5 | google() | |
7 | 6 | jcenter() | |
8 | 7 | } | |
9 | 8 | dependencies { | |
10 | - | classpath 'com.android.tools.build:gradle:4.0.0' | |
11 | - | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" | |
9 | + | classpath 'com.android.tools.build:gradle:4.0.2' | |
12 | 10 | ||
13 | 11 | // NOTE: Do not place your application dependencies here; they belong | |
14 | 12 | // in the individual module build.gradle files |
furiganatextview/.gitignore unknown status 2
1 | - | /build |
furiganatextview/build.gradle unknown status 2
1 | - | apply plugin: 'com.android.library' | |
2 | - | apply plugin: 'kotlin-android' | |
3 | - | ||
4 | - | android { | |
5 | - | compileSdkVersion 29 | |
6 | - | ||
7 | - | defaultConfig { | |
8 | - | minSdkVersion 15 | |
9 | - | targetSdkVersion 29 | |
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 'androidx.appcompat:appcompat:1.3.0' | |
25 | - | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" | |
26 | - | } | |
27 | - | repositories { | |
28 | - | mavenCentral() | |
29 | - | } |
furiganatextview/proguard-rules.pro unknown status 2
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 2
1 | - | package eu.lepiller.furiganatextview; | |
2 | - | ||
3 | - | import android.content.Context; | |
4 | - | import androidx.test.platform.app.InstrumentationRegistry; | |
5 | - | import androidx.test.ext.junit.runners.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 2
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 | > | ||
14 | 0 | > | \ No newline at end of file |
furiganatextview/src/main/AndroidManifest.xml unknown status 2
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 2
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 2
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 2
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 2
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 2
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 2
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 | > | ||
34 | 0 | > | \ No newline at end of file |
furiganatextview/src/main/java/se/fekete/furiganatextview/furiganaview/TextNormal.kt unknown status 2
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 2
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 2
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 | > | ||
8 | 0 | > | \ No newline at end of file |
furiganatextview/src/main/res/values/strings.xml unknown status 2
1 | - | <resources> | |
2 | - | <string name="app_name">FuriganaTextView</string> | |
3 | - | </resources> |
furiganatextview/src/test/java/eu/lepiller/furiganatextview/ExampleUnitTest.java unknown status 2
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 | > | ||
18 | 0 | > | \ No newline at end of file |
furiganatextview/src/test/java/se/fekete/furiganatextview/ExampleUnitTest.java unknown status 2
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 | > | ||
16 | 0 | > | \ No newline at end of file |
rubytextview/.gitignore unknown status 1
1 | + | /build | |
1 | < | ||
0 | 2 | < | \ No newline at end of file |
rubytextview/build.gradle unknown status 1
1 | + | apply plugin: 'com.android.library' | |
2 | + | ||
3 | + | android { | |
4 | + | compileSdkVersion 29 | |
5 | + | buildToolsVersion "29.0.3" | |
6 | + | ||
7 | + | defaultConfig { | |
8 | + | minSdkVersion 15 | |
9 | + | targetSdkVersion 29 | |
10 | + | versionCode 1 | |
11 | + | versionName "1.0" | |
12 | + | ||
13 | + | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" | |
14 | + | consumerProguardFiles "consumer-rules.pro" | |
15 | + | } | |
16 | + | ||
17 | + | buildTypes { | |
18 | + | release { | |
19 | + | minifyEnabled false | |
20 | + | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' | |
21 | + | } | |
22 | + | } | |
23 | + | } | |
24 | + | ||
25 | + | dependencies { | |
26 | + | implementation fileTree(dir: "libs", include: ["*.jar"]) | |
27 | + | implementation 'androidx.appcompat:appcompat:1.3.1' | |
28 | + | testImplementation 'junit:junit:4.12' | |
29 | + | androidTestImplementation 'androidx.test.ext:junit:1.1.3' | |
30 | + | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' | |
31 | + | ||
32 | + | } | |
32 | < | ||
0 | 33 | < | \ No newline at end of file |
rubytextview/consumer-rules.pro unknown status 1
rubytextview/proguard-rules.pro unknown status 1
1 | + | # Add project specific ProGuard rules here. | |
2 | + | # You can control the set of applied configuration files using the | |
3 | + | # proguardFiles setting in build.gradle. | |
4 | + | # | |
5 | + | # For more details, see | |
6 | + | # http://developer.android.com/guide/developing/tools/proguard.html | |
7 | + | ||
8 | + | # If your project uses WebView with JS, uncomment the following | |
9 | + | # and specify the fully qualified class name to the JavaScript interface | |
10 | + | # class: | |
11 | + | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { | |
12 | + | # public *; | |
13 | + | #} | |
14 | + | ||
15 | + | # Uncomment this to preserve the line number information for | |
16 | + | # debugging stack traces. | |
17 | + | #-keepattributes SourceFile,LineNumberTable | |
18 | + | ||
19 | + | # If you keep the line number information, uncomment this to | |
20 | + | # hide the original source file name. | |
21 | + | #-renamesourcefileattribute SourceFile | |
21 | < | ||
0 | 22 | < | \ No newline at end of file |
rubytextview/src/androidTest/java/eu/lepiller/views/ExampleInstrumentedTest.java unknown status 1
1 | + | package eu.lepiller.views; | |
2 | + | ||
3 | + | import android.content.Context; | |
4 | + | ||
5 | + | import androidx.test.platform.app.InstrumentationRegistry; | |
6 | + | import androidx.test.ext.junit.runners.AndroidJUnit4; | |
7 | + | ||
8 | + | import org.junit.Test; | |
9 | + | import org.junit.runner.RunWith; | |
10 | + | ||
11 | + | import static org.junit.Assert.*; | |
12 | + | ||
13 | + | /** | |
14 | + | * Instrumented test, which will execute on an Android device. | |
15 | + | * | |
16 | + | * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> | |
17 | + | */ | |
18 | + | @RunWith(AndroidJUnit4.class) | |
19 | + | public class ExampleInstrumentedTest { | |
20 | + | @Test | |
21 | + | public void useAppContext() { | |
22 | + | // Context of the app under test. | |
23 | + | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); | |
24 | + | assertEquals("eu.lepiller.furiganatextview.test", appContext.getPackageName()); | |
25 | + | } | |
26 | + | } | |
26 | < | ||
0 | 27 | < | \ No newline at end of file |
rubytextview/src/main/AndroidManifest.xml unknown status 1
1 | + | <manifest xmlns:android="http://schemas.android.com/apk/res/android" | |
2 | + | package="me.weilunli.views"> | |
3 | + | ||
4 | + | / | |
5 | + | </manifest> | |
5 | < | ||
0 | 6 | < | \ No newline at end of file |
rubytextview/src/main/java/me/weilunli/views/RubyTextView.java unknown status 1
1 | + | package me.weilunli.views; | |
2 | + | ||
3 | + | import android.content.Context; | |
4 | + | import android.content.res.TypedArray; | |
5 | + | import android.graphics.Canvas; | |
6 | + | import android.graphics.Paint; | |
7 | + | import android.os.Build; | |
8 | + | import android.util.AttributeSet; | |
9 | + | import android.util.TypedValue; | |
10 | + | ||
11 | + | import androidx.appcompat.widget.AppCompatTextView; | |
12 | + | ||
13 | + | import java.util.ArrayList; | |
14 | + | import java.util.List; | |
15 | + | ||
16 | + | public class RubyTextView extends AppCompatTextView { | |
17 | + | ||
18 | + | private Paint textPaint; | |
19 | + | private Paint rubyTextPaint; | |
20 | + | private String combinedText = ""; | |
21 | + | private float rubyTextSize= 28f; | |
22 | + | private int rubyTextColor ; | |
23 | + | private float spacing = 0f; | |
24 | + | private float lineSpacingExtra; | |
25 | + | ||
26 | + | // Need to address first line because it don't need extra spacing. | |
27 | + | private float lineheight = 0; | |
28 | + | private float firstLineheight = 0; | |
29 | + | StringBuilder originalText; | |
30 | + | List<String[]> combinedTextArray; | |
31 | + | ||
32 | + | ||
33 | + | public RubyTextView(Context context) { | |
34 | + | super(context); | |
35 | + | initialize(); | |
36 | + | setValue(); | |
37 | + | } | |
38 | + | ||
39 | + | public RubyTextView(Context context, AttributeSet attrs) { | |
40 | + | super(context, attrs); | |
41 | + | ||
42 | + | initialize(); | |
43 | + | ||
44 | + | TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.RubyTextView); | |
45 | + | try { | |
46 | + | TypedValue tv = new TypedValue(); | |
47 | + | getContext().getTheme().resolveAttribute(android.R.attr.textColorPrimary, tv, true); | |
48 | + | combinedText = ta.getString(R.styleable.RubyTextView_combinedText); | |
49 | + | rubyTextSize = ta.getDimension(R.styleable.RubyTextView_rubyTextSize, 28f); | |
50 | + | rubyTextColor = ta.getColor(R.styleable.RubyTextView_rubyTextColor, rubyTextColor); | |
51 | + | spacing = ta.getDimension(R.styleable.RubyTextView_spacing, 0); | |
52 | + | lineSpacingExtra = ta.getDimension(R.styleable.RubyTextView_lineSpacingExtra, 0); | |
53 | + | ||
54 | + | } finally { | |
55 | + | ta.recycle(); | |
56 | + | } | |
57 | + | ||
58 | + | setValue(); | |
59 | + | } | |
60 | + | ||
61 | + | ||
62 | + | private void initialize() { | |
63 | + | textPaint = getPaint(); | |
64 | + | rubyTextPaint = new Paint(); | |
65 | + | originalText = new StringBuilder(); | |
66 | + | rubyTextColor = getCurrentTextColor(); | |
67 | + | combinedTextArray = new ArrayList<>(); | |
68 | + | } | |
69 | + | ||
70 | + | ||
71 | + | private void setValue() { | |
72 | + | textPaint.setColor(getCurrentTextColor()); | |
73 | + | rubyTextPaint.setTextSize((getRubyTextSize())); | |
74 | + | rubyTextPaint.setColor(getRubyTextColor()); | |
75 | + | lineheight = getTextSize() + getRubyTextSize() + getLineSpacingExtra() + getSpacing(); | |
76 | + | firstLineheight = lineheight - getLineSpacingExtra(); | |
77 | + | splitCombinedText(); | |
78 | + | setLineHeight((int) lineheight); | |
79 | + | } | |
80 | + | ||
81 | + | public float getLineSpacingExtra() { | |
82 | + | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { | |
83 | + | return super.getLineSpacingExtra(); | |
84 | + | } | |
85 | + | return lineSpacingExtra; | |
86 | + | } | |
87 | + | ||
88 | + | ||
89 | + | private int getMySize(int measureSpec, int mBoundLength) { | |
90 | + | int result; | |
91 | + | int specMode = MeasureSpec.getMode(measureSpec); | |
92 | + | int specSize = MeasureSpec.getSize(measureSpec); | |
93 | + | if (specMode == MeasureSpec.EXACTLY) { | |
94 | + | result = specSize; | |
95 | + | } else if (specMode == MeasureSpec.AT_MOST) { | |
96 | + | result = Math.min(mBoundLength, specSize); | |
97 | + | } else { | |
98 | + | result = mBoundLength; | |
99 | + | } | |
100 | + | return result; | |
101 | + | } | |
102 | + | ||
103 | + | ||
104 | + | @Override | |
105 | + | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | |
106 | + | ||
107 | + | int width = MeasureSpec.getSize(widthMeasureSpec); | |
108 | + | float cur_x = 0; | |
109 | + | int lineCount = 1; | |
110 | + | float maxwidth = 0; | |
111 | + | ||
112 | + | for(String[] t : combinedTextArray) { | |
113 | + | float textWidth = textPaint.measureText(t[0]); | |
114 | + | float rubyWidth = rubyTextPaint.measureText(t[1]); | |
115 | + | float elementWidth = Math.max(textWidth, rubyWidth); | |
116 | + | ||
117 | + | // if t[0] == '\n' | |
118 | + | if(t[0].equals(System.getProperty("line.separator"))){ | |
119 | + | cur_x = 0; | |
120 | + | lineCount++; | |
121 | + | continue; | |
122 | + | } | |
123 | + | ||
124 | + | if (cur_x + elementWidth > width){ | |
125 | + | cur_x = 0; | |
126 | + | lineCount++; | |
127 | + | } | |
128 | + | ||
129 | + | cur_x += elementWidth; | |
130 | + | if(cur_x > maxwidth) | |
131 | + | maxwidth = cur_x; | |
132 | + | } | |
133 | + | ||
134 | + | // total height | |
135 | + | int height = getMySize(heightMeasureSpec, | |
136 | + | (int) (firstLineheight + lineheight * (lineCount-1)) + getLastBaselineToBottomHeight()); | |
137 | + | setMeasuredDimension((int) maxwidth, height); | |
138 | + | } | |
139 | + | ||
140 | + | @Override | |
141 | + | protected void onDraw(Canvas canvas) { | |
142 | + | boolean isFirstLine = true; | |
143 | + | float cur_x = 0; | |
144 | + | float cur_y = firstLineheight; | |
145 | + | for(String[] t : combinedTextArray) { | |
146 | + | /* ********** | |
147 | + | * Draw text * | |
148 | + | * ***********/ | |
149 | + | float textWidth = textPaint.measureText(t[0]); | |
150 | + | float rubyWidth = rubyTextPaint.measureText(t[1]); | |
151 | + | float elementWidth = Math.max(textWidth, rubyWidth); | |
152 | + | ||
153 | + | if(t[0].equals(System.getProperty("line.separator"))){ | |
154 | + | cur_x = 0; | |
155 | + | if(isFirstLine) isFirstLine = false; | |
156 | + | cur_y += lineheight; | |
157 | + | continue; | |
158 | + | } | |
159 | + | ||
160 | + | if (cur_x + textWidth > getWidth()) { | |
161 | + | cur_x = 0; | |
162 | + | if(isFirstLine) isFirstLine = false; | |
163 | + | cur_y += lineheight; | |
164 | + | } | |
165 | + | float text_posX = cur_x + (1 / 2f) * (elementWidth - textWidth); | |
166 | + | canvas.drawText(t[0], text_posX, cur_y, textPaint); | |
167 | + | ||
168 | + | /* **************** | |
169 | + | * Draw ruby text * | |
170 | + | * ****************/ | |
171 | + | float rubyText_posX = cur_x + (1 / 2f) * (elementWidth - rubyWidth); | |
172 | + | canvas.drawText(t[1], rubyText_posX, cur_y - getTextSize() - getSpacing(), rubyTextPaint); | |
173 | + | ||
174 | + | // update cur_x position | |
175 | + | cur_x += elementWidth; | |
176 | + | } | |
177 | + | } | |
178 | + | ||
179 | + | public String getCombinedText() { | |
180 | + | return combinedText; | |
181 | + | } | |
182 | + | public float getRubyTextSize() { | |
183 | + | return rubyTextSize; | |
184 | + | } | |
185 | + | public float getSpacing() { | |
186 | + | return spacing; | |
187 | + | } | |
188 | + | public int getRubyTextColor() { | |
189 | + | return rubyTextColor; | |
190 | + | } | |
191 | + | ||
192 | + | private void updateLineheight(){ | |
193 | + | lineheight = getTextSize() + getRubyTextSize() + getLineSpacingExtra() + getSpacing(); | |
194 | + | firstLineheight = lineheight - getLineSpacingExtra(); | |
195 | + | } | |
196 | + | ||
197 | + | public void setCombinedText(String text) { | |
198 | + | combinedText = text; | |
199 | + | splitCombinedText(); | |
200 | + | requestLayout(); | |
201 | + | invalidate(); | |
202 | + | } | |
203 | + | public void setRubyTextSize(float textSize) { | |
204 | + | rubyTextSize = sp2px(textSize); | |
205 | + | rubyTextPaint.setTextSize(rubyTextSize); | |
206 | + | updateLineheight(); | |
207 | + | invalidate(); | |
208 | + | requestLayout(); | |
209 | + | } | |
210 | + | public void setRubyTextColor(int color) { | |
211 | + | rubyTextColor = color; | |
212 | + | rubyTextPaint.setColor(rubyTextColor); | |
213 | + | invalidate(); | |
214 | + | } | |
215 | + | ||
216 | + | @Override | |
217 | + | public void setLetterSpacing(float letterSpacing) { | |
218 | + | super.setLetterSpacing(letterSpacing); | |
219 | + | invalidate(); | |
220 | + | requestLayout(); | |
221 | + | } | |
222 | + | ||
223 | + | @Override | |
224 | + | public void setTextSize(float size) { | |
225 | + | super.setTextSize(size); | |
226 | + | updateLineheight(); | |
227 | + | requestLayout(); | |
228 | + | invalidate(); | |
229 | + | } | |
230 | + | ||
231 | + | public void setSpacing(float spacing) { | |
232 | + | this.spacing = dp2px(spacing); | |
233 | + | updateLineheight(); | |
234 | + | invalidate(); | |
235 | + | requestLayout(); | |
236 | + | } | |
237 | + | ||
238 | + | @Override | |
239 | + | public void setTextColor(int color) { | |
240 | + | textPaint.setColor(color); | |
241 | + | super.setTextColor(color); | |
242 | + | } | |
243 | + | ||
244 | + | public void splitCombinedText() { | |
245 | + | combinedTextArray.clear(); | |
246 | + | originalText.setLength(0); | |
247 | + | if(getCombinedText() == null) | |
248 | + | return; | |
249 | + | ||
250 | + | String[] split = getCombinedText().split(" "); | |
251 | + | for (String value : split) { | |
252 | + | String[] t = value.split("\\|"); | |
253 | + | if (t.length == 2) { | |
254 | + | if ((t[1].equals("-"))) { | |
255 | + | combinedTextArray.add(new String[]{t[0], ""}); | |
256 | + | } else { | |
257 | + | combinedTextArray.add(new String[]{t[0], t[1]}); | |
258 | + | } | |
259 | + | } else { | |
260 | + | for (int j = 0; j < t[0].length(); j++) { | |
261 | + | String s = String.valueOf(t[0].charAt(j)); | |
262 | + | combinedTextArray.add(new String[]{s, ""}); | |
263 | + | } | |
264 | + | } | |
265 | + | originalText.append(t[0]); | |
266 | + | } | |
267 | + | setText(originalText); | |
268 | + | } | |
269 | + | ||
270 | + | ||
271 | + | /** | |
272 | + | * convert dp to its equivalent px | |
273 | + | */ | |
274 | + | private float dp2px(float dp) { | |
275 | + | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()); | |
276 | + | } | |
277 | + | ||
278 | + | /** | |
279 | + | * convert sp to its equivalent px | |
280 | + | */ | |
281 | + | private float sp2px(float sp) { | |
282 | + | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics()); | |
283 | + | } | |
284 | + | ||
285 | + | } |
rubytextview/src/main/res/values/attrs.xml unknown status 1
1 | + | <?xml version="1.0" encoding="utf-8"?> | |
2 | + | <resources> | |
3 | + | <declare-styleable name="RubyTextView"> | |
4 | + | <attr name="combinedText" format="string"/> | |
5 | + | <attr name="rubyText" format="string"/> | |
6 | + | <attr name="rubyTextSize" format="dimension"/> | |
7 | + | <attr name="rubyTextColor" format="color"/> | |
8 | + | <attr name="spacing" format="dimension"/> | |
9 | + | <attr name="lineSpacingExtra" format="dimension"/> | |
10 | + | </declare-styleable> | |
11 | + | </resources> | |
11 | < | ||
0 | 12 | < | \ No newline at end of file |
rubytextview/src/main/res/values/strings.xml unknown status 1
1 | + | <resources> | |
2 | + | <string name="app_name">RubyTextView</string> | |
3 | + | </resources> |
rubytextview/src/test/java/eu/lepiller/views/ExampleUnitTest.java unknown status 1
1 | + | package eu.lepiller.views; | |
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 |
settings.gradle
1 | - | include ':app', ':furiganatextview' | |
1 | + | include ':rubytextview' | |
2 | + | include ':app' |