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 | combinedText = ta.getString(R.styleable.RubyTextView_combinedText); |
47 | rubyTextSize = ta.getDimension(R.styleable.RubyTextView_rubyTextSize, 28f); |
48 | rubyTextColor = ta.getColor(R.styleable.RubyTextView_rubyTextColor, rubyTextColor); |
49 | spacing = ta.getDimension(R.styleable.RubyTextView_spacing, 0); |
50 | lineSpacingExtra = ta.getDimension(R.styleable.RubyTextView_lineSpacingExtra, 0); |
51 | |
52 | } finally { |
53 | ta.recycle(); |
54 | } |
55 | |
56 | setValue(); |
57 | } |
58 | |
59 | |
60 | private void initialize() { |
61 | textPaint = getPaint(); |
62 | rubyTextPaint = new Paint(); |
63 | originalText = new StringBuilder(); |
64 | rubyTextColor = getCurrentTextColor(); |
65 | combinedTextArray = new ArrayList<>(); |
66 | } |
67 | |
68 | |
69 | private void setValue() { |
70 | textPaint.setColor(getCurrentTextColor()); |
71 | rubyTextPaint.setTextSize((getRubyTextSize())); |
72 | rubyTextPaint.setColor(getRubyTextColor()); |
73 | lineheight = getTextSize() + getRubyTextSize() + getLineSpacingExtra() + getSpacing(); |
74 | firstLineheight = lineheight - getLineSpacingExtra(); |
75 | splitCombinedText(); |
76 | setLineHeight((int) lineheight); |
77 | } |
78 | |
79 | public float getLineSpacingExtra() { |
80 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { |
81 | return super.getLineSpacingExtra(); |
82 | } |
83 | return lineSpacingExtra; |
84 | } |
85 | |
86 | |
87 | private int getMySize(int measureSpec, int mBoundLength) { |
88 | int result; |
89 | int specMode = MeasureSpec.getMode(measureSpec); |
90 | int specSize = MeasureSpec.getSize(measureSpec); |
91 | if (specMode == MeasureSpec.EXACTLY) { |
92 | result = specSize; |
93 | } else if (specMode == MeasureSpec.AT_MOST) { |
94 | result = Math.min(mBoundLength, specSize); |
95 | } else { |
96 | result = mBoundLength; |
97 | } |
98 | return result; |
99 | } |
100 | |
101 | |
102 | @Override |
103 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
104 | |
105 | int width = MeasureSpec.getSize(widthMeasureSpec); |
106 | float cur_x = 0; |
107 | int lineCount = 1; |
108 | float maxwidth = 0; |
109 | |
110 | for(String[] t : combinedTextArray) { |
111 | float textWidth = textPaint.measureText(t[0]); |
112 | float rubyWidth = rubyTextPaint.measureText(t[1]); |
113 | float elementWidth = Math.max(textWidth, rubyWidth); |
114 | |
115 | // if t[0] == '\n' |
116 | if(t[0].equals(System.getProperty("line.separator"))){ |
117 | cur_x = 0; |
118 | lineCount++; |
119 | continue; |
120 | } |
121 | |
122 | if (cur_x + elementWidth > width){ |
123 | cur_x = 0; |
124 | lineCount++; |
125 | } |
126 | |
127 | cur_x += elementWidth; |
128 | if(cur_x > maxwidth) |
129 | maxwidth = cur_x; |
130 | } |
131 | |
132 | // total height |
133 | int height = getMySize(heightMeasureSpec, |
134 | (int) (firstLineheight + lineheight * (lineCount-1)) + getLastBaselineToBottomHeight()); |
135 | setMeasuredDimension((int) maxwidth, height); |
136 | } |
137 | |
138 | @Override |
139 | protected void onDraw(Canvas canvas) { |
140 | boolean isFirstLine = true; |
141 | float cur_x = 0; |
142 | float cur_y = firstLineheight; |
143 | for(String[] t : combinedTextArray) { |
144 | /* ********** |
145 | * Draw text * |
146 | * ***********/ |
147 | float textWidth = textPaint.measureText(t[0]); |
148 | float rubyWidth = rubyTextPaint.measureText(t[1]); |
149 | float elementWidth = Math.max(textWidth, rubyWidth); |
150 | |
151 | if(t[0].equals(System.getProperty("line.separator"))){ |
152 | cur_x = 0; |
153 | if(isFirstLine) isFirstLine = false; |
154 | cur_y += lineheight; |
155 | continue; |
156 | } |
157 | |
158 | if (cur_x + textWidth > getWidth()) { |
159 | cur_x = 0; |
160 | if(isFirstLine) isFirstLine = false; |
161 | cur_y += lineheight; |
162 | } |
163 | float text_posX = cur_x + (1 / 2f) * (elementWidth - textWidth); |
164 | canvas.drawText(t[0], text_posX, cur_y, textPaint); |
165 | |
166 | /* **************** |
167 | * Draw ruby text * |
168 | * ****************/ |
169 | float rubyText_posX = cur_x + (1 / 2f) * (elementWidth - rubyWidth); |
170 | canvas.drawText(t[1], rubyText_posX, cur_y - getTextSize() - getSpacing(), rubyTextPaint); |
171 | |
172 | // update cur_x position |
173 | cur_x += elementWidth; |
174 | } |
175 | } |
176 | |
177 | public String getCombinedText() { |
178 | return combinedText; |
179 | } |
180 | public float getRubyTextSize() { |
181 | return rubyTextSize; |
182 | } |
183 | public float getSpacing() { |
184 | return spacing; |
185 | } |
186 | public int getRubyTextColor() { |
187 | return rubyTextColor; |
188 | } |
189 | |
190 | private void updateLineheight(){ |
191 | lineheight = getTextSize() + getRubyTextSize() + getLineSpacingExtra() + getSpacing(); |
192 | firstLineheight = lineheight - getLineSpacingExtra(); |
193 | } |
194 | |
195 | public void setCombinedText(String text) { |
196 | combinedText = text; |
197 | splitCombinedText(); |
198 | requestLayout(); |
199 | invalidate(); |
200 | } |
201 | public void setRubyTextSize(float textSize) { |
202 | rubyTextSize = sp2px(textSize); |
203 | rubyTextPaint.setTextSize(rubyTextSize); |
204 | updateLineheight(); |
205 | invalidate(); |
206 | requestLayout(); |
207 | } |
208 | public void setRubyTextColor(int color) { |
209 | rubyTextColor = color; |
210 | rubyTextPaint.setColor(rubyTextColor); |
211 | invalidate(); |
212 | } |
213 | |
214 | @Override |
215 | public void setLetterSpacing(float letterSpacing) { |
216 | super.setLetterSpacing(letterSpacing); |
217 | invalidate(); |
218 | requestLayout(); |
219 | } |
220 | |
221 | @Override |
222 | public void setTextSize(float size) { |
223 | super.setTextSize(size); |
224 | updateLineheight(); |
225 | requestLayout(); |
226 | invalidate(); |
227 | } |
228 | |
229 | public void setSpacing(float spacing) { |
230 | this.spacing = dp2px(spacing); |
231 | updateLineheight(); |
232 | invalidate(); |
233 | requestLayout(); |
234 | } |
235 | |
236 | @Override |
237 | public void setTextColor(int color) { |
238 | textPaint.setColor(color); |
239 | super.setTextColor(color); |
240 | } |
241 | |
242 | public void splitCombinedText() { |
243 | combinedTextArray.clear(); |
244 | originalText.setLength(0); |
245 | if(getCombinedText() == null) |
246 | return; |
247 | |
248 | String[] split = getCombinedText().split(" "); |
249 | for (String value : split) { |
250 | String[] t = value.split("\\|"); |
251 | if (t.length == 2) { |
252 | if ((t[1].equals("-"))) { |
253 | combinedTextArray.add(new String[]{t[0], ""}); |
254 | } else { |
255 | combinedTextArray.add(new String[]{t[0], t[1]}); |
256 | } |
257 | } else { |
258 | for (int j = 0; j < t[0].length(); j++) { |
259 | String s = String.valueOf(t[0].charAt(j)); |
260 | combinedTextArray.add(new String[]{s, ""}); |
261 | } |
262 | } |
263 | originalText.append(t[0]); |
264 | } |
265 | setText(originalText); |
266 | } |
267 | |
268 | |
269 | /** |
270 | * convert dp to its equivalent px |
271 | */ |
272 | private float dp2px(float dp) { |
273 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()); |
274 | } |
275 | |
276 | /** |
277 | * convert sp to its equivalent px |
278 | */ |
279 | private float sp2px(float sp) { |
280 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics()); |
281 | } |
282 | |
283 | } |
284 |