How to disable copy/paste/cut in a TextField Jetpack Compose? - android

I'm trying to find a simple solution on how to disable copy/paste/cut in a TextField. I did come across a couple of question but no answer.

Create an empty toolbar:
object EmptyTextToolbar: TextToolbar {
override val status: TextToolbarStatus = TextToolbarStatus.Hidden
override fun hide() { }
override fun showMenu(
rect: Rect,
onCopyRequested: (() -> Unit)?,
onPasteRequested: (() -> Unit)?,
onCutRequested: (() -> Unit)?,
onSelectAllRequested: (() -> Unit)?,
) {
}
}
Then you can provide it using LocalTextToolbar.
Most probably you also don't need text selection in this case, here's how you can disable it too:
var textValue by remember { mutableStateOf(TextFieldValue("")) }
CompositionLocalProvider(
LocalTextToolbar provides EmptyTextToolbar
) {
TextField(
value = textValue,
onValueChange = { newValue ->
textValue = if (newValue.selection.length > 0) {
newValue.copy(selection = textValue.selection)
} else {
newValue
}
}
)
}

For now, you could just add a dummy Transparent Composable on TOP of the TextField and hook up it's click listener with the TextField's focusRequestor.
val tffr = remember { FocusRequestor() } // TextField Focus-Requestor
Box{
TextField(
value = value,
onValueChange = { value = it }
modifier = Modifier.focusRequestor(tffr).focusable()
)
Surface(
modifier = Modifier.clickable { tffr.requestFocus() }.alpha(0)
)
}
This way, when users try to click on the TextField, the tap would be intercepted by our dummy Composable, which would transfer the focus, effectively, to the TextField.
This means that they will technically be allowed to tap on the TextField and type in it, but the long press won't be registered by the field for them.
You could even add a Long-Click-Listener to the Surface using the combinedClick modifier, and display a toast to they users stating that the copy/paste actions are disabled, if that is something you'd require, that is.

Try this answer it's disable cut, copy in textfield
etSearchData.customSelectionActionModeCallback = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return false
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
return false
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
return false
}
override fun onDestroyActionMode(mode: ActionMode?) {
}
}
where etSearchData is your textField ID

Related

Passing State value, or State, as Composable function parameter

In a Composable function, I can pass as parameter the State, or the value of the State. Any reason for preferring to pass the value of the State, instead of the State?
In both cases, the composable is stateless, so why should I distinguish both cases?
It's possible to pass state's value. For example:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val isLoading = mutableStateOf(false)
val onClickAtButton = {
lifecycleScope.launch(Dispatchers.Main) {
isLoading.value = true
withContext(Dispatchers.IO) {
//Do some heavy operation live REST call
}
isLoading.value = false
}
}
setContent {
MyComposable(isLoading.value, onClickAtButton)
}
}
}
#Composable
fun MyComposable(
isLoading: Boolean = false,
onClickAtButton: () -> Unit = {}
){
Box(modifier = Modifier.fillMaxSize(){
Button(onClick = onClickAtButton)
if(isLoading){
CircularProgressIndicator()
}
}
}
Hope it helps somebody.
There is a slight difference between passing State or just the value of a State regarding recomposition.
Let's start with passing State:
#Composable
fun Example1(text: State<String>) {
SideEffect { Log.d("Example", "Example1 recomposition") }
Example2(text)
}
#Composable
fun Example2(text: State<String>) {
SideEffect { Log.d("Example", "Example2 recomposition") }
Text(text.value)
}
#Composable
fun Screen() {
val text = remember { mutableStateOf("hello") } }
Example1(text)
Button(
onClick = { text.value = "world" }
) {
Text("Click me")
}
}
On first start you will see the log output
Example1 recomposition
Example2 recomposition
However when you click the button, you will only see an additional
Example2 recomposition
Because you're passing down State and only Example2 is reading the state, Example1 does not need to be recomposed.
Let's change the parameters to a plain type:
#Composable
fun Example1(text: String) {
SideEffect { Log.d("Example", "Example1 recomposition") }
Example2(text)
}
#Composable
fun Example2(text: String) {
SideEffect { Log.d("Example", "Example2 recomposition") }
Text(text)
}
#Composable
fun Screen() {
val text = remember { mutableStateOf("hello") } }
Example1(text.value)
Button(
onClick = { text.value = "world" }
) {
Text("Click me")
}
}
When you click the button now, you will see two additional lines in the log output
Example1 recomposition
Example2 recomposition
Since text is now a plain type of the function signatures of both composables, both need to be recomposed when the value changes.
However always passing down State can become quite cumbersome. Compose is quite good at detecting what needs to be recomposed so this should be considered a micro optimization. I just wanted to point out that there is a slight difference which every developer using Compose should know about.

