From 6f50f4a316d90f9d7c11eea2887351bffbb008b9 Mon Sep 17 00:00:00 2001 From: Suhas Dissanayake Date: Sun, 9 Jul 2023 23:35:40 +0530 Subject: [PATCH] Implement a fully functional (but minimal) canvas overlay --- .../com/bnyro/recorder/ui/MainActivity.kt | 7 +- .../bnyro/recorder/ui/models/RecorderModel.kt | 7 + .../com/bnyro/recorder/ui/views/Canvas.kt | 150 ++++++++++++++++++ .../bnyro/recorder/ui/views/CanvasOverlay.kt | 21 +-- .../bnyro/recorder/ui/views/OverlayView.kt | 28 ++++ 5 files changed, 198 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/bnyro/recorder/ui/views/Canvas.kt create mode 100644 app/src/main/java/com/bnyro/recorder/ui/views/OverlayView.kt diff --git a/app/src/main/java/com/bnyro/recorder/ui/MainActivity.kt b/app/src/main/java/com/bnyro/recorder/ui/MainActivity.kt index b4a71cb2..0ae8b235 100644 --- a/app/src/main/java/com/bnyro/recorder/ui/MainActivity.kt +++ b/app/src/main/java/com/bnyro/recorder/ui/MainActivity.kt @@ -1,6 +1,5 @@ package com.bnyro.recorder.ui -import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -16,7 +15,6 @@ import com.bnyro.recorder.enums.ThemeMode import com.bnyro.recorder.ui.models.ThemeModel import com.bnyro.recorder.ui.screens.RecorderView import com.bnyro.recorder.ui.theme.RecordYouTheme -import com.bnyro.recorder.ui.views.CanvasOverlay class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -29,10 +27,7 @@ class MainActivity : ComponentActivity() { "screen" -> Recorder.SCREEN else -> Recorder.NONE } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val canvasOverlay = CanvasOverlay(this) - canvasOverlay.show() - } + setContent { RecordYouTheme( when (val mode = themeModel.themeMode) { diff --git a/app/src/main/java/com/bnyro/recorder/ui/models/RecorderModel.kt b/app/src/main/java/com/bnyro/recorder/ui/models/RecorderModel.kt index f854134d..17d9ef68 100644 --- a/app/src/main/java/com/bnyro/recorder/ui/models/RecorderModel.kt +++ b/app/src/main/java/com/bnyro/recorder/ui/models/RecorderModel.kt @@ -24,6 +24,7 @@ import com.bnyro.recorder.services.AudioRecorderService import com.bnyro.recorder.services.LosslessRecorderService import com.bnyro.recorder.services.RecorderService import com.bnyro.recorder.services.ScreenRecorderService +import com.bnyro.recorder.ui.views.CanvasOverlay import com.bnyro.recorder.util.PermissionHelper import com.bnyro.recorder.util.Preferences @@ -34,6 +35,7 @@ class RecorderModel : ViewModel() { var recordedTime by mutableStateOf(null) val recordedAmplitudes = mutableStateListOf() private var activityResult: ActivityResult? = null + var canvasOverlay: CanvasOverlay? = null private val handler = Handler(Looper.getMainLooper()) @@ -59,6 +61,10 @@ class RecorderModel : ViewModel() { activityResult = result val serviceIntent = Intent(context, ScreenRecorderService::class.java) startRecorderService(context, serviceIntent) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvasOverlay = CanvasOverlay(context) + canvasOverlay?.show() + } } @SuppressLint("NewApi") @@ -98,6 +104,7 @@ class RecorderModel : ViewModel() { fun stopRecording() { recorderService?.onDestroy() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) canvasOverlay?.remove() recordedTime = null recordedAmplitudes.clear() } diff --git a/app/src/main/java/com/bnyro/recorder/ui/views/Canvas.kt b/app/src/main/java/com/bnyro/recorder/ui/views/Canvas.kt new file mode 100644 index 00000000..d43ae2fc --- /dev/null +++ b/app/src/main/java/com/bnyro/recorder/ui/views/Canvas.kt @@ -0,0 +1,150 @@ +package com.bnyro.recorder.ui.views + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.positionChange + +enum class MotionEvent { + Up, Down, Idle, Move +} + +enum class DrawMode { + Pen, Eraser +} + +@Composable +fun MainCanvas() { + val paths = remember { + mutableStateListOf() + } + val pathsUndone = remember { mutableStateListOf() } + var motionEvent by remember { mutableStateOf(MotionEvent.Idle) } + var currentPath by remember { mutableStateOf(PathProperties()) } + + var currentPosition by remember { mutableStateOf(Offset.Unspecified) } + + val drawModifier = Modifier + .fillMaxSize() + .pointerInput(Unit) { + awaitEachGesture { + val downEvent = awaitFirstDown() + currentPosition = downEvent.position + motionEvent = MotionEvent.Down + if (downEvent.pressed != downEvent.previousPressed) downEvent.consume() + do { + val event = awaitPointerEvent() + if (event.changes.size == 1) { + currentPosition = event.changes[0].position + motionEvent = MotionEvent.Move + if (event.changes[0].positionChange() != Offset.Zero) event.changes[0].consume() + } + } while (event.changes.any { it.pressed }) + motionEvent = MotionEvent.Up + + } + } + Canvas(modifier = drawModifier) { + with(drawContext.canvas.nativeCanvas) { + val checkPoint = saveLayer(null, null) + when (motionEvent) { + MotionEvent.Idle -> Unit + MotionEvent.Down -> { + paths.add(currentPath) + currentPath.path.moveTo( + currentPosition.x, currentPosition.y + ) + } + + MotionEvent.Move -> { + currentPath.path.lineTo( + currentPosition.x, currentPosition.y + ) + drawCircle( + center = currentPosition, + color = Color.Gray, + radius = currentPath.strokeWidth / 2, + style = Stroke( + width = 1f + ) + ) + } + + MotionEvent.Up -> { + currentPath.path.lineTo( + currentPosition.x, currentPosition.y + ) + currentPath = PathProperties( + path = Path(), + strokeWidth = currentPath.strokeWidth, + color = currentPath.color, + drawMode = currentPath.drawMode + ) + pathsUndone.clear() + currentPosition = Offset.Unspecified + motionEvent = MotionEvent.Idle + } + } + paths.forEach { path -> + path.draw(this@Canvas) + } + restoreToCount(checkPoint) + } + + } +} + +class PathProperties( + var path: Path = Path(), + var strokeWidth: Float = 10f, + var color: Color = Color.Red, + var drawMode: DrawMode = DrawMode.Pen +) { + fun draw(scope: DrawScope) { + when (drawMode) { + DrawMode.Pen -> { + + scope.drawPath( + color = color, + path = path, + style = Stroke( + width = strokeWidth, + cap = StrokeCap.Round, + join = StrokeJoin.Round + ) + ) + } + + DrawMode.Eraser -> { + scope.drawPath( + color = Color.Transparent, + path = path, + style = Stroke( + width = strokeWidth, + cap = StrokeCap.Round, + join = StrokeJoin.Round + ), + blendMode = BlendMode.Clear + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bnyro/recorder/ui/views/CanvasOverlay.kt b/app/src/main/java/com/bnyro/recorder/ui/views/CanvasOverlay.kt index e9ca56db..f8886de2 100644 --- a/app/src/main/java/com/bnyro/recorder/ui/views/CanvasOverlay.kt +++ b/app/src/main/java/com/bnyro/recorder/ui/views/CanvasOverlay.kt @@ -8,8 +8,6 @@ import android.util.Log import android.view.ViewGroup import android.view.WindowManager import androidx.annotation.RequiresApi -import androidx.compose.material3.Button -import androidx.compose.material3.Text import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.Lifecycle import androidx.lifecycle.setViewTreeLifecycleOwner @@ -23,17 +21,14 @@ class CanvasOverlay(context: Context) { WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - PixelFormat.TRANSLUCENT + PixelFormat.TRANSPARENT ) private var windowManager = context.getSystemService(WINDOW_SERVICE) as WindowManager private var composeView = ComposeView(context).apply { setContent { - Button(onClick = { - this@CanvasOverlay.hide() - }) { - Text("Close") - } + OverlayView( + onDismissRequest = { this@CanvasOverlay.remove() }) } } @@ -58,12 +53,20 @@ class CanvasOverlay(context: Context) { } fun hide() { + try { + windowManager.removeView(composeView) + } catch (e: Exception) { + Log.e("Hide Overlay", e.toString()) + } + } + + fun remove() { try { windowManager.removeView(composeView) composeView.invalidate() (composeView.parent as ViewGroup).removeAllViews() } catch (e: Exception) { - Log.e("Hide Overlay", e.toString()) + Log.e("Remove Overlay", e.toString()) } } diff --git a/app/src/main/java/com/bnyro/recorder/ui/views/OverlayView.kt b/app/src/main/java/com/bnyro/recorder/ui/views/OverlayView.kt new file mode 100644 index 00000000..38f1ab23 --- /dev/null +++ b/app/src/main/java/com/bnyro/recorder/ui/views/OverlayView.kt @@ -0,0 +1,28 @@ +package com.bnyro.recorder.ui.views + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun OverlayView(onDismissRequest: () -> Unit) { + Box(Modifier.fillMaxSize()) { + MainCanvas() + Card(Modifier.align(Alignment.TopEnd)) { + Row { + IconButton(onClick = { onDismissRequest() }) { + Icon(Icons.Default.Close, "Close Overlay") + } + } + } + } + +} \ No newline at end of file