CameraX: front camera video recording failed - android-jetpack-compose

I try video recording on front camera (selfie). when it comes to implementation, it causes no recording response instead of start recording. When I press change camera button back, video recording does not work.
Please help me review the preview setup:
Below is part of my code in jetpack compose:
fun StoryCaptureScreen(
modifier: Modifier,
photoPath: Uri,
imageLoader: ImageLoader,
isAudioPermissionGranted: Boolean,
isLabelLoaded: Boolean,
isShowAlbumPermissionScreen: Boolean,
isDeviceSupportVideo: Boolean,
....
Surface(
modifier = modifier,
color = MaterialTheme.colors.background
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val standardIconSize = 36.dp
val cameraPermissionState = rememberMultiplePermissionsState(
permissions = listOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
)
)
val necessaryCameraPermissionState = rememberMultiplePermissionsState(
permissions = listOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
)
)
val tempUploadDir by lazy {
FileUtil.createFolder(
FOLDER_TEMP_UPLOAD_FOLDER,
FileManager.getExternalFilesDir(context).absolutePath
)
}
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp.dp
val screenWidth = configuration.screenWidthDp.dp
var second by remember { mutableStateOf(0.toLong()) }
var currentNanoSecond by remember { mutableStateOf(0.toLong()) }
val maxSecond by remember { mutableStateOf((1000000000.0 * maximumRecordingSecond).toLong()) } // ktlint-disable max-line-length
var recording: Recording? = remember { null }
val recordingStarted: MutableState<Boolean> = remember { mutableStateOf(false) }
val cameraSelector: MutableState<CameraSelector> = remember {
mutableStateOf(CameraSelector.DEFAULT_BACK_CAMERA)
}
val audioEnabled: MutableState<Boolean> = remember { mutableStateOf(true) }
val isAudioMenuVisible: MutableState<Boolean> = remember { mutableStateOf(false) }
val isLoading: MutableState<Boolean> = remember { mutableStateOf(false) }
val flashEnabled: MutableState<Int> =
remember { mutableStateOf(0) } // 0 off , 1 on , 2 auto
val isFlashMenuVisible: MutableState<Boolean> = remember { mutableStateOf(false) }
val animatedProgress = remember { Animatable(1f) }
val animatedLoaded = remember { mutableStateOf(isLabelLoaded) }
LaunchedEffect(Unit) {
cameraPermissionState.launchMultiplePermissionRequest()
}
var preview = Preview.Builder().build()
val previewView = remember { PreviewView(context) }
val imageCapture: ImageCapture = remember {
ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG)
.build()
}
imageCapture.flashMode = when (flashEnabled.value) {
0 -> ImageCapture.FLASH_MODE_OFF
1 -> ImageCapture.FLASH_MODE_ON
2 -> ImageCapture.FLASH_MODE_AUTO
else -> ImageCapture.FLASH_MODE_OFF
}
val qualitySelector = QualitySelector.from(
Quality.UHD,
FallbackStrategy.lowerQualityThan(Quality.UHD)
)
val recorder = Recorder.Builder()
.setExecutor(context.mainExecutor)
.setQualitySelector(qualitySelector)
.build()
val videoCapture = VideoCapture.withOutput(recorder)
LaunchedEffect(cameraSelector.value) {
Timber.d("image change camera lens: ${cameraSelector.value}")
preview = Preview.Builder().build().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val cameraProvider = context.getCameraProvider()
cameraProvider.unbindAll()
if (isDeviceSupportVideo) {
cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector.value,
preview,
imageCapture,
videoCapture
).apply {
preview.setSurfaceProvider(previewView.surfaceProvider)
}
}
else {
cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector.value,
preview,
imageCapture
).apply {
preview.setSurfaceProvider(previewView.surfaceProvider)
}
}
}
.......
// front camera button
IconButton(
onClick = {
cameraSelector.value =
if (cameraSelector.value == CameraSelector.DEFAULT_BACK_CAMERA) CameraSelector.DEFAULT_FRONT_CAMERA // ktlint-disable max-line-length
else CameraSelector.DEFAULT_BACK_CAMERA
onTracking(
if (cameraSelector.value == CameraSelector.DEFAULT_FRONT_CAMERA) FirstLookTools.FrontCamera else FirstLookTools.BackCamera // ktlint-disable max-line-length
)
if (cameraSelector.value == CameraSelector.DEFAULT_FRONT_CAMERA) {
onLabelLoaded(true)
}
},
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(bottom = 48.dp, end = 24.dp)
) {
Image(
painter = painterResource(R.drawable.ic_outline_change_source),
contentDescription = "",
modifier = Modifier.size(64.dp)
)
}
Start recording:
#SuppressLint("MissingPermission")
fun startRecordingVideo(
context: Context,
filename: String,
videoCapture: VideoCapture<Recorder>,
outputDirectory: File,
executor: Executor,
audioEnabled: Boolean,
consumer: Consumer<VideoRecordEvent>
): Recording {
val videoFile = File(
outputDirectory,
"$filename.mp4"
)
val outputOptions = FileOutputOptions.Builder(videoFile).build()
return videoCapture.output
.prepareRecording(context, outputOptions)
.apply { if (audioEnabled) withAudioEnabled() }
.start(executor, consumer)
}

