【Android】Rich Text EditorのCompose Rich Editorを試す
概要
AndroidでRich Text Editorを実装する際に、良さげなライブラリを探していた所 Compose Rich Editor
がComposeで扱いやすそうだったので試してみました。
※ Compose Multiplatform 対応との事ですが今回はAndroidのみでしか試してません。
動作環境
- M3 MacBook Air 14.4.1
- Android Studio Iguana | 2023.2.1 Patch 1
- エミュレータ: Pixel 8 Pro API Level VanillalceCream
ベースとなるプロジェクト作成
-
「New Project…」で「Empty Activity」を選択します
-
今回はプロジェクト名を「RichTextEditorExamples」として以下内容で作成します
Compose Rich Editor
- Jetpack Compose と Compose Multiplatform の両方に対応
- 一般的なテキスト スタイル機能をサポートする WYSIWYG エディタ
↑こちらでデモを触ることができます。
インストール
settings.gradle(.kts)
の dependencyResolutionManagement
> repositories
に
mavenCentral()
が設定されている事を確認し gradle/libs.versions.toml
に以下を追加します。
[versions]
richeditorCompose = "1.0.0-rc03"
[libraries]
richeditor-compose = { module = "com.mohamedrejeb.richeditor:richeditor-compose", version.ref = "richeditorCompose" }
次に app/build.gradle.kts
に以下を追加します。
dependencies {
implementation(libs.richeditor.compose)
}
簡単な実装
早速シンプルなものを試してみたいと思います。まず MainActivity.kt
に以下の様な Composable
を作成します。
@Composable
fun Editor() {
val state = rememberRichTextState()
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
) {
item {
Button(onClick = {
state.toggleSpanStyle(
SpanStyle(
fontWeight = FontWeight.Bold
)
)
}) {
Text(text = "bold", color = if (state.currentSpanStyle.fontWeight == FontWeight.Bold) Color.Red else Color.White)
}
}
item {
RichTextEditor(
modifier = Modifier.fillMaxWidth(),
state = state,
)
}
}
}
これを MainActivity
内で呼ぶように修正します。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RichTextEditorExamplesTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Editor() // ← 修正
}
}
}
}
}
早速実行してみます!
↑ちょっと分かりづらいかもですが、テキストが編集できて上部の「bold」のボタンを押すと、その後入力した文字に bold
の装飾が追加されていることが分かります。
他のスタイル実装
bold
以外にもスタイル変更ボタンを設置し、色々試してみたいと思います。
@Composable
fun Editor() {
val state = rememberRichTextState()
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
) {
item {
LazyRow(
verticalAlignment = Alignment.CenterVertically,
) {
item {
StyleButton(
state = state,
text = "left",
isSelected = state.currentParagraphStyle.textAlign == TextAlign.Left,
onClick = {
state.addParagraphStyle(
ParagraphStyle(
textAlign = TextAlign.Left,
)
)
}
)
StyleButton(
state = state,
text = "center",
isSelected = state.currentParagraphStyle.textAlign == TextAlign.Center,
onClick = {
state.addParagraphStyle(
ParagraphStyle(
textAlign = TextAlign.Center,
)
)
}
)
StyleButton(
state = state,
text = "right",
isSelected = state.currentParagraphStyle.textAlign == TextAlign.Right,
onClick = {
state.addParagraphStyle(
ParagraphStyle(
textAlign = TextAlign.Right,
)
)
}
)
StyleButton(
state = state,
text = "bold",
isSelected = state.currentSpanStyle.fontWeight == FontWeight.Bold,
onClick = {
state.toggleSpanStyle(
SpanStyle(
fontWeight = FontWeight.Bold
)
)
}
)
StyleButton(
state = state,
text = "italic",
isSelected = state.currentSpanStyle.fontStyle == FontStyle.Italic,
onClick = {
state.toggleSpanStyle(
SpanStyle(
fontStyle = FontStyle.Italic
)
)
}
)
StyleButton(
state = state,
text = "underline",
isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.Underline) == true,
onClick = {
state.toggleSpanStyle(
SpanStyle(
textDecoration = TextDecoration.Underline
)
)
}
)
StyleButton(
state = state,
text = "lineThrough",
isSelected = state.currentSpanStyle.textDecoration?.contains(TextDecoration.LineThrough) == true,
onClick = {
state.toggleSpanStyle(
SpanStyle(
textDecoration = TextDecoration.LineThrough
)
)
}
)
StyleButton(
state = state,
text = "fontSize",
isSelected = state.currentSpanStyle.fontSize == 28.sp,
onClick = {
state.toggleSpanStyle(
SpanStyle(
fontSize = 28.sp
)
)
}
)
StyleButton(
state = state,
text = "red",
isSelected = state.currentSpanStyle.color == Color.Red,
onClick = {
state.toggleSpanStyle(
SpanStyle(
color = Color.Red
)
)
}
)
StyleButton(
state = state,
text = "unorderedList",
isSelected = state.isUnorderedList,
onClick = {
state.toggleUnorderedList()
}
)
StyleButton(
state = state,
text = "orderedList",
isSelected = state.isOrderedList,
onClick = {
state.toggleOrderedList()
}
)
StyleButton(
state = state,
text = "code",
isSelected = state.isCodeSpan,
onClick = {
state.toggleCodeSpan()
}
)
}
}
}
item {
RichTextEditor(
modifier = Modifier.fillMaxWidth(),
state = state,
)
}
}
}
@Composable
fun StyleButton(state: RichTextState, text: String, isSelected: Boolean, onClick: () -> Unit) {
Button(onClick = onClick) {
Text(
text = text,
color = if (isSelected) Color.Red else Color.White
)
}
}
長くなりましたが、↑を実行すると以下のように色々試せます。
現在サポートしているスタイルはこちらで確認できます。
Import / Export
Compose Rich Editor
では HTML
と Markdown
のImport / Export に対応している様です。
早速試してみたいと思います。 先ほどのEditor
に以下を追加します。
@Composable
fun Editor() {
val state = rememberRichTextState()
val text = remember { mutableStateOf("") } // 追加
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
) {
item {
LazyRow(
verticalAlignment = Alignment.CenterVertically,
) {
item {
// ...
}
}
item {
RichTextEditor(
modifier = Modifier.fillMaxWidth(),
state = state,
)
}
// ↓新規追加
item {
Button(onClick = { text.value = state.toHtml() }) {
Text(text = "HTML")
}
}
item {
Button(onClick = { text.value = state.toMarkdown() }) {
Text(text = "Markdown")
}
}
item {
Text(text = text.value)
}
}
}
入力したテキストを HTML
と Markdown
で出力するボタンを設けて、出力された内容を表示させています。実行してみると↓の様に表示されました。
Markdown
では left
, center
, right
などの text-align
や fontSize
, fontColor
, underline
はhtmlで補完して出力などはしてくれないみたいです。
まとめ
見出しや画像、チェックボックスなど、あったら嬉しい機能はありますが、シンプルなものであれば使えそうかなという感想です。また link
や codeブロック
のちょっとしたスタイル調整はできそうですが、その他の細かなスタイル調整はできなさそうでした。
(※ 画像、チェックボックスははドキュメントに将来実装予定と明記されてます)
Discussion