結論
- OnGlobalLayout()でLayout#getLineEnd(line)でline行目の末尾が何文字めなのかがわかるから,それ以降を削除してsetText()しなおせ
Multiline Ellipsizeは難しい
TextView で,複数行(例えば2行とする)入れたあと末尾を省略したいとする.
日本をリスペクトする原作者の執念が描き出すリアル・ジャパンの凄い味!
米国在住の彼らが写真やインターネット検索や甥の手紙を通じて
綿密に取材した深い理解の産物、それがネオサイタマだ。
これを
日本をリスペクトする原作者の執念が描き出すリアル・ジャパンの凄い味!
米国在住の彼らが写真やインターネット検索や甥の手紙を通じ…
のようにしたい場合だ.
ふつうに考えると
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:ellipsize="end" />
でいけると思うだろうが,これではだめ.これはTextViewの有名なバグだ.
Android 4.0以降なら
厳密に検証してないが,Android4.0以降なら
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:scrollHorizontally="true" />
でうまくいくようだ。なぜscrollHorizontallyをつけなきゃいかんのか不気味だが,4系ではとにかくこれでうまくいく。
# ちなみに,よく見かける android:singleLine="true" を使う,というのは,
# ソースを見ればわかるとおり,中で setHorizontallyScrolling(true)を呼んでいるから.
# singleLineはdeprecatedだし,そもそも複数行のEllipsingをしたいので,使わない.
だがAndroid2.2ではうまくいかない.
いろんな解決があるが……
どれもうまくいかないOSバージョンがあった.
解決
こちらの回答のとおり,TextViewに対してonGlobalLayout()のリスナをつけてやり,そのなかでレイアウト後の行数に応じて文字を削ればよろしい.
Thanks, Percuss!!
ViewTreeObserver observer = textView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
int maxLines = 2;
if(textView.getLineCount() > maxLines){
final int lineEndIndex = textView.getLayout().getLineEnd(maxLines - 1);
final String text = textView.getText().subSequence(0, lineEndIndex-3) + "...";
textView.setText(text);
}
ViewUtils.removeGlobalOnLayoutListener(textView.getViewTreeObserver(), this);
}
});
追記
2行め最後の文字がちょうど半角英数だった場合などで,"…"(1文字がAndroid推奨)に置換してしまうと逆にはみ出してしまうことがある(端末によっても違う).
以下のように気を使って,1回置換したあと再度OnGlobalLayout()をListenし,本当にぴったり入ったのかを確認するほうがよい.
ViewTreeObserver observer = textView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
private boolean mIsPassedOnce = false;
private static final int MAX_LINES = 2;
private static final String REPLACER = "…";
@Override
public void onGlobalLayout() {
if(topicView.getLineCount() > MAX_LINES){
final int lineEndIndex = textView.getLayout().getLineEnd(MAX_LINES - 1);
int reservingPosition = (mIsPassedOnce ? lineEndIndex : lineEndIndex - 1);
final CharSequence text = textView.getText();
if (mIsPassedOnce && isTooShortToReplace(text, lineEndIndex)) reservingPosition -= 1;
final String result = text.subSequence(0, reservingPosition) + REPLACER;
topicView.setText(result);
// 無限ループしないよう,mIsPassedOnceを経由済みであればリスナは落とす
// 逆にmIsPassedOnceでなければ,置換後文字列でのレイアウトを再度listenし,本当に入るかどうか様子を見る
if (mIsPassedOnce) ViewUtils.removeGlobalOnLayoutListener(textView.getViewTreeObserver(), this);
mIsPassedOnce = true;
} else {
ViewUtils.removeGlobalOnLayoutListener(textView.getViewTreeObserver(), this);
}
}
private boolean isTooShortToReplace(CharSequence charSequence, int lineEndIndex) {
String str = String.valueOf(charSequence.charAt(lineEndIndex));
if (REPLACER.equals(str)) return true;
return isAnASCII(str);
}
private boolean isAnASCII(String str){
Pattern p = Pattern.compile("^[\\p{ASCII}]$");
Matcher m = p.matcher(str);
return m.find();
}
});
レイアウトxml側では,ellipsize設定はしないこと.
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content" />
ちょっと注意
- この方法は,TextViewに入っている文字列を,見た目だけでなく実際に短くしてしまう.したがって,getText()しても省略された文字列しか返ってこない.必要なら別のメンバ変数に入れておくこと.
- このへんをうまく処理したWrappedなTextViewを作ってもいいと思う.
- ViewTreeObserver#removeGlobalOnLayoutListener(OnGlobalLayoutListener) はdeprecatedになったので,以下のような回避コードを挟むとよい.
@SuppressLint("NewApi")
@SuppressWarnings("deprecation")
public static void removeGlobalOnLayoutListener(ViewTreeObserver obs, OnGlobalLayoutListener listener) {
if (obs == null) return;
if (Build.VERSION.SDK_INT < 16) {
obs.removeGlobalOnLayoutListener(listener);
} else {
obs.removeOnGlobalLayoutListener(listener);
}
}