OnKeyEvent without focus in Jetpack Compose

I am creating an app that makes use of a physical button on the device.
This button will have a different functionality depending on the screen that is active.
With Activities what I would do would be to have an Activity for each screen and in each one I would override the onKeyDown function. How would I do this with a single activity that navigates between different Jetpack Compose screens?
According to the Android documentation the correct way would be something like this...
Box(modifier = Modifier
.onKeyEvent {
Log.e("Pressed", it.nativeKeyEvent.keyCode.toString())
true
}
.focusable()
.fillMaxSize()
.background(Color.Gray)
) {
// All screen components
}
But this only works when one of the elements on the screen is focused and what I require is that it always works or not, is there a way to achieve this?
One option is to keep the focus on the view so that the Modifier.onKeyEvent always works. Note that it may conflict with other focusable views, like TextField, so all these views should be children(at any level) of this always-focusable view.
val focusRequester = remember { FocusRequester() }
var hasFocus by remember { mutableStateOf(false) }
Box(
Modifier
.focusRequester(focusRequester)
.onFocusChanged {
hasFocus = it.hasFocus
}
.focusable()
.onKeyEvent {
TODO()
}
) {
}
if (!hasFocus) {
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
Another option is to create compositional local handlers and pass events from your activity:
class MainActivity : AppCompatActivity() {
private val keyEventHandlers = mutableListOf<KeyEventHandler>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CompositionLocalProvider(LocalKeyEventHandlers provides keyEventHandlers) {
// your app
}
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
return keyEventHandlers.reversed().any { it(keyCode, event) } || super.onKeyDown(keyCode, event)
}
}
val LocalKeyEventHandlers = compositionLocalOf<MutableList<KeyEventHandler>> {
error("LocalKeyEventHandlers is not provided")
}
typealias KeyEventHandler = (Int, KeyEvent) -> Boolean
#Composable
fun ListenKeyEvents(handler: KeyEventHandler) {
val handlerState = rememberUpdatedState(handler)
val eventHandlers = LocalKeyEventHandlers.current
DisposableEffect(handlerState) {
val localHandler: KeyEventHandler = {
handlerState.value(it)
}
eventHandlers.add(localHandler)
onDispose {
eventHandlers.remove(localHandler)
}
}
}
Usage:
ListenKeyEvents { code, event ->
TODO()
}

How can I pass a delegated mutableVariable to a Compose function?

I'm trying to pass the MutableState variable over another function. The below is all good. But I don't like the myMutableState.value
#Composable
fun Func() {
val myMutableState = remember { mutableStateOf(true) }
Column {
AnotherFunc(mutableValue = myMutableState)
Text(if (myMutableState.value) "Stop" else "Start")
}
}
#Composable
fun AnotherFunc(mutableValue: MutableState<Boolean>) {
}
So I decided to use val myMutableState by remember { mutableStateOf(true) }, as shown below. I no longer need to use myMutableState.value as shown below.
Unfortunately, the below won't compile. This is because I cannot pass it over the function AnotherFunc(mutableValue = myMutableState)
#Composable
fun Func() {
val myMutableState by remember { mutableStateOf(true) }
Column {
AnotherFunc(mutableValue = myMutableState) // Error here
Text(if (myMutableState) "Stop" else "Start")
}
}
#Composable
fun AnotherFunc(mutableValue: MutableState<Boolean>) {
}
How can I still use by and still able to pass the MutableState over the function?
=-0987our composable function should just take a boolean:
#Composable
fun AnotherFunc(mutableValue: Boolean) {
}
Not sure why your composable function (AnotherFun) needs to have a mutable state. The calling function (Fun) will automatically recompose when the value changes, triggering recomposition of AnotherFun.

How to disable simultaneous clicks on multiple items in Jetpack Compose List / Column / Row (out of the box debounce?)

