15
3

More than 1 year has passed since last update.

Jetpack Composeでテキストエディター(Beta版)を作ってみた

Last updated at Posted at 2022-06-25

はじめに

Jetpack Composeで開発しているアプリのテキストエディターをBasicTextFieldで作成してみたのですが書きごごちがイマイチと感じました。

私は普段はmacOSとiOSでCraftと呼ばれるノートアプリを愛用しています。Craftのテキストエディタは書きごごちが素晴らしく不満点がないのでこれに近いテキストエディタが欲しいなと思いました。

Craftライクのテキストエディタに近づけたものが欲しい、無いのであれば自分で作ろうかと思いtext-editor-composeというライブラリをJetpack Composeで作成してみました。

できること

テキスト編集

各行のテキスト編集ができます、以下のような感じでテキストの追加&削除ができます。

EMULATOR-2022_06_25_14_57_28.gif

改行&改行削除

改行また改行削除ができます、以下のような感じでテキストの途中での改行&改行削除もできます。

AnimatedImage.gif

行数表示

行数表示のカスタマイズができます。

AnimatedImage.gif

選択位置表示

行数選択表示のカスタマイズができます。

AnimatedImage.gif

複数行の選択&コピー&削除

複数行に対しての選択・コピー・削除ができます。UIに関してはライブラリ側で用意していないので各自でカスタマイズする形になります。

AnimatedImage.gif

複数行を選択してコピーする

AnimatedImage.gif

複数行を選択して削除する

AnimatedImage.gif

まだできないこと

物理キーボードでの操作は未対応

物理キーボードでの操作にはまだ対応していないです。基本的な文字の入力はできますが上下矢印キーでの行移動やBackspaceキーでの改行削除などできない操作が色々とあります。Androidタブレット端末だと物理キーボードを接続するケースもあると思うので上下矢印キーやBackspaceキーでの操作する機能の追加を予定中です。

AnimatedImage.gif

使い方

ライブラリの使い方は簡単です。
以下の手順で依存関係を追加して、複数行コードを記載するだけで簡単に使えます。

ステップ1: JitPack リポジトリをbuild.gradleに追加する

allprojects {
	repositories {
		...
		maven { url 'https://jitpack.io' }
	}
}

ステップ2: ライブラリを依存関係に追加する

dependencies {
	implementation 'com.github.kaleidot725:text-editor-compose:0.1.0'
}

ステップ3: TextEditor&TextEditorStateを宣言する

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            SampleTheme {
                val textEditorState by rememberTextEditorState(lines = DemoText.lines())
                TextEditor(
                    textEditorState = textEditorState, // TextEditorの状態
                    onUpdatedState = { },              // TextEditorの状態更新時によばれるラムダ関数
                    modifier = Modifier.fillMaxSize()  // TextEditorのModifier 
                )
            }
        }
    }
}

ステップ4: 各行の表示内容をカスタマイズする

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            SampleTheme {
                val textEditorState by rememberTextEditorState(lines = DemoText.lines())
                TextEditor(
                    textEditorState = textEditorState,
                    onUpdatedState = { },
                    modifier = Modifier.fillMaxSize(),
                    // index : 行数を表す識別子(1行目はindexが0でN行目はindexがn-1となる)
                    // isSelected : 各行の選択状態
                    // innerTextField : 各行の必要なComposable関数
                    decorationBox = { index, isSelected, innerTextField ->
                        // 行表示の他に行数表示&選択状態表示を追加する
                        val backgroundColor = if (isSelected) Color(0x8000ff00) else Color.White           
                        Row(modifier = Modifier.background(backgroundColor)) {
                            Text(text = (index + 1).toString().padStart(3, '0'))
                            Spacer(modifier = Modifier.width(4.dp))
                            innerTextField(modifier = Modifier.fillMaxWidth())
                        }
                    }
                )
            }
        }
    }
}

ステップ5: 複数行選択メニューをカスタマイズする

```kotlin
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            SampleTheme {
                val textEditorState by rememberTextEditorState(lines = DemoText.lines())
                TextEditor(
                    textEditorState = textEditorState,
                    onUpdatedState = { },
                    modifier = Modifier.fillMaxSize(),
                    decorationBox = { index, isSelected, innerTextField ->
                        val backgroundColor = if (isSelected) Color(0x8000ff00) else Color.White
                        Row(modifier = Modifier.background(backgroundColor)) {
                            Text(text = (index + 1).toString().padStart(3, '0'))
                            Spacer(modifier = Modifier.width(4.dp))
                            innerTextField(modifier = Modifier.fillMaxWidth())
                        }
                    }
                )
            }
        }
    }
}

@Composable
private fun ColumnScope.TextEditorMenu(textEditorState: TextEditorState) {
    val context: Context = LocalContext.current
    val clipboardManager: ClipboardManager = LocalClipboardManager.current

    Row(modifier = Modifier.padding(8.dp)) {
        Text(
            text = "Enable multiple selection mode",
            modifier = Modifier
                .weight(0.9f, true)
                .align(Alignment.CenterVertically)
        )
        Switch(
            checked = textEditorState.isMultipleSelectionMode.value,
            onCheckedChange = {
                textEditorState.enableMultipleSelectionMode(
                    !textEditorState.isMultipleSelectionMode.value
                )
            }
        )
    }

    Row(modifier = Modifier.padding(8.dp)) {
        Text(
            text = "Copy selected lines",
            modifier = Modifier
                .weight(0.9f, true)
                .align(Alignment.CenterVertically)
        )
        Button(
            onClick = {
                val text = textEditorState.getSelectedText()
                textEditorState.enableMultipleSelectionMode(false)

                clipboardManager.setText(AnnotatedString(text))
                Toast.makeText(context, "Copy selected text to clipboard", Toast.LENGTH_SHORT).show()
            }
        ) {
            Text(text = "EXECUTE")
        }
    }
    
    Row(modifier = Modifier.padding(8.dp)) {
        Text(
            text = "Delete selected lines",
            modifier = Modifier
                .weight(0.9f, true)
                .align(Alignment.CenterVertically)
        )
        Button(onClick = {
            textEditorState.deleteSelectedLines()
            textEditorState.enableMultipleSelectionMode(false)
        }) {
            Text(text = "EXECUTE")
        }
    }
}

おわりに

プログラミングには欠かせないテキストエディターですが自分で実装するとなると以外と難しいところがあったり、普段は使っているけども仕様として認識できていないところがあったりと勉強になりました。

text-editor-composeと並行してノートアプリを開発しているのですが、ノートアプリの進化にあわせてtext-editor-composeも成長させていたけたらなと思っています。

text-editor-composeは以下のGitHubレポジトリで開発中なので、興味がある方はスター登録&コントリビュートしていただればと思います。

15
3
0

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
15
3