TextViewの複数行末尾Ellipsize

# 結論

* OnGlobalLayout()でLayout#getLineEnd(line)でline行目の末尾が何文字めなのかがわかるから，それ以降を削除してsetText()しなおせ

# Multiline Ellipsizeは難しい
TextView で，複数行（例えば2行とする）入れたあと末尾を省略したいとする．

> 日本をリスペクトする原作者の執念が描き出すリアル・ジャパンの凄い味！
> 米国在住の彼らが写真やインターネット検索や甥の手紙を通じて
> 綿密に取材した深い理解の産物、それがネオサイタマだ。

これを

> 日本をリスペクトする原作者の執念が描き出すリアル・ジャパンの凄い味！
> 米国在住の彼らが写真やインターネット検索や甥の手紙を通じ…

のようにしたい場合だ．
ふつうに考えると

```xml
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:maxLines="2"
            android:ellipsize="end" />
```

でいけると思うだろうが，これではだめ．これはTextViewの有名なバグだ．

* [参考1](http://code.google.com/p/android/issues/list?can=2&q=Ellipsize&colspec=ID+Type+Status+Owner+Summary+Stars&cells=tiles) 

# Android 4.0以降なら

厳密に検証してないが，Android4.0以降なら

```xml
        <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バージョンがあった．

* [参考2](https://github.com/triposo/barone/blob/master/src/com/triposo/barone/EllipsizingTextView.java) 
* [参考3](http://code.google.com/p/android-textview-multiline-ellipse/)
* [参考4](http://stackoverflow.com/a/14175311/2338047)


# 解決

* [参考5](http://stackoverflow.com/a/11247803/2338047)

こちらの回答のとおり，TextViewに対してonGlobalLayout()のリスナをつけてやり，そのなかでレイアウト後の行数に応じて文字を削ればよろしい．
Thanks, [Percuss](http://stackoverflow.com/users/971333/percuss)!!

```java
    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し，本当にぴったり入ったのかを確認するほうがよい．

```java
    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設定はしないこと．

```xml
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
```

## ちょっと注意

* この方法は，TextViewに入っている文字列を，見た目だけでなく実際に短くしてしまう．したがって，getText()しても省略された文字列しか返ってこない．必要なら別のメンバ変数に入れておくこと．
* このへんをうまく処理したWrappedなTextViewを作ってもいいと思う．
* ViewTreeObserver#removeGlobalOnLayoutListener(OnGlobalLayoutListener) はdeprecatedになったので，以下のような回避コードを挟むとよい．

```java: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);
        }
    }
```