nani/app/rubytextview/src/main/java/me/weilunli/views/RubyTextView.java

RubyTextView.java

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
}
286