自己紹介
こんにちは @wasabeef_jp です
去年に引き続き、今年も12/1を担当させて頂きます。
今回は、TextViewをもっとHackしようということで、Advanced TextViewを紹介します。
Advanced TextView
Compound Drawable
これは、とてもポピュラーな使い方で
TextViewのテキスト本文の上下左右にアイコンを表示したい場合などに有効です
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/animation"
android:drawableLeft="@drawable/rotating_loading"
android:drawableRight="@drawable/animated_wifi"
android:drawableBottom="@drawable/animated_clock"/>
<!--
android:drawableLeft
android:drawableRight
android:drawableTop
android:drawableBottom
-->
Drawable XML (Animatable)
上記で説明した android:drawable... にはアイコンは勿論ですが、Animation XMLなども定義が出来ます
- AnimatedRotateDrawableで、1つの画像を回転させる
<!-- res/drawable/rotating_loading.xml -->
<animated-rotate
android:pivotX="50%"
android:pivotY="50%"
android:drawable="@drawable/ic_loading"
android:duration="500" />
- AnimationDrawableで、画像をGifアニメーションように動かす
<!-- res/drawable/animated_wifi.xml -->
<animation-list>
<item android:drawable="@drawable/ic_wifi_0" android:duration="250" />
<item android:drawable="@drawable/ic_wifi_1" android:duration="250" />
<item android:drawable="@drawable/ic_wifi_2" android:duration="250" />
<item android:drawable="@drawable/ic_wifi_3" android:duration="250" />
</animation-list>
- AnimatedVectorDrawableで、細かいアニメーションを設定する
(Introduced in Lollipop)
<!-- res/drawable/animated_clock.xml -->
<animated-vector android:drawable="@drawable/clock">
<target android:name="hours" android:animation="@anim/hours_rotation" />
<target android:name="minutes" android:animation="@anim/minutes_rotation" />
</animated-vector>
<!-- anim-v21/hours_rotation.xml -->
<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="12000"
android:propertyName="rotation"
android:valueFrom="0"
android:valueTo="360"
android:repeatCount="-1"
android:interpolator="@android:anim/linear_interpolator"/>
<!-- anim-v21/minutes_rotation.xml -->
<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:propertyName="rotation"
android:valueFrom="0"
android:valueTo="360"
android:repeatCount="-1"
android:interpolator="@android:anim/linear_interpolator"/>
先ほど、説明したAnimatable XMLを利用し、アニメーションの開始をします
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/animation"
android:drawableLeft="@drawable/rotating_loading"
android:drawableRight="@drawable/animated_wifi"
android:drawableBottom="@drawable/animated_clock"/>
private void startAnimation(TextView textView) {
Drawable[] drawables = textView.getCompoundDrawables();
for (Drawable drawable : drawables) {
if (drawable != null && drawable instanceof Animatable) {
((Animatable) drawable).start();
}
}
}
Shadow
TextViewは、Canvasや9patchなどを使わずに影を簡単に描くことが出来ます
- 設定するプロパティは、shadowColor, shadowDx, shadowDy, shadowRadius
- dpで設定するのではなく、pxの単位になります
- また、paddingを入れてあげましょう
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:text="@string/shadow"
android:textSize="80sp"
android:textStyle="bold"
android:shadowColor="#7000"
android:shadowDx="12"
android:shadowDy="12"
android:shadowRadius="8"/>
Shadow, abused
単純な影だけではなく、応用することでこのように幅が広がります
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:text="@string/blocky"
android:textColor="@color/purple"
android:textSize="@dimen/80sp"
android:textStyle="bold"
android:shadowColor="@color/green"
android:shadowDx="4"
android:shadowDy="-4"
android:shadowRadius="1"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:text="@string/glow"
android:textSize="80sp"
android:textStyle="bold"
android:textColor="@android:color/white"
android:background="@android:color/black"
android:shadowColor="@color/yellow"
android:shadowDx="0"
android:shadowDy="0"
android:shadowRadius="24"/>
Custom font
- カスタムフォント(日本語もOK)も簡単に適用させることが出来ます
- アプリ全体にカスタムフォントを適用させたい場合などは
CalligraphyなどのFont Libraryを使いましょう
Typeface typeface = Typeface.createFromAsset(getAssets(), "Ruthie.ttf");
textView.setTypeface(typeface);
Gradient
- Shaderを用いることで、単純な色だけではなくグラデーションをあてることもできます
Shader shader = new LinearGradient(
0, 0, 0, textView.getTextSize(), Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
textView.getPaint().setShader(shader);
Pattern
- グラデーションだけでなく、文字をリピート画像でリッチに仕上げることも出来ます
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.cheetah_tile);
Shader shader = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
textView.getPaint().setShader(shader);
HTML
- TextViewは、Plain文字列だけでなく、HTML文字列も対応しています
- ※全てのHTML Tagに対応してる訳ではありません。
<!-- 簡単なHTMLの例 -->
<h1>Hello World</h1>
Here is an <img src="octopus"><i>octopus</i>.<br>
And here is a
<a href="http://d.android.com/reference/android/text/Html.html">
link</a>.
- 上記のHTMLをstrings.xmlに設定します。
<string name="from_html_text">
<![CDATA[
<h1>Hello World</h1>
Here is an <img src="octopus"><i>octopus</i>.<br>
And here is a
<a href="http://d.android.com/reference/android/text/Html.html">
link</a>.
]]>
</string>
HTML.fromHtml
- getStringで取得したデータをそのままTextView#setTextするのではなく HTML.fromHtmlを使います
String html = getString(R.string.from_html_text);
- imgタグに対応していても画像を表示してくれないので、Html.ImageGetterを継承して画像を処理します。
String html = getString(R.string.from_html_text);
textView.setText(Html.fromHtml(html, new ResouroceImageGetter(this), null));
private static class ResouroceImageGetter implements Html.ImageGetter {
private final Context context;
public ResouroceImageGetter(Context context) {
this.context = context;
}
public Drawable getDrawable(String source) {
int path = context.getResources().getIdentifier(source, "drawable", BuildConfig.APPLICATION_ID);
Drawable drawable = context.getResources().getDrawable(path);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
return drawable;
}
}
Clickable link
- aタグのリンクは、デフォルトで無効になっているので、有効にしたい場合は下記のようにします
textView.setMovementMethod(LinkMovementMethod.getInstance());
Supported tags
- 下記以外のタグは、TagHandlerを使って自分で対応していく必要があります
<!-- Supported Html tags -->
<a href="...">
<b>, <big>, <blockquote>, <br>, <cite>, <dfn>
<div align="...">, <em>, <font size="..." color="..." face="...">
<h1>, <h2>, <h3>, <h4>, <h5>, <h6>
<i>, <img src="...">, <p>, <small>
<strike>, <strong>, <sub>, <sup>, <tt>, <u>
SPAN
TextViewのSpan, TagHandlerは、とても強力で、カスタムしやすくなっています
by Flavien Laurent
http://flavienlaurent.com/blog/2014/01/31/spans/
One <u>two</u> three
↓
"One two three" + Underline from position 4 to 6
↓
spannableString.setSpan(new UnderlineSpan(), 4, 6, flags);
CharacterStyle
- 文字のフォーマットで使うキーワード
- e.g. ForegroundColorSpan, BackgroundColorSpan, UnderlineSpan
ParagraphStyle
- 段落のフォーマットで使うキーワード
- e.g. BulletSpan, IconMarginSpan, QuoteSpan
Stacked fractions
- 画像ではなく、分数の括線のように文字を積み重ねて表示することもできます
- ただ、setFontFeatureSettingsを使う場合は、Lollipop以上である必要があります
TextPaint#setFontFeatureSettings
@TargetApi(LOLLIPOP)
<string name="fraction_text">
<![CDATA[
1<afrc>1/2</afrc> + <afrc>11/16</afrc> = 2<afrc>3/16</afrc>
]]>
</string>
Typeface typeface = Typeface.createFromAsset(getAssets(), "Nutso2.otf");
textView.setTypeface(typeface);
- HTML.fromHtmlをする際にTagHandlerも指定います
String html = getString(R.string.fraction_text);
textView.setText(Html.fromHtml(html, null, new FractionTagHandler())
- Html.TagHandlerを継承してhandleTagの中のoutputに対して処理します
private static class FractionTagHandler implements Html.TagHandler {
@Override
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
if (!"afrc".equalsIgnoreCase(tag)) return;
int len = output.length();
if (opening) {
output.setSpan(new FractionSpan(), len, len, Spannable.SPAN_MARK_MARK);
} else {
Object obj = getLast(output, FractionSpan.class);
int where = output.getSpanStart(obj);
output.removeSpan(obj);
if (where != len) {
output.setSpan(new FractionSpan(), where, len, 0);
}
}
}
private Object getLast(Editable text, Class kind) {
Object[] objs = text.getSpans(0, text.length(), kind);
if (objs.length == 0) return null;
for (int i = objs.length - 1; i >= 0; --i) {
if(text.getSpanFlags(objs[i]) == Spannable.SPAN_MARK_MARK) {
return objs[i];
}
}
return null;
}
- Html.TagHandlerとSpan系は、相互に利用することが多く、ここではMetricAffectingSpanを使います
private static class FractionSpan extends MetricAffectingSpan {
public void updateMeasureState(TextPaint textPaint) {
textPaint.setFontFeatureSettings("afrc");
}
public void updateDrawState(TextPaint textPaint) {
textPaint.setFontFeatureSettings("afrc");
}
}
Arbitrary fractions
Styled string
- SpannableStringを使うとテキストに細かいスタイルを当てる事ができます
private static SpannableString formatString(Context context, int textId, int styleId) {
String text = context.getString(textId);
SpannableString spannableString = new SpannableString(text);
spannableString.setSpan(new TextAppearanceSpan(context, styleId), 0, text.length(), 0);
return spannableString;
}
- 一行毎に違うスタイルを当てていきます
SpannableStringBuilder builder = new SpannableStringBuilder()
.append(formatString(this, R.string.big_red, R.style.BigRedTextAppearance))
.append("\n")
.append(formatString(this, R.string.medium_green, R.style.MediumGreenTextAppearance))
.append("\n")
.append(formatString(this, R.string.small_blue, R.style.SmallBlueTextAppearance));
textView.setText(builder.subSequence(0, builder.length()));
- この例では、テキストの一部分を赤色太字するために下記のようなstyleを使います
<style name="BigRedTextAppearance" parent="@android:style/TextAppearance">
<item name="android:textSize">56sp</item>
<item name="android:textColor">#c00</item>
</style>
AlignmentSpan
AlignmentSpanを使うと指定したテキスト範囲の揃え位置を変更できます
public void click(View button) {
String text = editText.getText().toString();
Layout.Alignment align = button.getId() == R.id.add_to_right_button ?
Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_NORMAL;
appendText(text, align);
editText.setText(null);
}
private void appendText(CharSequence text, Layout.Alignment align) {
AlignmentSpan span = new AlignmentSpan.Standard(align);
SpannableString spannableString = new SpannableString(text);
spannableString.setSpan(span, 0, text.length(), 0);
if (textView.length() > 0) {
textView.append("\n\n");
}
textView.append(spannableString);
}
Rainbow span
- LinearGradientを応用して、テキストの一部分にGradientを当てます
String text = textView.getText().toString();
SpannableString spannableString = new SpannableString(text);
int start = text.toLowerCase().indexOf(substring);
int end = start + substring.length();
spannableString.setSpan(new RainbowSpan(this), start, end, 0);
private static class RainbowSpan extends CharacterStyle implements UpdateAppearance {
private final int[] colors;
public RainbowSpan(Context context) {
colors = context.getResources().getIntArray(R.array.rainbow);
}
public void updateDrawState(TextPaint paint) {
paint.setStyle(Paint.Style.FILL);
Shader shader = new LinearGradient(0, 0, 0, paint.getTextSize() * colors.length, colors,
null, Shader.TileMode.MIRROR);
Matrix matrix = new Matrix();
matrix.setRotate(90);
shader.setLocalMatrix(matrix);
paint.setShader(shader);
}
}
Animated rainbow span
実際、Spanは本当に強力で、自作さえすれば何でも出来ます
ここでは、テキストの一部にアニメーションを当ててみます
- spanに対してPropertyの作成ObjectAnimatorを設定します
private static final Property<AnimatedColorSpan, Float>
ANIMATED_COLOR_SPAN_FLOAT_PROPERTY
= new Property<AnimatedColorSpan, Float>(Float.class, "ANIMATED_COLOR_SPAN_FLOAT_PROPERTY") {
public void set(AnimatedColorSpan span, Float value) {
span.setTranslateXPercentage(value);
}
public Float get(AnimatedColorSpan span) {
return span.getTranslateXPercentage();
}
};
ObjectAnimator objectAnimator =
ObjectAnimator.ofFloat(span, ANIMATED_COLOR_SPAN_FLOAT_PROPERTY, 0, 100);
objectAnimator.setEvaluator(new FloatEvaluator());
objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
textView.setText(spannableString);
}
});
objectAnimator.setInterpolator(new LinearInterpolator());
objectAnimator.setDuration(DateUtils.MINUTE_IN_MILLIS * 3);
objectAnimator.setRepeatCount(ValueAnimator.INFINITE);
objectAnimator.start();
private static class AnimatedColorSpan extends CharacterStyle implements UpdateAppearance {
public AnimatedColorSpan(Context context) {
colors = context.getResources().getIntArray(R.array.rainbow);
}
public void setTranslateXPercentage(float value) {
translateXPercentage = value;
}
public float getTranslateXPercentage() {
return translateXPercentage;
}
public void updateDrawState(TextPaint paint) {
paint.setStyle(Paint.Style.FILL);
float width = paint.getTextSize() * colors.length;
if (this.shader == null) {
this.shader = new LinearGradient(0, 0, 0, width, colors, null, Shader.TileMode.MIRROR);
}
this.matrix.reset();
this.matrix.setRotate(90);
this.matrix.postTranslate(width * translateXPercentage, 0);
this.shader.setLocalMatrix(this.matrix);
paint.setShader(this.shader);
}
}
ClickableSpan
- TextView.setMovementMethodを応用すると、クリックのハンドリングも行えます
- SpannableString#setSpanで、適用するSpanクラス、開始から終了地点を指定します
String text = textView.getText().toString();
String goToSettings = getString(R.string.go_to_settings);
int start = text.indexOf(goToSettings);
int end = start + goToSettings.length();
SpannableString spannableString = new SpannableString(text);
spannableString.setSpan(new GoToSettingsSpan(), start, end, 0);
textView.setText(spannableString);
textView.setMovementMethod(new LinkMovementMethod());
- 設定画面にIntent使います
private static class GoToSettingsSpan extends ClickableSpan {
public void onClick(View view) {
view.getContext().startActivity(new Intent(android.provider.Settings.ACTION_SETTINGS));
}
}
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/clickable_span_text"
android:textColorLink="@color/go_to_settings"
android:textColorHighlight="@color/light_green"/>
Lined paper
- 紙のノートのように行にEditTextのベースラインに線を引くも出来ます
public class LinedEditText extends EditText {
// Called from constructors
private void init() {
this.paint = new Paint();
this.paint.setStyle(Paint.Style.STROKE);
this.paint.setColor(getResources().getColor(R.color.paper_line));
this.paint.setStrokeWidth(getLineHeight() / 10);
this.paint.setStrokeCap(Paint.Cap.ROUND);
}
}
Emoji
- "\u26F7"に該当する文字列の部分に自作のIconFontSpanを当てます
String text = textView.getText().toString();
SpannableString spannableString = new SpannableString(text);
IconFontSpan iconFontSpan = new IconFontSpan(textView.getContext());
Pattern pattern = Pattern.compile("\u26F7"); // skier
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
spannableString.setSpan(iconFontSpan, matcher.start(), matcher.end(), 0);
}
- カスタムフォントを用いてアイコンを設定します
private static class IconFontSpan extends MetricAffectingSpan {
private static Typeface typeface = null;
public IconFontSpan(Context context) {
if (typeface == null) {
typeface = Typeface.createFromAsset(context.getAssets(), "icomoon.ttf");
}
}
public void updateMeasureState(TextPaint textPaint) {
textPaint.setTypeface(typeface);
}
public void updateDrawState(TextPaint textPaint) {
textPaint.setTypeface(typeface);
}
}
ImageSpan from resources
- カスタムフォントではなく、該当のところにBitmapを当てることもできます
Pattern pattern = Pattern.compile(":octopus:");
Matcher matcher = pattern.matcher(text);
Bitmap octopus = null;
int size = (int) (-textView.getPaint().ascent());
while (matcher.find()) {
if (octopus == null) {
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.octopus);
octopus = Bitmap.createScaledBitmap(bitmap, size, size, true);
bitmap.recycle();
}
ImageSpan span = new ImageSpan(this, octopus, ImageSpan.ALIGN_BASELINE);
spannableString.setSpan(span, matcher.start(), matcher.end(), 0);
}
Dynamic ImageSpan (SpeedSignDrawable)
- ":speed_50:"や":speed_110:"などにPatternで正規表現を用いて、動的にCanvasで描いていきます
// :speed_50: :speed_110:
Pattern pattern = Pattern.compile(":speed_(\\d\\d\\d?):");
Pattern matcher = pattern.matcher(text);
while (matcher.find()) {
SpeedSignDrawable drawable = new SpeedSignDrawable(textView, matcher.group(1));
ImageSpan span = new ImageSpan(drawable, ImageSpan.ALIGN_BASELINE);
spannableString.setSpan(span, matcher.start(), matcher.end(), 0);
}
- カスタムフォントでもBitmapでもなく、Drawable(Paint, Canvas)で描画します
private static class SpeedSignDrawable extends Drawable {
public SpeedSignDrawable(TextView textView, String number) {
this.ascent = textView.getPaint().ascent();
this.descent = textView.getPaint().descent();
this.textSize = textView.getTextSize();
this.strokeWidth = textView.getPaint().getStrokeWidth();
this.number = number;
int size = (int) -ascent;
this.setBounds(0, 0, size, size);
}
- 黄色の丸
private void drawYellowCircle(Canvas canvas) {
int size = (int) -ascent;
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.YELLOW);
canvas.drawCircle(size/2, size/2, size/2, paint);
}
}
- 外周の赤枠
private void drawRedRing(Canvas canvas) {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.RED);
float ringWidth = size / 10;
paint.setStrokeWidth(ringWidth);
canvas.drawCircle(size/2, size/2, size/2 - ringWidth/2, paint);
}
- 数字部分
private void drawBlackNumber(Canvas canvas) {
float ratio = 0.4f;
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.BLACK);
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(textSize * ratio);
paint.setStrokeWidth(strokeWidth);
paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
int x = size / 2;
int y = (int) (size/2 - ((descent + ascent)/2) * ratio);
canvas.drawText(number, x, y, paint);
}
- 上記で定義したメソッドを呼びます
public void draw(Canvas canvas) {
drawYellowCircle(canvas);
drawRedRing(canvas);
drawBlackNumber(canvas);
}
原文:https://github.com/chiuki/advanced-textview
雑感
なんか面白そうな勉強会がありましたら、呼んで下さい。
@wasabeef_jpまで(੭ु๑•ૅω•´)੭ु⁾⁾