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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ Uncategorized
- DownloadManager sample. Should cover all cases (internet connection loss, fresh boot etc, cancellation) Compare with WorkManager, and describe pros & cons.
- [Baseline Profile](https://developer.android.com/studio/profile/baselineprofiles#creating-profile-rules). Measure the impact on dummy flows
- check the issue with compose & svg's. Icons don't mirror?
- SignatureView sample. Check if there is any way to optimize code (initial comment https://github.com/Skyyo/samples/pull/16#discussion_r795047275). Check out another sample (https://github.com/SmartToolFactory/Jetpack-Compose-Tutorials/tree/master/Tutorial1-1Basics/src/main/java/com/smarttoolfactory/tutorial1_1basics/chapter6_graphics)

# License
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ sealed class Destination(val route: String) {
object Otp : Destination("otp")
object Table : Destination("table")
object CustomView : Destination("customViewScreen")
object SignatureView : Destination("signatureViewScreen")
object MarqueeText : Destination("marqueeText")
object Autofill : Destination("autofill")
object DogFeed : Destination("dogFeed")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,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.snap.SnapScreen
import com.skyyo.samples.features.stickyHeaders.ListsScreen
Expand Down Expand Up @@ -141,6 +142,7 @@ fun PopulatedNavHost(
composable(Destination.Table.route) { TableScreen() }
composable(Destination.ParallaxEffect.route) { ParallaxEffectScreen() }
composable(Destination.CustomView.route) { CustomViewScreen() }
composable(Destination.SignatureView.route) { SignatureViewScreen() }
composable(Destination.MarqueeText.route) { MarqueeTextScreen() }
composable(Destination.Autofill.route) { AutofillScreen() }
navigation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ fun UIelements(viewModel: SampleContainerViewModel) {
)
)
}
Button(
modifier = Modifier.fillMaxWidth(),
onClick = viewModel::goSignatureView
) { Text(text = "signature view") }
Button(
modifier = Modifier.fillMaxWidth(),
onClick = viewModel::goMarqueeText
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ class SampleContainerViewModel @Inject constructor(
it.navigate(Destination.CustomView.route)
}

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

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

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize
data class MotionEventValue(val eventType: Int, val x: Float, val y: Float) : Parcelable
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.skyyo.samples.features.signatureView

import android.graphics.Bitmap
import android.view.MotionEvent
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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 kotlin.math.roundToInt

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SignatureView(
Skyyo marked this conversation as resolved.
Show resolved Hide resolved
modifier: Modifier = Modifier,
stroke: Stroke,
paintColor: Color = Color.Black,
canvasColor: Color = Color.Transparent,
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()).apply {
nativeCanvas.drawColor(canvasColor.toArgb())
}
canvasForSnapshot.drawPath(savedList.toPath(), paint)

return bitmap
}

LaunchedEffect(Unit) {
events.collect { event ->
when (event) {
SignatureViewEvent.Reset -> {
savedList.clear()
motionEventValue.value = MotionEventValue(MotionEvent.ACTION_DOWN, 0.0f, 0.0f)
}
SignatureViewEvent.Save -> {
val bitmap = getBitmap()
onBitmapSaved(bitmap)
}
}
}
}

Canvas(
modifier = modifier
.onGloballyPositioned { viewBounds.value = it.boundsInRoot() }
.clipToBounds()
.background(canvasColor)
.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 {
if (this != null) savedList.add(this)
drawPath(
path = savedList.toPath(),
color = paintColor,
style = stroke
)
}
}
}

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,3 @@
package com.skyyo.samples.features.signatureView

enum class SignatureViewEvent { Save, Reset }
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
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

@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
val stroke = remember { Stroke(10f) }

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)
}
val quality = 100
saved = bitmap.compress(Bitmap.CompressFormat.PNG, quality, fos) == true
fos?.flush()
fos?.close()
return saved
}
Skyyo marked this conversation as resolved.
Show resolved Hide resolved

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,
stroke = stroke,
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")
}
}
}
}