Related

Jetpack Compose LazyColumn Performance Issue

I'm writing example-screen with using lazyColumn. I encountered some performance issues on release build. Frame skipping happens when I fast-scroll the list.
All models and composables are stable. My code is below;
Screen record link -> https://imgur.com/a/cvlA8g0
viewModel:
#HiltViewModel
class ExampleViewModel #Inject constructor(
private val repo: ExampleRepository,
) : ViewModel() {
private val _viewState = MutableStateFlow(ItemsViewState())
val viewState = _viewState.asStateFlow()
init {
fetch()
}
private fun fetch() = viewModelScope.launch {
repo.getItems()
.onStart { _viewState.value = _viewState.value.copy(state = PageState.Loading) }
.onCompletion { _viewState.value = _viewState.value.copy(state = PageState.Content) }
.collect { _viewState.value = _viewState.value.copy(items = it.toImmutableList()) }
}
}
viewState and models:
data class ItemsViewState(
val items: ImmutableList<Item> = persistentListOf(),
val state: PageState = PageState.Loading,
)
data class Item(
val id: Int,
val imageUrl: String,
val name: String,
val rating: Double,
val campaignText: String,
val isChecked: Boolean = false,
)
data class ItemViewState(val item: Item) {
fun isRatingVisible(): Boolean = item.rating > 7.0
}
sealed class PageState {
object Content : PageState()
object Loading : PageState()
object Error : PageState()
}
and my composable functions:
#Composable
fun ExampleScreen(
viewModel: ExampleViewModel = hiltViewModel(),
) {
val viewState by viewModel.viewState.collectAsState()
when (viewState.state) {
PageState.Content -> {
ExampleList(viewState = viewState)
}
PageState.Loading -> LoadingScreen()
PageState.Error -> {}
}
}
#Composable
private fun ExampleList(
viewState: ItemsViewState,
) {
LazyColumn(
state = rememberLazyListState(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxSize()
) {
items(viewState.items, key = { it.id }) { item ->
ExampleListItem(item = item)
}
}
}
#Composable
private fun ExampleListItem(item: Item) {
val viewState = ItemViewState(item)
Card(
shape = RoundedCornerShape(8.dp),
backgroundColor = MaterialTheme.colors.background
) {
Row(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.wrapContentHeight()
) {
AsyncImage(
model = item.imageUrl,
contentDescription = viewState.item.name,
contentScale = ContentScale.FillHeight,
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.width(120.dp)
.height(120.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Column(verticalArrangement = Arrangement.SpaceBetween) {
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
Text(
text = viewState.item.name,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier = Modifier.weight(1f),
)
Icon(imageVector = Icons.Default.List, contentDescription = null)
}
Spacer(modifier = Modifier.height(2.dp))
Row {
if (viewState.isRatingVisible()) {
Spacer(modifier = Modifier.width(4.dp))
Text(
text = viewState.item.rating.toString(),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
Spacer(modifier = Modifier.height(2.dp))
CampaignRow(campaignText = viewState.item.campaignText)
}
}
}
}
#Composable
private fun CampaignRow(
modifier: Modifier = Modifier,
campaignText: String,
) = Row(modifier = modifier) {
Image(
painter = painterResource(androidx.ui.livedata.R.drawable.abc_ic_star_black_16dp),
contentDescription = "",
modifier = Modifier
.wrapContentSize()
.align(Alignment.CenterVertically)
.padding(end = 4.dp)
)
Text(
text = campaignText,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
I followed google guideline to fix performance issues.
I created Baseline profile
I tested on release build
I used key parameter on lazyColumn
but still happening performance issues. How can I prevent this?

Loading progress bar does not appear when background task runs

I want to show a circular progress bar while the shopping list items are being retrieved from the database. I have a Lazycolumn that displays the retrieved shopping list items, but the circular progress bar is never displayed, and the message "You don't have any items in this shopping list." is displayed briefly before the list is shown. This behavior is not desired. In the viewmodel, placing loading.value = false after the database call in the viewModelScope coroutine does not work. How can I fix this?
ShoppingListScreen Composable
fun ShoppingListScreen(
navController: NavHostController,
shoppingListScreenViewModel: ShoppingListScreenViewModel,
sharedViewModel: SharedViewModel
) {
val scope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
val allItems = shoppingListScreenViewModel.shoppingListItemsState.value?.collectAsLazyPagingItems()
val showProgressBar = shoppingListScreenViewModel.loading.value
Scaffold(
topBar = {
CustomAppBar(
title = "Shopping List Screen",
titleFontSize = 20.sp,
appBarElevation = 4.dp,
navController = navController
)
},
floatingActionButton = {
FloatingActionButton(
onClick = {
shoppingListScreenViewModel.setStateValue(SHOW_ADD_ITEM_DIALOG_STR, true)
},
backgroundColor = Color.Blue,
contentColor = Color.White
) {
Icon(Icons.Filled.Add, "")
}
},
backgroundColor = Color.White,
// Defaults to false
isFloatingActionButtonDocked = false,
bottomBar = { BottomNavigationBar(navController = navController) }
) {
Box {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.height(screenHeight)
) {
if (allItems?.itemCount == 0) {
item { Text("You don't have any items in this shopping list.") }
}
items(
items = allItems!!,
key = { item ->
item.id
}
) { item ->
ShoppingListScreenItem(
navController = navController,
item = item,
sharedViewModel = sharedViewModel
) { isChecked ->
scope.launch {
shoppingListScreenViewModel.changeItemChecked(item!!, isChecked)
}
}
}
item { Spacer(modifier = Modifier.padding(screenHeight - (screenHeight - 70.dp))) }
}
ConditionalCircularProgressBar(isDisplayed = showProgressBar)
}
}
}
ShoppingListScreenViewModel
#HiltViewModel
class ShoppingListScreenViewModel #Inject constructor(
private val getAllShoppingListItemsUseCase: GetAllShoppingListItemsUseCase
) {
private val _shoppingListItemsState = mutableStateOf<Flow<PagingData<ShoppingListItem>>?>(null)
val shoppingListItemsState: State<Flow<PagingData<ShoppingListItem>>?> get() = _shoppingListItemsState
val loading = mutableStateOf(false)
init {
loading.value = true
getAllShoppingListItemsFromDb()
}
private fun getAllShoppingListItemsFromDb() {
viewModelScope.launch {
_shoppingListItemsState.value = getAllShoppingListItemsUseCase().distinctUntilChanged()
loading.value = false
}
}
}
put the CircularProgressIndicator inside a Surface or Box, maybe you are using Scaffold with navigation, in my case I was using padding top 60.dp in my circular progrees then it was kind of hinding.

Alarm runs immediately instead of specified time with jetpack compose

I am trying to set an alarm using a TimePicker but the alarm ignores the time sent by parameter and instead it is executed immediately !!
This is my code:
class MainActivity : ComponentActivity() {
lateinit var navController: NavHostController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
navController = rememberNavController()
NotePadReminderTheme {
SetupNavGraph(navController = navController)
}
}
}
}
#RequiresApi(Build.VERSION_CODES.M)
fun createAlarm(context: Context,time:Long){
val alarmManager =context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
val intent =Intent(context, AlarmReceiver::class.java)
Log.d("receivedtime","time $time")
val pendingIntent = PendingIntent.getBroadcast(context, Date().seconds, intent, PendingIntent.FLAG_IMMUTABLE)
alarmManager?.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pendingIntent)
Toast.makeText(context,"Alarma creada",Toast.LENGTH_LONG).show()
}
#RequiresApi(Build.VERSION_CODES.M)
#Composable
fun AddPage(navController: NavController, vm: TaskViewModel = viewModel()) {
val scope= rememberCoroutineScope()
val title = remember {
mutableStateOf("")
}
val description = remember {
mutableStateOf("")
}
val date = remember {
mutableStateOf("")
}
val time = Calendar.getInstance()
val iscompleted = remember {
mutableStateOf(false)
}
val context = LocalContext.current
Column(
modifier = Modifier
.fillMaxSize()
.padding(top = 30.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Agregar Tarea",
style = NoteTheme.typography.h1
)
RowOfThis(title, "Título", true)
RowOfThis(description, "Descripción", false)
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
DatePick(date)
}
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
TimePick(time)
}
Button(
onClick = {
if (title.value.isNullOrBlank() || description.value.isNullOrBlank() ||
date.value.isNullOrBlank()
) {
Log.d("date","date:${date.value}")
Toast.makeText(context, "Por favor ingrese todos los datos", Toast.LENGTH_LONG)
.show()
return#Button
}
vm.task.value.title = title.value
vm.task.value.description = description.value
vm.task.value.date = date.value
vm.task.value.time = time.timeInMillis
Log.d("tmepickerreceived","date:${time.timeInMillis}")
scope.launch {
vm.createTask(context, vm.task.value)
createAlarm(context, time=time.timeInMillis). //here set the alarm ****************
Toast.makeText(context, "Tarea creada correctamente", Toast.LENGTH_LONG).show()
vm.resetTask()
}
},
colors = ButtonDefaults.buttonColors(backgroundColor = Color.Blue)
) {
Text(text = "Guardar", color = Color.White, style = NoteTheme.typography.subtitle)
}
}
}
#Composable
fun RowOfThis(value: MutableState<String>, label: String, single: Boolean) {
val focusManager = LocalFocusManager.current
Row(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
) {
TextField(
modifier = Modifier
.fillMaxWidth()
.border(BorderStroke(width = 1.dp, color = Color.Black)),
singleLine = single,
maxLines = 3,
textStyle = NoteTheme.typography.body,
value = value.value, onValueChange = {
value.value = it
}, label = {
Text(
text = label, textAlign = TextAlign.Start,
color = Color.DarkGray,
style = NoteTheme.typography.subtitle
)
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.White),
keyboardActions = KeyboardActions(onNext =
{ focusManager.moveFocus((FocusDirection.Next)) })
)
}
}
#Composable
fun DatePick(mDate:MutableState<String>) {
val mContext = LocalContext.current
val mYear: Int
val mMonth: Int
val mDay: Int
val mCalendar = Calendar.getInstance()
mYear = mCalendar.get(Calendar.YEAR)
mMonth = mCalendar.get(Calendar.MONTH)
mDay = mCalendar.get(Calendar.DAY_OF_MONTH)
mCalendar.time = Date()
val mDatePickerDialog = DatePickerDialog(
mContext,
{ _: DatePicker, mYear: Int, mMonth: Int, mDayOfMonth: Int ->
mDate.value = "$mDayOfMonth/${mMonth + 1}/$mYear"
}, mYear, mMonth, mDay
)
mDatePickerDialog.datePicker.minDate=Date().time
Log.d("datepicker","dateset:${mDate.value}")
Button(modifier = Modifier
.padding(7.dp)
.fillMaxWidth(0.5f), onClick = {
mDatePickerDialog.show()
}, colors = ButtonDefaults.buttonColors(backgroundColor = Color(0XFF227093))) {
Text(text = "Seleccionar fecha",
color = Color.White, style = NoteTheme.typography.subtitle)
}
Text(text = "${mDate.value}",style = NoteTheme.typography.caption, fontSize = 22.sp, color = Color(0XFF227093))
}
#Composable
fun TimePick(mCalendar:Calendar) {
val mContext = LocalContext.current
val mHour = mCalendar[Calendar.HOUR_OF_DAY]
val mMinute = mCalendar[Calendar.MINUTE]
val mTime = remember { mutableStateOf("") }
val mTimePickerDialog = TimePickerDialog(
mContext,
{_, mHour : Int, mMinute: Int ->
mTime.value = "$mHour:$mMinute"
}, mHour, mMinute, false
)
Log.d("tmepicker","time:${mCalendar.timeInMillis}")
Button(modifier = Modifier
.padding(7.dp)
.fillMaxWidth(0.5f), onClick = {
mTimePickerDialog.show()
}, colors = ButtonDefaults.buttonColors(backgroundColor = Color(0XFF2c2c54))) {
Text(text = "Seleccionar hora",
color = Color.White, style = NoteTheme.typography.subtitle)
}
Text(text = "${mTime.value}",style = NoteTheme.typography.caption, fontSize = 22.sp, color = Color(0XFF2c2c54))
}
The problem I have is that I want the alarm to run at the scheduled time of the DatePicker ad TimePicker but it is running immediately !!
I don't know if this has something to do with the context, but how can I make the alarm be "scheduled" at the time and date of the DatePicker and Timepicker instead of firing at once
Well this was because when you get the calendar month you have to add one for some reason, so that was what caused an inaccurate time when setting the alarm. Final code:
class MainActivity : ComponentActivity() {
#RequiresApi(Build.VERSION_CODES.M)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val context= LocalContext.current
AlarmTestTheme {
mainPage(context)
}
}
}
}
#RequiresApi(Build.VERSION_CODES.M)
#Composable
fun mainPage(context: Context) {
val scope= rememberCoroutineScope()
val calendar=Calendar.getInstance().apply {
timeInMillis=System.currentTimeMillis()
}
val title = remember {
mutableStateOf("")
}
val description = remember {
mutableStateOf("")
}
val year= remember {
mutableStateOf(calendar.get(Calendar.YEAR))
}
val month= remember {
mutableStateOf(calendar.get(Calendar.MONTH)+1)
}
val day= remember {
mutableStateOf(calendar.get(Calendar.DAY_OF_MONTH))
}
val hour= remember {
mutableStateOf(calendar.get(Calendar.HOUR_OF_DAY))
}
val minutes= remember {
mutableStateOf(calendar.get(Calendar.MINUTE))
}
val iscompleted = remember {
mutableStateOf(false)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(top = 30.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Agregar Tarea"
)
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
DatePick(context = context, year = year, month = month, day = day)
}
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
TimePick(context = context, hours = hour, minutes = minutes)
}
Button(
onClick = {
Log.d("tmepickerreceived","date and time :${year.value}/${month.value}/${day.value} : ${hour.value}:${minutes.value}")
calendar.apply {
set(Calendar.YEAR,year.value)
set(Calendar.MONTH,(month.value-1))
set(Calendar.DAY_OF_MONTH,day.value)
set(Calendar.HOUR_OF_DAY,hour.value)
set(Calendar.MINUTE,minutes.value)
}
val alarmMgr= context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val shuff=(1..99999).shuffled().first()
val intent= Intent(context,AlarmReceiver::class.java)
intent.action="alarma"
intent.putExtra("ALARMA_STRING","alarm number: $shuff")
val pending=
PendingIntent.getBroadcast(context,shuff,intent, PendingIntent.FLAG_IMMUTABLE)
Log.e("time final","compare ${System.currentTimeMillis()}" +
" mytime: ${calendar.timeInMillis} " +
"hour: ${calendar.get(Calendar.HOUR_OF_DAY)} " +
"minute: ${calendar.get(Calendar.MINUTE)} " +
"year: ${calendar.get(Calendar.YEAR)} " +
"month: ${calendar.get(Calendar.MONTH)+1} " +
"day: ${calendar.get(Calendar.DAY_OF_MONTH)}")
alarmMgr?.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,calendar.timeInMillis,pending)
Toast.makeText(context, "Alarm created", Toast.LENGTH_LONG)
.show()
},
colors = ButtonDefaults.buttonColors(backgroundColor = Color.Blue)
) {
Text(text = "Guardar", color = Color.White)
}
}
}
#Composable
fun DatePick(context: Context, year: MutableState<Int>,month: MutableState<Int>,day: MutableState<Int>) {
val mDatePickerDialog = DatePickerDialog(
context,{d,mYear,mMonth,mDay ->
year.value=mYear
month.value=mMonth+1
day.value=mDay
}, year.value, month.value, day.value
)
mDatePickerDialog.datePicker.minDate=System.currentTimeMillis() - 1000
Log.d("datepicker","${day.value}/${month.value}/${year.value}")
Button(modifier = Modifier
.padding(7.dp)
.fillMaxWidth(0.5f), onClick = {
mDatePickerDialog.show()
}, colors = ButtonDefaults.buttonColors(backgroundColor = Color(0XFF227093))) {
Text(text = "Seleccionar fecha",
color = Color.White)
}
Text(text = "${day.value}/${month.value}/${year.value}", fontSize = 22.sp, color = Color(0XFF227093))
}
#Composable
fun TimePick(context: Context,hours:MutableState<Int>,minutes:MutableState<Int>) {
val mTimePickerDialog = TimePickerDialog(
context,
{_, mHour : Int, mMinute: Int ->
hours.value=mHour
minutes.value=mMinute
}, hours.value, minutes.value, false
)
Log.d("tmepicker","hour selected :${hours.value}: munute selected: ${minutes.value}")
Button(modifier = Modifier
.padding(7.dp)
.fillMaxWidth(0.5f), onClick = {
mTimePickerDialog.show()
}, colors = ButtonDefaults.buttonColors(backgroundColor = Color(0XFF2c2c54))) {
Text(text = "Seleccionar hora",
color = Color.White)
}
Text(text = "${hours.value}:${minutes.value}", fontSize = 22.sp, color = Color(0XFF2c2c54))
}

