Skip to content
This repository has been archived by the owner on Aug 7, 2024. It is now read-only.

Commit

Permalink
Implement a fully functional (but minimal) canvas overlay
Browse files Browse the repository at this point in the history
  • Loading branch information
SuhasDissa committed Jul 9, 2023
1 parent 78823d2 commit 6f50f4a
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 15 deletions.
7 changes: 1 addition & 6 deletions app/src/main/java/com/bnyro/recorder/ui/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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?) {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -34,6 +35,7 @@ class RecorderModel : ViewModel() {
var recordedTime by mutableStateOf<Long?>(null)
val recordedAmplitudes = mutableStateListOf<Int>()
private var activityResult: ActivityResult? = null
var canvasOverlay: CanvasOverlay? = null

private val handler = Handler(Looper.getMainLooper())

Expand All @@ -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")
Expand Down Expand Up @@ -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()
}
Expand Down
150 changes: 150 additions & 0 deletions app/src/main/java/com/bnyro/recorder/ui/views/Canvas.kt
Original file line number Diff line number Diff line change
@@ -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<PathProperties>()
}
val pathsUndone = remember { mutableStateListOf<PathProperties>() }
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
)
}
}
}
}
21 changes: 12 additions & 9 deletions app/src/main/java/com/bnyro/recorder/ui/views/CanvasOverlay.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() })
}
}

Expand All @@ -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())
}
}

Expand Down
28 changes: 28 additions & 0 deletions app/src/main/java/com/bnyro/recorder/ui/views/OverlayView.kt
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
}

}

0 comments on commit 6f50f4a

Please sign in to comment.