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' |