Jetpack Compose detect drag gesture and detect Interaction source

I want to develop a float draggable Button using jetpack compose,
also I need to know when user drag interaction starts and when it ends.
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
content = {
val interactionSource = remember { MutableInteractionSource() }
val interactions = remember { mutableStateListOf<Interaction>() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
Log.i("dragInteraction", "-> $interaction")
}
}
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Surface(
modifier = Modifier
.offset {
IntOffset(
x = offsetX.roundToInt(),
y = offsetY.roundToInt()
)
}
.size(60.dp)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
offsetX += dragAmount.x
offsetY += dragAmount.y
change.consume()
}
},
interactionSource = interactionSource,
onClick = {
},
content = {
},
color = Purple500
)
}
)
in this code my Surface moves currectly but I can't get DragInteraction.Start and
DragInteraction.Stop when I'm dragging it!
all I get is
androidx.compose.foundation.interaction.PressInteraction$Press#c38442d androidx.compose.foundation.interaction.PressInteraction$Cancel#e6d1ef3
any suggestion how can I detect drag state ?
detectDragGestures doesn't emit DragInteraction by default. You should either emit DragInteraction.Start, DragInteraction.Stop, DragInteraction.Cancel
as
#OptIn(ExperimentalMaterial3Api::class)
#Composable
private fun DragInteractionSample() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
content = {
val interactionSource = remember { MutableInteractionSource() }
val interactions = remember { mutableStateListOf<Interaction>() }
var text by remember { mutableStateOf("") }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is DragInteraction.Start -> {
text = "Drag Start"
}
is DragInteraction.Stop -> {
text = "Drag Stop"
}
is DragInteraction.Cancel -> {
text = "Drag Cancel"
}
}
}
}
val coroutineScope = rememberCoroutineScope()
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
val modifier = Modifier
.offset {
IntOffset(
x = offsetX.roundToInt(),
y = offsetY.roundToInt()
)
}
.size(60.dp)
.pointerInput(Unit) {
var interaction: DragInteraction.Start? = null
detectDragGestures(
onDragStart = {
coroutineScope.launch {
interaction = DragInteraction.Start()
interaction?.run {
interactionSource.emit(this)
}
}
},
onDrag = { change: PointerInputChange, dragAmount: Offset ->
offsetX += dragAmount.x
offsetY += dragAmount.y
},
onDragCancel = {
coroutineScope.launch {
interaction?.run {
interactionSource.emit(DragInteraction.Cancel(this))
}
}
},
onDragEnd = {
coroutineScope.launch {
interaction?.run {
interactionSource.emit(DragInteraction.Stop(this))
}
}
}
)
}
Surface(
modifier = modifier,
interactionSource = interactionSource,
onClick = {},
content = {},
color = MaterialTheme.colorScheme.primary
)
Text(text = text)
}
)
}
Result
or simply create an enum class and set it to a MutableState on each gesture function
enum class DragState {
Idle, DragStart, Drag, DragEnd, DragCancel
}
var dragState by remember{mutableStateOf(Idle}
And change this state on every drag callback.

Keyboard does not hide, when focused TextField leaves the LazyColumn

Perhaps this is the normal behaviour, but i wish it was different. I had tried to google the solution, but did not find anything suitable (or merely missed it).
Sample code (for simplicity i hold mutable states right here, not using ViewModel):
#Composable
fun Greeting() {
Scaffold(topBar = {
TopAppBar(title = { Text(text = "Some title") })
}) {
val focusManager = LocalFocusManager.current
LazyColumn(
contentPadding = PaddingValues(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
items(count = 20) { index ->
val (value, onValueChange) = rememberSaveable { mutableStateOf("Some value $index") }
TextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier.fillMaxWidth(),
label = { Text(text = "Some label $index") },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = {
if (!focusManager.moveFocus(FocusDirection.Down))
focusManager.clearFocus()
}),
singleLine = true
)
}
}
}
}
Compose version 1.0.5
You could try just hiding the keyboard whenever scrolling occurs. This is okay as long as you don't have a large set of items. But since you're using TextFields, it isn't likely that you'll have such a large number. This sample illustrates hiding the keyboard when scrolling occurs:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
Greeting(this)
}
}
}
#Composable
fun Greeting(activity: Activity) {
Scaffold(topBar = {
TopAppBar(title = { Text(text = "Some title") })
}) {
val lazyListState = rememberLazyListState()
val ctx = LocalContext.current
LaunchedEffect(lazyListState.firstVisibleItemIndex) {
val inputMethodManager = ctx.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(activity.window?.decorView?.windowToken, 0)
}
LazyColumn(
state = lazyListState,
contentPadding = PaddingValues(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
items(
count = 20,
key = { index ->
// Return a stable + unique key for the item
index
}
) { index ->
val (value, onValueChange) = rememberSaveable { mutableStateOf("Some value $index") }
TextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier
.fillMaxWidth(),
label = { Text(text = "Some label $index") },
singleLine = true
)
}
}
}
}

Resources