136
131

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TextViewの複数行末尾Ellipsize

Last updated at Posted at 2013-05-21

結論

  • 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になったので,以下のような回避コードを挟むとよい.
ViewUtils.java
    @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);
        }
    }
136
131
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
136
131

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?