Change furiganatextview to a better version.

Julien LepillerSat Jul 31 00:46:33+0200 2021

d9e2047

Change furiganatextview to a better version.

.idea/gradle.xml

1111
          <set>
1212
            <option value="$PROJECT_DIR$" />
1313
            <option value="$PROJECT_DIR$/app" />
14-
            <option value="$PROJECT_DIR$/furiganatextview" />
14+
            <option value="$PROJECT_DIR$/rubytextview" />
1515
          </set>
1616
        </option>
1717
        <option name="resolveModulePerSourceSet" value="false" />
1818
      </GradleProjectSettings>
1919
    </option>
2020
  </component>
21-
</project>
21+
</project>
21<
022<
\ No newline at end of file

app/build.gradle

2929
    implementation 'com.andree-surya:moji4j:1.0.0'
3030
    implementation 'com.google.android:flexbox:2.0.1'
3131
    implementation 'com.google.android.material:material:1.4.0'
32-
    implementation project(path: ':furiganatextview')
32+
    implementation project(path: ':rubytextview')
3333
    testImplementation 'junit:junit:4.12'
3434
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
3535
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

app/src/main/java/eu/lepiller/nani/ResultPagerAdapter.java

22
33
import android.content.Context;
44
import android.content.Intent;
5-
import android.os.Build;
65
import android.text.Html;
76
import android.util.Log;
8-
import android.view.Gravity;
97
import android.view.LayoutInflater;
108
import android.view.View;
119
import android.view.ViewGroup;

1513
import androidx.annotation.NonNull;
1614
import androidx.recyclerview.widget.RecyclerView;
1715
18-
import org.w3c.dom.Text;
19-
2016
import java.util.ArrayList;
2117
import java.util.HashMap;
2218
import java.util.List;
2319
import java.util.Map;
2420
21+
import eu.lepiller.views.RubyTextView;
2522
import eu.lepiller.nani.result.KanjiResult;
2623
import eu.lepiller.nani.result.Result;
27-
import se.fekete.furiganatextview.furiganaview.FuriganaTextView;
2824
2925
public class ResultPagerAdapter extends RecyclerView.Adapter<ResultPagerAdapter.ViewHolder> {
3026
    static List<Result> results = new ArrayList<>();

133129
            for(Result result: results) {
134130
                View child_result = LayoutInflater.from(context).inflate(R.layout.layout_result, result_view, false);
135131
136-
                FuriganaTextView kanji_view = child_result.findViewById(R.id.kanji_view);
132+
                RubyTextView kanji_view = child_result.findViewById(R.id.kanji_view);
137133
                TextView reading_view = child_result.findViewById(R.id.reading_view);
138134
                LinearLayout senses_view = child_result.findViewById(R.id.sense_view);
139135
                TextView additional_info = child_result.findViewById(R.id.additional_info_view);

141137
142138
                // Populate the data into the template view using the data object
143139
                if(readingStyle.compareTo("furigana") == 0) {
144-
                    kanji_view.setFuriganaText(result.getKanjiFurigana());
140+
                    kanji_view.setCombinedText(result.getKanjiFurigana());
145141
                } else {
146-
                    kanji_view.setFuriganaText(result.getKanji());
142+
                    kanji_view.setCombinedText(result.getKanji());
147143
                    reading_view.setVisibility(View.VISIBLE);
148144
                    reading_view.setText(readingStyle.compareTo("kana") == 0? result.getReading(): result.getRomajiReading());
149145
                }

app/src/main/java/eu/lepiller/nani/result/Result.java

191191
        int group = 1;
192192
        for(String s: portions) {
193193
            if(Character.UnicodeBlock.of(s.charAt(0)) == CJK_UNIFIED_IDEOGRAPHS) {
194-
                current.append("<ruby>");
195194
                current.append(s);
196-
                current.append("<rt>");
195+
                current.append("|");
197196
                current.append(m.group(group));
198-
                current.append("</rt>");
199-
                current.append("</ruby>");
197+
                current.append(" ");
200198
                group++;
201199
            } else {
202200
                current.append(s);
201+
                current.append(" ");
203202
            }
204203
        }
205204
        Log.v("RESULT", "Finaly: " + current.toString());

app/src/main/res/layout/layout_result.xml

1818
            android:layout_height="wrap_content"
1919
            android:layout_gravity="center"
2020
            android:orientation="vertical">
21-
            <se.fekete.furiganatextview.furiganaview.FuriganaTextView
21+
            <me.weilunli.views.RubyTextView
2222
                android:id="@+id/kanji_view"
23-
                android:layout_width="match_parent"
23+
                android:layout_width="wrap_content"
2424
                android:layout_height="wrap_content"
2525
                android:layout_marginLeft="8dp"
2626
                android:layout_marginStart="8dp"

2929
                android:textAlignment="center"
3030
                android:contentDescription="@string/kanji_description"
3131
                android:textIsSelectable="true"
32-
                app:contains_ruby_tags="true"
3332
                android:textSize="@dimen/title_size"
33+
                app:rubyTextSize="@dimen/normal_size"
3434
                android:gravity="center_horizontal" />
3535
3636
            <TextView

4646
                android:contentDescription="@string/kanji_description"
4747
                android:textIsSelectable="true"
4848
                android:gravity="center_horizontal"
49+
                android:minWidth="@dimen/title_size"
4950
                android:visibility="gone" />
5051
        </LinearLayout>
5152

app/src/main/res/values/dimens.xml

22
    <dimen name="fab_margin">16dp</dimen>
33
    <dimen name="title_size">32sp</dimen>
44
    <dimen name="subtitle_size">24sp</dimen>
5+
    <dimen name="normal_size">18sp</dimen>
56
    <dimen name="text_margin">16dp</dimen>
67
</resources>

build.gradle

11
// Top-level build file where you can add configuration options common to all sub-projects/modules.
22
33
buildscript {
4-
    ext.kotlin_version = '1.3.72'
54
    repositories {
65
        google()
76
        jcenter()
87
    }
98
    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'
1210
1311
        // NOTE: Do not place your application dependencies here; they belong
1412
        // 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>
140>
\ 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>
340>
\ 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>
80>
\ 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>
180>
\ 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>
160>
\ No newline at end of file

rubytextview/.gitignore unknown status 1

1+
/build
1<
02<
\ 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<
033<
\ 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<
022<
\ 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<
027<
\ 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<
06<
\ 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<
012<
\ 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<
018<
\ No newline at end of file

settings.gradle

1-
include ':app', ':furiganatextview'
1+
include ':rubytextview'
2+
include ':app'