Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement signatureView (without PD handling) #16

Merged
merged 10 commits into from
Oct 26, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ sealed class Destination(val route: String) {
object Otp : Destination("otp")
object Table : Destination("table")
object CustomView : Destination("customViewScreen")
object SignatureView : Destination("signatureViewScreen")
object DogFeed : Destination("dogFeed")
object DogDetails : Destination("dogDetails/{dogId}") {
fun createRoute(dogId: String) = "dogDetails/$dogId"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import com.skyyo.samples.features.sharedViewModel.ProfileSharedViewModel
import com.skyyo.samples.features.sharedViewModel.confirmProfile.EditProfileConfirmationScreen
import com.skyyo.samples.features.sharedViewModel.editProfile.EditProfileScreen
import com.skyyo.samples.features.sharedViewModel.profile.ProfileScreen
import com.skyyo.samples.features.signatureView.SignatureViewScreen
import com.skyyo.samples.features.snackbar.SnackbarScreen
import com.skyyo.samples.features.stickyHeaders.ListsScreen
import com.skyyo.samples.features.table.TableScreen
Expand Down Expand Up @@ -115,6 +116,7 @@ fun PopulatedNavHost(
composable(Destination.Table.route) { TableScreen() }
composable(Destination.ParallaxEffect.route) { ParallaxEffectScreen() }
composable(Destination.CustomView.route) { CustomViewScreen() }
composable(Destination.SignatureView.route) { SignatureViewScreen() }
navigation(
route = ProfileGraph.route,
startDestination = ProfileGraph.Profile.route
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ fun UIelements(viewModel: SampleContainerViewModel) {
)
)
}
Button(
modifier = Modifier.fillMaxWidth(),
onClick = viewModel::goSignatureView
) { Text(text = "signature view") }
Spacer(modifier = Modifier.height(16.dp))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ class SampleContainerViewModel @Inject constructor(
it.navigate(Destination.CustomView.route)
}

fun goSignatureView() = navigationDispatcher.emit {
it.navigate(Destination.SignatureView.route)
}

fun goHiltComposeSharedViewModel() = navigationDispatcher.emit {
it.navigate(ProfileGraph.Profile.route)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.skyyo.samples.features.signatureView

import android.graphics.Bitmap
import android.view.MotionEvent
import androidx.compose.foundation.Canvas
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.onGloballyPositioned
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlin.math.roundToInt

data class MotionEventValue(val eventType: Int, val x: Float, val y: Float)
Skyyo marked this conversation as resolved.
Show resolved Hide resolved

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SignatureView(
Skyyo marked this conversation as resolved.
Show resolved Hide resolved
modifier: Modifier = Modifier,
stroke: Stroke = Stroke(10f),
paintColor: Color = Color.Black,
events: Flow<SignatureViewEvent>,
onBitmapSaved: (bitmap: Bitmap) -> Unit
) {
val viewBounds = remember { mutableStateOf(Rect.Zero) }
val paint = remember {
Paint().apply {
style = PaintingStyle.Stroke
color = paintColor
strokeWidth = stroke.width
}
}
val savedList = rememberSaveable { mutableListOf<MotionEventValue>() }
val motionEventValue = remember { mutableStateOf<MotionEventValue?>(null) }

fun getBitmap(): Bitmap {
Skyyo marked this conversation as resolved.
Show resolved Hide resolved
val bounds = viewBounds.value
val bitmap = Bitmap.createBitmap(
bounds.width.roundToInt(), bounds.height.roundToInt(),
Bitmap.Config.ARGB_8888
)
val canvasForSnapshot = Canvas(bitmap.asImageBitmap())
canvasForSnapshot.drawPath(savedList.toPath(), paint)

return bitmap
}

LaunchedEffect(Unit) {
events.collect { event ->
when (event) {
SignatureViewEvent.Reset -> {
motionEventValue.value = null
savedList.clear()
}
SignatureViewEvent.Save -> {
val bitmap = getBitmap()
onBitmapSaved(bitmap)
}
}
}
}

Canvas(
modifier = modifier
.onGloballyPositioned { viewBounds.value = it.boundsInRoot() }
.clipToBounds()
.pointerInteropFilter {
val x = it.x
val y = it.y
val value: MotionEventValue
when (it.action) {
MotionEvent.ACTION_DOWN -> {
value = MotionEventValue(it.action, x,y)
motionEventValue.value = value
}
MotionEvent.ACTION_MOVE -> {
value = MotionEventValue(it.action, x,y)
motionEventValue.value = value
}
}
true
},
) {
motionEventValue.value.run {
this?.let {
savedList.add(it)
}
Skyyo marked this conversation as resolved.
Show resolved Hide resolved
drawPath(
path = savedList.toPath(),
color = paintColor,
alpha = 1f,
style = Stroke(4f)
)
}
Skyyo marked this conversation as resolved.
Show resolved Hide resolved
}
}

fun List<MotionEventValue>.toPath(): Path {
val path = Path()
forEach { value ->
if (value.eventType == MotionEvent.ACTION_DOWN) {
path.moveTo(value.x, value.y)
} else {
path.lineTo(value.x, value.y)
}
}
return path
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.skyyo.samples.features.signatureView

import android.content.ContentResolver
import android.content.ContentValues
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream

enum class SignatureViewEvent { Save, Reset }

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SignatureViewScreen() {
val lifecycleOwner = LocalLifecycleOwner.current
val signatureEvents = remember { Channel<SignatureViewEvent>(Channel.UNLIMITED) }
val signatureEventsFlow = remember(signatureEvents, lifecycleOwner) {
signatureEvents
.receiveAsFlow()
.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.RESUMED)
}
val context = LocalContext.current

fun saveMediaToStorage(bitmap: Bitmap, imageName: String = "bitmap"): Boolean {
val saved: Boolean
val fos: OutputStream?
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val resolver: ContentResolver = context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, imageName)
put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/")
}

val imageUri: Uri? =
resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
fos = imageUri?.let { resolver.openOutputStream(it) }
} else {
val imagesDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DCIM
).toString() + File.separator
val file = File(imagesDir)
if (!file.exists()) {
file.mkdir()
}
val image = File(imagesDir, "$imageName.png")
fos = FileOutputStream(image)
}
saved = bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos) == true
fos?.flush()
fos?.close()
return saved
}

Column(
modifier = Modifier
.fillMaxSize()
.background(Color.LightGray),
verticalArrangement = Arrangement.Center
) {
SignatureView(
modifier = Modifier
.height(200.dp)
.width(200.dp)
.align(CenterHorizontally)
.background(Color.White),
events = signatureEventsFlow,
onBitmapSaved = {
saveMediaToStorage(it)
}
)
Row(
Modifier
.fillMaxWidth()
.align(CenterHorizontally),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(onClick = { signatureEvents.trySend(SignatureViewEvent.Save) }) {
Text(text = "Save")
}
Button(onClick = { signatureEvents.trySend(SignatureViewEvent.Reset) }) {
Text(text = "Reset")
}
}
}
}