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

Added a screen annotation tool (experimental) #154

Merged
merged 6 commits into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

<!-- Opt out for network permissions that are optional for the ExoPlayer -->
<uses-permission
android:name="android.permission.ACCESS_NETWORK_STATE"
Expand Down
144 changes: 144 additions & 0 deletions app/src/main/java/com/bnyro/recorder/canvas_overlay/Canvas.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package com.bnyro.recorder.canvas_overlay

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.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
import androidx.lifecycle.viewmodel.compose.viewModel

enum class MotionEvent {
Up, Down, Idle, Move
}

enum class DrawMode {
Pen, Eraser
}

@Composable
fun MainCanvas(canvasViewModel: CanvasViewModel = viewModel()) {
var motionEvent by remember { mutableStateOf(MotionEvent.Idle) }

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 -> {
canvasViewModel.paths.add(canvasViewModel.currentPath)
canvasViewModel.currentPath.path.moveTo(
currentPosition.x, currentPosition.y
)
}

MotionEvent.Move -> {
canvasViewModel.currentPath.path.lineTo(
currentPosition.x, currentPosition.y
)
drawCircle(
center = currentPosition,
color = Color.Gray,
radius = canvasViewModel.currentPath.strokeWidth / 2,
style = Stroke(
width = 1f
)
)
}

MotionEvent.Up -> {
canvasViewModel.currentPath.path.lineTo(
currentPosition.x, currentPosition.y
)
canvasViewModel.currentPath = PathProperties(
path = Path(),
strokeWidth = canvasViewModel.currentPath.strokeWidth,
color = canvasViewModel.currentPath.color,
drawMode = canvasViewModel.currentPath.drawMode
)
currentPosition = Offset.Unspecified
motionEvent = MotionEvent.Idle
}
}
canvasViewModel.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 = 50f,
cap = StrokeCap.Round,
join = StrokeJoin.Round
),
blendMode = BlendMode.Clear
)
}
}
}
}
106 changes: 106 additions & 0 deletions app/src/main/java/com/bnyro/recorder/canvas_overlay/CanvasOverlay.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.bnyro.recorder.canvas_overlay

import android.content.Context
import android.content.Context.WINDOW_SERVICE
import android.graphics.PixelFormat
import android.os.Build
import android.util.Log
import android.view.Gravity
import android.view.WindowManager
import androidx.annotation.RequiresApi
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.bnyro.recorder.ui.theme.RecordYouTheme
import com.bnyro.recorder.util.CustomLifecycleOwner


@RequiresApi(Build.VERSION_CODES.O)
class CanvasOverlay(context: Context) {
private var params: WindowManager.LayoutParams = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSPARENT
)
private var windowManager = context.getSystemService(WINDOW_SERVICE) as WindowManager
private var canvasView = ComposeView(context).apply {
setContent {
RecordYouTheme() {
MainCanvas()
}
}
}
private var toolbarView = ComposeView(context).apply {
setContent {
RecordYouTheme {
ToolbarView(hideCanvas = { hide ->
if (hide) {
hideCanvas()
} else {
showCanvas()
}
})
}
}
}

init {
val lifecycleOwner = CustomLifecycleOwner()
val viewModelStoreOwner = object : ViewModelStoreOwner {
override val viewModelStore: ViewModelStore = ViewModelStore()
}
lifecycleOwner.performRestore(null)
lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
canvasView.setViewTreeLifecycleOwner(lifecycleOwner)
canvasView.setViewTreeViewModelStoreOwner(viewModelStoreOwner)
canvasView.setViewTreeSavedStateRegistryOwner(lifecycleOwner)

toolbarView.setViewTreeLifecycleOwner(lifecycleOwner)
toolbarView.setViewTreeViewModelStoreOwner(viewModelStoreOwner)
toolbarView.setViewTreeSavedStateRegistryOwner(lifecycleOwner)

hideCanvas()
}

fun showAll() {
try {
if (canvasView.windowToken == null && canvasView.parent == null) {
windowManager.addView(canvasView, params)

}
if (toolbarView.windowToken == null && toolbarView.parent == null) {
val toolbarParams = params
toolbarParams.gravity = Gravity.TOP or Gravity.END
windowManager.addView(toolbarView, toolbarParams)
}
} catch (e: Exception) {
Log.e("Show Overlay", e.toString())
}
}

fun showCanvas() {
canvasView.isVisible = true
}

fun hideCanvas() {
canvasView.isInvisible = true
}

fun remove() {
try {
windowManager.removeView(canvasView)
canvasView.invalidate()
windowManager.removeView(toolbarView)
toolbarView.invalidate()
} catch (e: Exception) {
Log.e("Remove Overlay", e.toString())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.bnyro.recorder.canvas_overlay

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel

class CanvasViewModel : ViewModel() {
var currentPath by mutableStateOf(PathProperties())
val paths = mutableStateListOf<PathProperties>()
}
53 changes: 53 additions & 0 deletions app/src/main/java/com/bnyro/recorder/canvas_overlay/ToolbarView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.bnyro.recorder.canvas_overlay

import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Draw
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.painterResource
import androidx.lifecycle.viewmodel.compose.viewModel
import com.bnyro.recorder.R

@Composable
fun ToolbarView(
hideCanvas: (Boolean) -> Unit,
canvasViewModel: CanvasViewModel = viewModel()
) {
var currentDrawMode by remember { mutableStateOf(DrawMode.Pen) }
Card() {
Row {
IconButton(
onClick = {
currentDrawMode = DrawMode.Pen
canvasViewModel.currentPath.drawMode = currentDrawMode
hideCanvas(false)
}) {
Icon(imageVector = Icons.Default.Draw, contentDescription = "Draw Mode")
}
IconButton(
onClick = {
currentDrawMode = DrawMode.Eraser
canvasViewModel.currentPath.drawMode = currentDrawMode
}) {
Icon(
painter = painterResource(id = R.drawable.ic_eraser_black_24dp),
contentDescription = "Erase Mode"
)
}
IconButton(onClick = {
hideCanvas(true)
canvasViewModel.paths.clear()
}) {
Icon(Icons.Default.Close, "Show/Hide Canvas")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,14 @@ fun SettingsBottomSheet(
)
}
Spacer(modifier = Modifier.height(10.dp))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CheckboxPref(
prefKey = Preferences.showOverlayAnnotationToolKey,
title = stringResource(R.string.screen_recorder_annotation),
summary = stringResource(R.string.screen_recorder_annotation_desc)
)
}
Spacer(modifier = Modifier.height(10.dp))
NamingPatternPref()
}

Expand Down
Loading
Loading