Skip to content

Instantly share code, notes, and snippets.

@stevdza-san
Created October 14, 2023 14:08
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stevdza-san/671a9c9123c88331c77b1d71f7476df2 to your computer and use it in GitHub Desktop.
Save stevdza-san/671a9c9123c88331c77b1d71f7476df2 to your computer and use it in GitHub Desktop.
Demo project of a Compose-Rich-Editor library
import android.annotation.SuppressLint
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddLink
import androidx.compose.material.icons.filled.FormatAlignCenter
import androidx.compose.material.icons.filled.FormatAlignLeft
import androidx.compose.material.icons.filled.FormatAlignRight
import androidx.compose.material.icons.filled.FormatBold
import androidx.compose.material.icons.filled.FormatColorText
import androidx.compose.material.icons.filled.FormatItalic
import androidx.compose.material.icons.filled.FormatSize
import androidx.compose.material.icons.filled.FormatUnderlined
import androidx.compose.material.icons.filled.Save
import androidx.compose.material.icons.filled.Title
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import com.mohamedrejeb.richeditor.model.RichTextState
import com.mohamedrejeb.richeditor.model.rememberRichTextState
import com.mohamedrejeb.richeditor.ui.material3.RichTextEditor
import com.stevdza.san.comptest.components.LinkDialog
/**
Be sure that you have those two dependencies:
// Rich Text Editor
implementation("com.mohamedrejeb.richeditor:richeditor-compose:1.0.0-beta03")
// Extension Icons
implementation("androidx.compose.material:material-icons-extended:1.5.3")
*/
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun MainScreen() {
val state = rememberRichTextState()
val titleSize = MaterialTheme.typography.displaySmall.fontSize
val subtitleSize = MaterialTheme.typography.titleLarge.fontSize
Scaffold {
Column(
modifier = Modifier
.fillMaxSize()
.padding(all = 20.dp)
.padding(bottom = it.calculateBottomPadding())
.padding(top = it.calculateTopPadding()),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
EditorControls(
modifier = Modifier.weight(2f),
state = state,
onBoldClick = {
state.toggleSpanStyle(SpanStyle(fontWeight = FontWeight.Bold))
},
onItalicClick = {
state.toggleSpanStyle(SpanStyle(fontStyle = FontStyle.Italic))
},
onUnderlineClick = {
state.toggleSpanStyle(SpanStyle(textDecoration = TextDecoration.Underline))
},
onTitleClick = {
state.toggleSpanStyle(SpanStyle(fontSize = titleSize))
},
onSubtitleClick = {
state.toggleSpanStyle(SpanStyle(fontSize = subtitleSize))
},
onTextColorClick = {
state.toggleSpanStyle(SpanStyle(color = Color.Red))
},
onStartAlignClick = {
state.toggleParagraphStyle(ParagraphStyle(textAlign = TextAlign.Start))
},
onEndAlignClick = {
state.toggleParagraphStyle(ParagraphStyle(textAlign = TextAlign.End))
},
onCenterAlignClick = {
state.toggleParagraphStyle(ParagraphStyle(textAlign = TextAlign.Center))
},
onExportClick = {
Log.d("Editor", state.toHtml())
}
)
RichTextEditor(
modifier = Modifier
.fillMaxWidth()
.weight(8f),
state = state,
)
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun EditorControls(
modifier: Modifier = Modifier,
state: RichTextState,
onBoldClick: () -> Unit,
onItalicClick: () -> Unit,
onUnderlineClick: () -> Unit,
onTitleClick: () -> Unit,
onSubtitleClick: () -> Unit,
onTextColorClick: () -> Unit,
onStartAlignClick: () -> Unit,
onEndAlignClick: () -> Unit,
onCenterAlignClick: () -> Unit,
onExportClick: () -> Unit,
) {
var boldSelected by rememberSaveable { mutableStateOf(false) }
var italicSelected by rememberSaveable { mutableStateOf(false) }
var underlineSelected by rememberSaveable { mutableStateOf(false) }
var titleSelected by rememberSaveable { mutableStateOf(false) }
var subtitleSelected by rememberSaveable { mutableStateOf(false) }
var textColorSelected by rememberSaveable { mutableStateOf(false) }
var linkSelected by rememberSaveable { mutableStateOf(false) }
var alignmentSelected by rememberSaveable { mutableIntStateOf(0) }
var showLinkDialog by remember { mutableStateOf(false) }
AnimatedVisibility(visible = showLinkDialog) {
LinkDialog(
onDismissRequest = {
showLinkDialog = false
linkSelected = false
},
onConfirmation = { linkText, link ->
state.addLink(
text = linkText,
url = link
)
showLinkDialog = false
linkSelected = false
}
)
}
FlowRow(
modifier = modifier
.fillMaxWidth()
.padding(all = 10.dp)
.padding(bottom = 24.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
ControlWrapper(
selected = boldSelected,
onChangeClick = { boldSelected = it },
onClick = onBoldClick
) {
Icon(
imageVector = Icons.Default.FormatBold,
contentDescription = "Bold Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = italicSelected,
onChangeClick = { italicSelected = it },
onClick = onItalicClick
) {
Icon(
imageVector = Icons.Default.FormatItalic,
contentDescription = "Italic Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = underlineSelected,
onChangeClick = { underlineSelected = it },
onClick = onUnderlineClick
) {
Icon(
imageVector = Icons.Default.FormatUnderlined,
contentDescription = "Underline Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = titleSelected,
onChangeClick = { titleSelected = it },
onClick = onTitleClick
) {
Icon(
imageVector = Icons.Default.Title,
contentDescription = "Title Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = subtitleSelected,
onChangeClick = { subtitleSelected = it },
onClick = onSubtitleClick
) {
Icon(
imageVector = Icons.Default.FormatSize,
contentDescription = "Subtitle Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = textColorSelected,
onChangeClick = { textColorSelected = it },
onClick = onTextColorClick
) {
Icon(
imageVector = Icons.Default.FormatColorText,
contentDescription = "Text Color Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = linkSelected,
onChangeClick = { linkSelected = it },
onClick = { showLinkDialog = true }
) {
Icon(
imageVector = Icons.Default.AddLink,
contentDescription = "Link Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = alignmentSelected == 0,
onChangeClick = { alignmentSelected = 0 },
onClick = onStartAlignClick
) {
Icon(
imageVector = Icons.Default.FormatAlignLeft,
contentDescription = "Start Align Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = alignmentSelected == 1,
onChangeClick = { alignmentSelected = 1 },
onClick = onCenterAlignClick
) {
Icon(
imageVector = Icons.Default.FormatAlignCenter,
contentDescription = "Center Align Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = alignmentSelected == 2,
onChangeClick = { alignmentSelected = 2 },
onClick = onEndAlignClick
) {
Icon(
imageVector = Icons.Default.FormatAlignRight,
contentDescription = "End Align Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
ControlWrapper(
selected = true,
selectedColor = MaterialTheme.colorScheme.tertiary,
onChangeClick = { },
onClick = onExportClick
) {
Icon(
imageVector = Icons.Default.Save,
contentDescription = "Export Control",
tint = MaterialTheme.colorScheme.onPrimary
)
}
}
}
@Composable
fun ControlWrapper(
selected: Boolean,
selectedColor: Color = MaterialTheme.colorScheme.primary,
unselectedColor: Color = MaterialTheme.colorScheme.inversePrimary,
onChangeClick: (Boolean) -> Unit,
onClick: () -> Unit,
content: @Composable () -> Unit
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(size = 6.dp))
.clickable {
onClick()
onChangeClick(!selected)
}
.background(
if (selected) selectedColor
else unselectedColor
)
.border(
width = 1.dp,
color = Color.LightGray,
shape = RoundedCornerShape(size = 6.dp)
)
.padding(all = 8.dp),
contentAlignment = Alignment.Center
) {
content()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment