テキストエディタに行数を表示するEditTextカスタム

AdventCalendar Android その2 の12日目の記事です

---

# テキストエディタを作るには
Androidでテキスト入力を実装するには`EditText`を使うことになるかと思います。
テキストエディタを作るには`EditText`をマルチラインにし、場合によっては`SpannableStringBuilder`を駆使してリッチな機能を盛り込んでいきます。

今回は機能面ではなく、このようにエディタの行数をどうやって表示するかを解説します。
![Screenshot_1513043110.png](https://qiita-image-store.s3.amazonaws.com/0/6046/dd6ff548-e590-f92b-44b2-18a001f5bbd7.png)

# 実現方法
`onDraw`をoverrideします

```java:CustomEdiText.java
package jp.advent2017.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.v7.widget.AppCompatEditText;
import android.text.Layout;
import android.util.AttributeSet;

/**
 * Created by yuta.ida on 2017/09/01.
 */

public class CustomEdiText extends AppCompatEditText {
    private static final String LF = "\n";

    public CustomEdiText(Context context) {
        super(context);
    }

    public CustomEdiText(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomEdiText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        String text = this.getText().toString();
        int displayLineCount = this.getLineCount();
        int lineNo = 1;
        int start = 0;
        Layout layout = this.getLayout();
        for (int i = 0; i < displayLineCount; i++) {

            if (isNewLine(text, start)) {
                drawLineNumber(canvas, i, lineNo);
                lineNo++;
            }

            int end = layout.getLineEnd(i);
            if (hasLineBreak(text, start, end)) {
                String lineString = text.substring(start, end);
                drawLineBreak(canvas, i, lineString);
            }
            start = end;
        }
        super.onDraw(canvas);
    }

    // ①その行の直前に改行が含まれているか
    private boolean isNewLine(String text, int charStart) {
        if (charStart == 0) {
            return true;
        }
        String lineString = text.substring(charStart - 1, charStart);
        return lineString.indexOf(LF) >= 0;
    }

    private void drawLineNumber(Canvas canvas, Integer number, Integer lineNo) {
        Layout layout = this.getLayout();
        Paint textPaint = this.getPaint();

        // ②行のY座標を取得
        int top = this.getPaddingTop() + layout.getLineBottom(number);

        // ③行の先頭にテキストを描画
        canvas.drawText(lineNo.toString(), 0, top, textPaint);
    }

}

```

## 詳説
### getLineCount
`getLineCount`はビューの*見た目上*の行数を取得します。
折り返しによる行数も含まれるので注意が必要です。

### 行範囲を取得
`layout.getLineEnd`(`layout.getLineStart`でも)で行範囲を取得し、直前に改行コードが含まれている場合は新しい行として検知します。

### 描画する
`layout.getLineBottom(lineNumber)`で行の位置を取得し、`canvas.drawText`を利用し行番号を描画します。

# 応用
このように行末位置を取得することで改行文字を表示することも可能です。

```java
    @Override
    protected void onDraw(Canvas canvas) {
        ...
        for (int i = 0; i < displayLineCount; i++) {
            ...
            int end = layout.getLineEnd(i);
            if (hasLineBreak(text, start, end)) {
                String lineString = text.substring(start, end);
                drawLineBreak(canvas, i, lineString);
            }
        }
        ...
    }
    private boolean hasLineBreak(String text, int charStart, int charEnd) {
        String lineString = text.substring(charStart, charEnd);
        return lineString.indexOf(LF) >= 0;
    }
    private void drawLineBreak(Canvas canvas, Integer lineNumber, String lineString) {
        Layout layout = this.getLayout();
        Paint textPaint = this.getPaint();
        float measure = textPaint.measureText(lineString, 0, lineString.length());
        measure += this.getPaddingLeft();
        int top = this.getPaddingTop() + layout.getLineBottom(lineNumber);
        canvas.drawText("⏎", measure, top, textPaint);
    }
```
![Screenshot_1513043190.png](https://qiita-image-store.s3.amazonaws.com/0/6046/7475acd7-27b0-98a7-7a01-807beeb02ffa.png)