I have implemented a column of buttons in jetpack compose. We realized it is possible to click multiple items at once (with multiple fingers for example), and we would like to disable this feature.
Is there an out of the box way to disable multiple simultaneous clicks on children composables by using a parent column modifier?
Here is an example of the current state of my ui, notice there are two selected items and two unselected items.
Here is some code of how it is implemented (stripped down)
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(nestedScrollParams.childScrollState),
) {
viewDataList.forEachIndexed { index, viewData ->
Row(modifier = modifier.fillMaxWidth()
.height(dimensionResource(id = 48.dp)
.background(colorResource(id = R.color.large_button_background))
.clickable { onClick(viewData) },
verticalAlignment = Alignment.CenterVertically
) {
//Internal composables, etc
}
}
Check this solution. It has similar behavior to splitMotionEvents="false" flag. Use this extension with your Column modifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import kotlinx.coroutines.coroutineScope
fun Modifier.disableSplitMotionEvents() =
pointerInput(Unit) {
coroutineScope {
var currentId: Long = -1L
awaitPointerEventScope {
while (true) {
awaitPointerEvent(PointerEventPass.Initial).changes.forEach { pointerInfo ->
when {
pointerInfo.pressed && currentId == -1L -> currentId = pointerInfo.id.value
pointerInfo.pressed.not() && currentId == pointerInfo.id.value -> currentId = -1
pointerInfo.id.value != currentId && currentId != -1L -> pointerInfo.consume()
else -> Unit
}
}
}
}
}
}
Here are four solutions:
Click Debounce (ViewModel)r
For this, you need to use a viewmodel. The viewmodel handles the click event. You should pass in some id (or data) that identifies the item being clicked. In your example, you could pass an id that you assign to each item (such as a button id):
// IMPORTANT: Make sure to import kotlinx.coroutines.flow.collect
class MyViewModel : ViewModel() {
val debounceState = MutableStateFlow<String?>(null)
init {
viewModelScope.launch {
debounceState
.debounce(300)
.collect { buttonId ->
if (buttonId != null) {
when (buttonId) {
ButtonIds.Support -> displaySupport()
ButtonIds.About -> displayAbout()
ButtonIds.TermsAndService -> displayTermsAndService()
ButtonIds.Privacy -> displayPrivacy()
}
}
}
}
}
fun onItemClick(buttonId: String) {
debounceState.value = buttonId
}
}
object ButtonIds {
const val Support = "support"
const val About = "about"
const val TermsAndService = "termsAndService"
const val Privacy = "privacy"
}
The debouncer ignores any clicks that come in within 500 milliseconds of the last one received. I've tested this and it works. You'll never be able to click more than one item at a time. Although you can touch two at a time and both will be highlighted, only the first one you touch will generate the click handler.
Click Debouncer (Modifier)
This is another take on the click debouncer but is designed to be used as a Modifier. This is probably the one you will want to use the most. Most apps will make the use of scrolling lists that let you tap on a list item. If you quickly tap on an item multiple times, the code in the clickable modifier will execute multiple times. This can be a nuisance. While users normally won't tap multiple times, I've seen even accidental double clicks trigger the clickable twice. Since you want to avoid this throughout your app on not just lists but buttons as well, you probably should use a custom modifier that lets you fix this issue without having to resort to the viewmodel approach shown above.
Create a custom modifier. I've named it onClick:
fun Modifier.onClick(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit
) = composed(
inspectorInfo = debugInspectorInfo {
name = "clickable"
properties["enabled"] = enabled
properties["onClickLabel"] = onClickLabel
properties["role"] = role
properties["onClick"] = onClick
}
) {
Modifier.clickable(
enabled = enabled,
onClickLabel = onClickLabel,
onClick = {
App.debounceClicks {
onClick.invoke()
}
},
role = role,
indication = LocalIndication.current,
interactionSource = remember { MutableInteractionSource() }
)
}
You'll notice that in the code above, I'm using App.debounceClicks. This of course doesn't exist in your app. You need to create this function somewhere in your app where it is globally accessible. This could be a singleton object. In my code, I use a class that inherits from Application, as this is what gets instantiated when the app starts:
class App : Application() {
override fun onCreate() {
super.onCreate()
}
companion object {
private val debounceState = MutableStateFlow { }
init {
GlobalScope.launch(Dispatchers.Main) {
// IMPORTANT: Make sure to import kotlinx.coroutines.flow.collect
debounceState
.debounce(300)
.collect { onClick ->
onClick.invoke()
}
}
}
fun debounceClicks(onClick: () -> Unit) {
debounceState.value = onClick
}
}
}
Don't forget to include the name of your class in your AndroidManifest:
<application
android:name=".App"
Now instead of using clickable, use onClick instead:
Text("Do Something", modifier = Modifier.onClick { })
Globally disable multi-touch
In your main activity, override dispatchTouchEvent:
class MainActivity : AppCompatActivity() {
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
return ev?.getPointerCount() == 1 && super.dispatchTouchEvent(ev)
}
}
This disables multi-touch globally. If your app has a Google Maps, you will want to add some code to to dispatchTouchEvent to make sure it remains enabled when the screen showing the map is visible. Users will use two fingers to zoom on a map and that requires multi-touch enabled.
State Managed Click Handler
Use a single click event handler that stores the state of which item is clicked. When the first item calls the click, it sets the state to indicate that the click handler is "in-use". If a second item attempts to call the click handler and "in-use" is set to true, it just returns without performing the handler's code. This is essentially the equivalent of a synchronous handler but instead of blocking, any further calls just get ignored.
The most simple approach that I found for this issue is to save the click state for each Item on the list, and update the state to 'true' if an item is clicked.
NOTE: Using this approach works properly only in a use-case where the list will be re-composed after the click handling; for example navigating to another Screen when the item click is performed.
Otherwise if you stay in the same Composable and try to click another item, the second click will be ignored and so on.
for example:
#Composable
fun MyList() {
// Save the click state in a MutableState
val isClicked = remember {
mutableStateOf(false)
}
LazyColumn {
items(10) {
ListItem(index = "$it", state = isClicked) {
// Handle the click
}
}
}
}
ListItem Composable:
#Composable
fun ListItem(
index: String,
state: MutableState<Boolean>,
onClick: () -> Unit
) {
Text(
text = "Item $index",
modifier = Modifier
.clickable {
// If the state is true, escape the function
if (state.value)
return#clickable
// else, call onClick block
onClick()
state.value = true
}
)
}
Trying to turn off multi-touch, or adding single click to the modifier, is not flexible enough. I borrowed the idea from #Johann‘s code. Instead of disabling at the app level, I can call it only when I need to disable it.
Here is an Alternative solution:
class ClickHelper private constructor() {
private val now: Long
get() = System.currentTimeMillis()
private var lastEventTimeMs: Long = 0
fun clickOnce(event: () -> Unit) {
if (now - lastEventTimeMs >= 300L) {
event.invoke()
}
lastEventTimeMs = now
}
companion object {
#Volatile
private var instance: ClickHelper? = null
fun getInstance() =
instance ?: synchronized(this) {
instance ?: ClickHelper().also { instance = it }
}
}
}
then you can use it anywhere you want:
Button(onClick = { ClickHelper.getInstance().clickOnce {
// Handle the click
} } ) { }
or:
Text(modifier = Modifier.clickable { ClickHelper.getInstance().clickOnce {
// Handle the click
} } ) { }
fun singleClick(onClick: () -> Unit): () -> Unit {
var latest: Long = 0
return {
val now = System.currentTimeMillis()
if (now - latest >= 300) {
onClick()
latest = now
}
}
}
Then you can use
Button(onClick = singleClick {
// TODO
})
Here is my solution.
It's based on https://stackoverflow.com/a/69914674/7011814
by I don't use GlobalScope (here is an explanation why) and I don't use MutableStateFlow as well (because its combination with GlobalScope may cause a potential memory leak).
Here is a head stone of the solution:
#OptIn(FlowPreview::class)
#Composable
fun <T>multipleEventsCutter(
content: #Composable (MultipleEventsCutterManager) -> T
) : T {
val debounceState = remember {
MutableSharedFlow<() -> Unit>(
replay = 0,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
}
val result = content(
object : MultipleEventsCutterManager {
override fun processEvent(event: () -> Unit) {
debounceState.tryEmit(event)
}
}
)
LaunchedEffect(true) {
debounceState
.debounce(CLICK_COLLAPSING_INTERVAL)
.collect { onClick ->
onClick.invoke()
}
}
return result
}
#OptIn(FlowPreview::class)
#Composable
fun MultipleEventsCutter(
content: #Composable (MultipleEventsCutterManager) -> Unit
) {
multipleEventsCutter(content)
}
The first function can be used as a wrapper around your code like this:
MultipleEventsCutter { multipleEventsCutterManager ->
Button(
onClick = { multipleClicksCutter.processEvent(onClick) },
...
) {
...
}
}
And you can use the second one to create your own modifier, like next one:
fun Modifier.clickableSingle(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit
) = composed(
inspectorInfo = debugInspectorInfo {
name = "clickable"
properties["enabled"] = enabled
properties["onClickLabel"] = onClickLabel
properties["role"] = role
properties["onClick"] = onClick
}
) {
multipleEventsCutter { manager ->
Modifier.clickable(
enabled = enabled,
onClickLabel = onClickLabel,
onClick = { manager.processEvent { onClick() } },
role = role,
indication = LocalIndication.current,
interactionSource = remember { MutableInteractionSource() }
)
}
}
Just add two lines in your styles. This will disable multitouch in whole application:
<style name="AppTheme" parent="...">
...
<item name="android:windowEnableSplitTouch">false</item>
<item name="android:splitMotionEvents">false</item>
</style>

How to switch between light and dark theme dynamically in app using composables

How would you dynamically switch between theme's color palette with a press of a button inside the app
This is what I am doing so far, but only works when I switch the Android Theme to dark or light mode
AppTheme.Kt
#Model
object ThemeState {
var isLight: Boolean = true
}
#Composable
fun MyAppTheme(
children: #Composable() () -> Unit
) {
MaterialTheme(colors = if (ThemeState.isLight) themeColorsLight else themColorDark) {
children()
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme(children = {
Surface {
Greetings(name = "Android")
}
})
}
}
}
#Composable
fun Greetings(name: String) {
Column(modifier = Modifier.fillMaxHeight()) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Hello $name", modifier = Modifier.padding(24.dp),
style = MaterialTheme.typography.h1
)
}
Button(onClick = { ThemeState.isLight = !ThemeState.isLight }) {
Text(text = "Change Theme IsLight:${ThemeState.isLight}")
}
}
}
At the moment I don't have any Idea why your code not works, I'll update this answer when I find out.
but instead of using if else for colors parameter use it for the whole MaterialTheme like this and it will work:
#Composable
fun MyAppTheme(
children: #Composable() () -> Unit
) {
if (ThemeState.isLight) {
MaterialTheme(colors = themeColorsLight) {
children()
}
} else {
MaterialTheme(colors = themColorDark) {
children()
}
}
}
Update:
seems that it's bug in Jetpack Compose dev11, I tried in dev12 and it works there.
NOTE 1:
#Model has been deprecated in dev 12
change your ThemeState to
object ThemeState {
var isLight by mutableStateOf(true)
}
more information: https://android-review.googlesource.com/c/platform/frameworks/support/+/1311293
NOTE 2
There are some problems with auto Import in recent versions of AndroidStudio
If the Idea throws error: Type 'MutableState<TypeVariable(T)>' has no method 'getValue(ThemeState, KProperty<*>)' and thus it cannot serve as a delegate
Import getValue and SetValue manually.
import androidx.compose.getValue
import androidx.compose.setValue
Since 0.1.0-dev16 use these imports:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
Use AppCompatDelegate class
Step 1: Define a state that will point to Light mode initially.
object ThemeState {
var darkModeState : MutableState<Boolean> = mutableStateOf(false)
}
Note : Whenever this state will be changed, all the methods reading this state value will also be called.
Step 2 : Define a variable for reading state
val isDark = ThemeState.darkModeState.value
Step 3 : Now change Theme mode from Dark to Light and vice versa as follows
Button(onClick = {
val theme = when(isDark){
true -> AppCompatDelegate.MODE_NIGHT_NO
false -> AppCompatDelegate.MODE_NIGHT_YES
}
AppCompatDelegate.setDefaultNightMode(theme)
ThemeState.darkModeState.value = !isDark
}) {
Text(text = "Theme Toggle Button")
}
As you can see here, I'm changing app theme every time Theme Toggle Button is clicked.

Categories

Resources