Předně zdůrazňuji, že nejsem programátor. Pouze pokročilý uživatel PC. Vím co je *.apk soubor a to je tak vše…
Chtěl jsem ale udělat pokus, jak daleko je umělá inteligence v roce 2024. Napadlo mě, že zkusím, jestli jen s pomocí AI dokážu vytvořit aplikaci pro sebe, kterou jsem vždy chtěl, ale nenašel ji v marketplace. Vybral jsem si placenou verzi Anthropicu – Claude 3.5 Sonnet NEW, protože s tou mám dlouhodobě nejlepší zkušenosti.
I tak jsem jí nedával velké naděje. Hlavně proto, že každá AI má omezené kontextové okno a paměť. A i když je aplikace primitivní, je docela náročné vést někoho, kdo o věci nic neví, krok za krokem ve stylu „přítele na telefonu“ v tak složitém konceptu. Na začátku jsem ani nevěděl, co to je nějaké Android Studio, natož jestli dokážu umělé inteligenci popisovat co vidím na monitoru a ona dokáže stále chápat jak pokračovat a v jaké fázi je.
Podotýkám, že jsem se na to nějak nepřipravoval ani neladil počáteční prompt. Prostě jsem hned od stolu napsal co mě napadlo a čekal co z toho vyleze metodou pokus omyl s pocitem, že po pár desítkách minut to stejně začne tápat a ztratí kontext a já nebudu vědět, jak to nasměrovat zpátky… Svatou trpělivost se mnou ale měla (musela, když si ji platim 🙂 a výsledek nakonec předčil všechna má očekávání.
Zadání tedy znělo:
„Chtěl bych naprogramovat aplikaci a widgetem pro Android. Jednalo by se o poznámkový blok, který by umožňoval psaní textu pomocí dotykového pera jako je například S pen. Takové aplikace jsem již v obchodě s aplikacemi našel, ale málokterá z nich umí widget udělat průhledný tak, aby výsledné psaní vypadalo, jako když jsou poznámky přímo na ploše nebo pozadí telefonu, protože kromě textu je celý widget průhledný. V aplikaci bych dále potřeboval tyto funkce: 1) Psaní pomocí dotykového pera 2) Posunování textu pomocí jednoho prstu 3) Zvětšování/zmenošovní, tedy zoom pomocí 2 prstů Dokázal bys takovou aplikaci naprogramovat a vysvětlit mi poté jak ji nahrát do obchodu Google Play? Příklady podobných aplikací, které již jsou v google play přítomny a kterými se můžeš inspirovat: Poznámkový bloček od HAPPY NEKO“
Zde se můžete podívat na průběh celé mé "facinující" konverzace s A.I (je ve dvou opravdu dlouhých oknech, kde stále drží kontext). Moc jsem jí to neulehčoval :-) 1 - claude.ai první okno - ( PDF ) 2 - claude.ai druhé okno - ( PDF )
No a výsledek? Neuvěřitelné, ale aplikace, po postupných krocích a upravách, skutečně funguje podle mých představ! Aplikaci mám ve verzi „0.4“ :-). Má pár drobných much, ale to vyladím v další verzi. Člověk by neřekl, že úplný laik bez jakýchkoliv znalostí programování dokáže vytvořit funkční aplikaci s množstvím řádků kódu, a navíc jej průběžně upravovat a vylepšovat. Nejde jen o soubory, ale nastavení celého prostředí Android Studia, různé doplňky, verzování aplikace, stromové struktury souborů, a kontext postupování a nabalování informací, atd atd. AI sice má své limity, hlavně v podobě omezeného kontextového okna, ale přesto je to už dnes ohromující nástroj. Ucelený výsledek jsem opravdu čekal jen v koutku duše.
Celkem bych řekl, že to zabralo tak 4-5 hodin čistého času. Předpokládám, že zkušený programátor by to zvládl mnohem rychleji. Ale jde o to, že se jedná o mne. Naprostého laika, který o tom nevěděl naprosto vůbec nic a v průběhu se i něco nového naučil. Navíc kdyby A.I. byla integrována přímo v Android Studiu a já ji nemusel neustále kopírovat screenshoty obrazovky, když mi to psalo nějakou chybu, trvalo by to také řádově kratší dobu. Nyní jsem tedy ve fázi, kdy v podstatě stačí doladit pár drobností a budu naprosto spokojen se vším, co to umí a dále už budu jen vylepšovat co mě dalšího napadne při používání…
Na závěr jsem A.I. nechal ještě vygenerovat celou dokumentaci (níže) do souboru *.md. Tak aby mohla v programování aplikace pokračovat i příště v novém kontextovém okně a mohla si „vzpomenout“ kde jsme skončili. Dokumentace je tedy primárně určena pro A.I. než pro mne. Uvidíme jak to bude fungovat a jestli mi to pomůže prodloužit kontextové okno. Navíc tvrdila, že zdrojáky nepotřebuje jen koncept.
Notees – Dokumentace projektu v0.4
1. Základní informace a architektura
Název: Notees
Package: com.example.notees
Min SDK: 24 (Android 7.0)
Target SDK: 34 (Android 14)
Verze: 0.4
Hlavní funkce: Kreslící aplikace s podporou S Pen a widgetem
2. Adresářová struktura
app/src/
├── main/
├── java/com.example.notees/
│ ├── MainActivity.kt # Hlavní aktivita
│ ├── DrawingView.kt # Komponenta pro kreslení
│ ├── DrawingManager.kt # Správa persistence dat
│ ├── PathData.kt # Datové třídy
│ ├── DrawingWidgetProvider.kt # Provider pro widget
│ └── DrawingWidgetView.kt # View komponenta widgetu
│
├── res/
│ ├── drawable/
│ │ ├── ic_launcher_*.xml
│ │ └── widget_preview.xml # Náhled widgetu
│ │
│ ├── layout/
│ │ ├── activity_main.xml # Layout hlavní aktivity
│ │ └── drawing_widget.xml # Layout widgetu
│ │
│ ├── values/
│ │ ├── colors.xml
│ │ ├── strings.xml # Textové řetězce
│ │ └── themes.xml
│ │
│ └── xml/
│ ├── backup_rules.xml
│ ├── data_extraction_rules.xml
│ └── drawing_widget_info.xml # Konfigurace widgetu
│
└── AndroidManifest.xml
3. Klíčové komponenty a jejich funkce
3.1 Kreslení (DrawingView)
class DrawingView : View {
// Hlavní vlastnosti
private val paths: MutableList<Pair<Path, Boolean>> // cesty a flag pro mazání
private var scaleFactor = 1f // aktuální zoom
private val maxZoom = 3.0f // limit zoomu
private val minZoom = 0.3f // limit zoomu
// Důležité funkční celky
- Kreslení S Penem (pouze stylus)
- Mazání tlačítkem S Penu
- Zoom dvěma prsty (fokální bod)
- Posun jedním prstem
- Double tap pro Undo
}
3.2 Persistence dat (DrawingManager)
class DrawingManager(context: Context) {
private val fileName = "drawing_data.bin"
// Hlavní funkce
fun saveDrawing(paths: List<Pair<Path, Boolean>>)
fun loadDrawing(): List<Pair<Path, Boolean>>
// Pomocné funkce pro konverzi
private fun convertPathToPoints(path: Path): ArrayList<Pair<Float, Float>>
private fun convertPointsToPath(points: ArrayList<Pair<Float, Float>>): Path
}
3.3 Widget systém
// Provider
class DrawingWidgetProvider : AppWidgetProvider {
// Konfigurace:
- Velikost: 180dp x 180dp
- Průhledné pozadí
- Bílá kresba
- Možnost resize
// Hlavní funkce:
override fun onUpdate() // Aktualizace widgetu
private fun updateWidget() // Překreslení obsahu
private fun createDrawingBitmap() // Vytvoření náhledu
}
// View
class DrawingWidgetView : View {
// Zobrazení kresby ve widgetu
- Průhledné pozadí
- Pouze kreslící tahy (bez gumy)
}
4. Datový model
4.1 Perzistentní datové třídy
data class PathData(
val points: ArrayList<Pair<Float, Float>>,
val isEraser: Boolean
) : Serializable
data class DrawingData(
val paths: ArrayList<PathData>
) : Serializable
4.2 Formát dat v paměti
List<Pair<Path, Boolean>>
- Path: Android grafická cesta
- Boolean: flag pro mazání (true = guma)
5. Uživatelské rozhraní
5.1 Hlavní aktivita
<ConstraintLayout>
<DrawingView
android:id="@+id/drawingView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<FloatingActionButton
android:id="@+id/clearButton"
android:src="@android:drawable/ic_menu_delete" />
</ConstraintLayout>
5.2 Widget
<FrameLayout
android:background="@android:color/transparent">
<ImageView
android:id="@+id/widget_preview"
android:contentDescription="@string/widget_preview_description" />
</FrameLayout>
6. Interakční model
6.1 Kreslení
- Pouze S Pen (stylus)
- Černá barva na bílém pozadí
- Tloušťka čáry: 2f
- Mazání: tlačítko S Pen + tah
6.2 Navigace
- Zoom: 2 prsty (0.3x – 3.0x)
- Pan: 1 prst
- Undo: double tap
- Reset: FAB tlačítko
6.3 Widget
- Tap: otevření aplikace
- Automatická aktualizace při změnách
- Interval aktualizace: 1800000ms (30 min)
7. Systém persistence
7.1 Automatické ukládání
- Při každé změně kresby
- Při opuštění aplikace (onPause)
- Při vymazání plátna
7.2 Formát souboru
- Název: „drawing_data.bin“
- Umístění: interní úložiště aplikace
- Serializovaný objekt DrawingData
8. Omezení a limity
- Fixní barva kreslení (černá)
- Fixní tloušťka čáry (2f, guma 20f)
- Velikost bitmapy pro widget (1000×1000)
- Interval aktualizace widgetu (30 min)
- Pouze jeden typ widgetu
9. Verze a changelog
v0.4 (aktuální)
- Přidán widget s průhledným pozadím
- Implementace persistence dat
- Bílá kresba ve widgetu
v0.3
- Vylepšené zoomování (fokální bod)
- Oprava limitů zoomu
v0.2
- Přidána podpora S Pen
- Implementace gest
v0.1
- Základní kreslení
- Základní UI
10. Přílohy – Kompletní zdrojové kódy (Dle A.I. toto není prý potřeba, vygeneroval jsem si navíc pro jistotu)
10.1 Kotlin soubory
MainActivity.kt
package com.example.notees
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.floatingactionbutton.FloatingActionButton
class MainActivity : AppCompatActivity() {
private lateinit var drawingView: DrawingView
private lateinit var drawingManager: DrawingManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Inicializace DrawingManager
drawingManager = DrawingManager(this)
// Inicializace DrawingView
drawingView = findViewById(R.id.drawingView)
// Načtení uložených dat
val savedPaths = drawingManager.loadDrawing()
drawingView.loadPaths(savedPaths)
// Nastavení posluchače změn v kresbě
drawingView.setOnPathsChangedListener { paths ->
drawingManager.saveDrawing(paths)
updateWidgets()
}
// Nastavení tlačítka pro vymazání
findViewById<FloatingActionButton>(R.id.clearButton).setOnClickListener {
drawingView.clearCanvas()
drawingManager.saveDrawing(emptyList())
updateWidgets()
}
}
private fun updateWidgets() {
try {
val intent = Intent(this, DrawingWidgetProvider::class.java)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val widgetManager = AppWidgetManager.getInstance(this)
val ids = widgetManager.getAppWidgetIds(ComponentName(this, DrawingWidgetProvider::class.java))
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
sendBroadcast(intent)
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onPause() {
super.onPause()
// Uložení dat při opuštění aplikace
val currentPaths = drawingView.getCurrentPaths()
drawingManager.saveDrawing(currentPaths)
updateWidgets()
}
}
PathData.kt
package com.example.notees
import android.graphics.Path
import java.io.Serializable
// Třída pro ukládání dat o cestách
data class PathData(
val points: ArrayList<Pair<Float, Float>>, // body cesty
val isEraser: Boolean // flag pro mazání
) : Serializable
// Třída pro celou kresbu
data class DrawingData(
val paths: ArrayList<PathData>
) : Serializable
DrawingManager.kt
package com.example.notees
import android.content.Context
import android.graphics.Path
import android.graphics.PathMeasure
import java.io.File
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
class DrawingManager(private val context: Context) {
private val fileName = "drawing_data.bin"
fun saveDrawing(paths: List<Pair<Path, Boolean>>) {
val drawingData = DrawingData(ArrayList(paths.map { (path, isEraser) ->
PathData(convertPathToPoints(path), isEraser)
}))
try {
context.openFileOutput(fileName, Context.MODE_PRIVATE).use { fos ->
ObjectOutputStream(fos).use { oos ->
oos.writeObject(drawingData)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun loadDrawing(): List<Pair<Path, Boolean>> {
try {
context.openFileInput(fileName).use { fis ->
ObjectInputStream(fis).use { ois ->
val drawingData = ois.readObject() as DrawingData
return drawingData.paths.map { pathData ->
Pair(convertPointsToPath(pathData.points), pathData.isEraser)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
return emptyList()
}
}
private fun convertPathToPoints(path: Path): ArrayList<Pair<Float, Float>> {
val points = ArrayList<Pair<Float, Float>>()
val pathMeasure = PathMeasure(path, false)
val coordinates = FloatArray(2)
val length = pathMeasure.length
var distance = 0f
val step = 1f // můžete upravit pro větší nebo menší přesnost
while (distance <= length) {
pathMeasure.getPosTan(distance, coordinates, null)
points.add(Pair(coordinates[0], coordinates[1]))
distance += step
}
return points
}
private fun convertPointsToPath(points: ArrayList<Pair<Float, Float>>): Path {
val path = Path()
if (points.isNotEmpty()) {
path.moveTo(points[0].first, points[0].second)
for (i in 1 until points.size) {
path.lineTo(points[i].first, points[i].second)
}
}
return path
}
}
DrawingView.kt
package com.example.notees
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import android.view.WindowManager
import kotlin.math.max
import kotlin.math.min
class DrawingView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paths = mutableListOf<Pair<Path, Boolean>>()
private var currentPath: Path? = null
private var isCurrentPathEraser = false
// Callback pro změny v kresbě
private var onPathsChangedListener: ((List<Pair<Path, Boolean>>) -> Unit)? = null
// Pro transformace plátna
private var scaleFactor = 1f
private var translateX = 0f
private var translateY = 0f
private var lastTouchX = 0f
private var lastTouchY = 0f
// Pro fokální bod zoomu
private var focusX = 0f
private var focusY = 0f
private var lastFocusX = 0f
private var lastFocusY = 0f
// Pro detekci double tapu
private var lastTapTime = 0L
private val doubleTapTimeout = 300L
// Pro pohyb a zoom
private var isZooming = false
private var isDrawing = false
private var isPanning = false
// Nastavení limitů plátna
private val maxZoom = 3.0f
private val minZoom = 0.3f
init {
setLayerType(LAYER_TYPE_HARDWARE, null)
}
private val penPaint = Paint().apply {
color = Color.BLACK
isAntiAlias = true
strokeWidth = 2f
style = Paint.Style.STROKE
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
}
private val eraserPaint = Paint().apply {
color = Color.TRANSPARENT
isAntiAlias = true
strokeWidth = 20f
style = Paint.Style.STROKE
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
}
private val scaleGestureDetector = ScaleGestureDetector(context,
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
isZooming = true
isPanning = false
lastFocusX = detector.focusX
lastFocusY = detector.focusY
return true
}
override fun onScale(detector: ScaleGestureDetector): Boolean {
val newScale = (scaleFactor * detector.scaleFactor)
.coerceAtLeast(minZoom)
.coerceAtMost(maxZoom)
if (newScale != scaleFactor) {
focusX = detector.focusX
focusY = detector.focusY
val focusShiftX = (focusX - lastFocusX)
val focusShiftY = (focusY - lastFocusY)
val oldScale = scaleFactor
scaleFactor = newScale
translateX += (focusX * (1 - newScale/oldScale) + focusShiftX)
translateY += (focusY * (1 - newScale/oldScale) + focusShiftY)
lastFocusX = focusX
lastFocusY = focusY
invalidate()
}
return true
}
override fun onScaleEnd(detector: ScaleGestureDetector) {
isZooming = false
}
})
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val viewWidth = width.toFloat()
val viewHeight = height.toFloat()
val maxTranslateX = viewWidth * (scaleFactor - 1)
val maxTranslateY = viewHeight * (scaleFactor - 1)
if (maxTranslateX > 0) {
translateX = translateX.coerceIn(-maxTranslateX, 0f)
} else {
translateX = 0f
}
if (maxTranslateY > 0) {
translateY = translateY.coerceIn(-maxTranslateY, 0f)
} else {
translateY = 0f
}
canvas.save()
canvas.scale(scaleFactor, scaleFactor)
canvas.translate(translateX / scaleFactor, translateY / scaleFactor)
for ((path, isEraser) in paths) {
canvas.drawPath(path, if (isEraser) eraserPaint else penPaint)
}
currentPath?.let { path ->
canvas.drawPath(path, if (isCurrentPathEraser) eraserPaint else penPaint)
}
canvas.restore()
}
private var isErasing = false
override fun onTouchEvent(event: MotionEvent): Boolean {
val isStylusActive = event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS
if (!isStylusActive) {
scaleGestureDetector.onTouchEvent(event)
}
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
handleActionDown(event)
return true
}
MotionEvent.ACTION_POINTER_DOWN -> {
if (!isStylusActive) {
isZooming = true
isDrawing = false
isPanning = false
}
return true
}
MotionEvent.ACTION_MOVE -> {
if (isStylusActive) {
isZooming = false
}
handleActionMove(event)
return true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
handleActionUp(event)
return true
}
MotionEvent.ACTION_POINTER_UP -> {
if (!isStylusActive) {
isZooming = event.pointerCount > 2
}
return true
}
}
return true
}
private fun handleActionDown(event: MotionEvent) {
isErasing = event.buttonState and MotionEvent.BUTTON_STYLUS_PRIMARY != 0
if (event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS) {
isDrawing = true
isPanning = false
isZooming = false
currentPath = Path().apply {
moveTo(getX(event), getY(event))
}
isCurrentPathEraser = isErasing
} else {
isDrawing = false
isPanning = true
lastTouchX = event.x
lastTouchY = event.y
}
}
private fun handleActionMove(event: MotionEvent) {
val isStylusActive = event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS
if (isStylusActive) {
isZooming = false
}
if (!isZooming && isDrawing && currentPath != null) {
currentPath?.lineTo(getX(event), getY(event))
invalidate()
} else if (!isZooming && isPanning && !isStylusActive) {
val dx = event.x - lastTouchX
val dy = event.y - lastTouchY
translateX += dx
translateY += dy
lastTouchX = event.x
lastTouchY = event.y
invalidate()
}
}
private fun handleActionUp(event: MotionEvent) {
if (isDrawing && currentPath != null) {
paths.add(Pair(currentPath!!, isCurrentPathEraser))
currentPath = null
isDrawing = false
onPathsChangedListener?.invoke(paths.toList())
} else if (!isZooming && !isDrawing && event.pointerCount == 1) {
val currentTime = System.currentTimeMillis()
if (currentTime - lastTapTime < doubleTapTimeout) {
undo()
lastTapTime = 0
} else {
lastTapTime = currentTime
}
}
isZooming = false
isPanning = false
}
private fun getX(event: MotionEvent): Float {
return (event.x - translateX) / scaleFactor
}
private fun getY(event: MotionEvent): Float {
return (event.y - translateY) / scaleFactor
}
private fun undo() {
if (paths.isNotEmpty()) {
paths.removeAt(paths.lastIndex)
invalidate()
onPathsChangedListener?.invoke(paths.toList())
}
}
fun setOnPathsChangedListener(listener: (List<Pair<Path, Boolean>>) -> Unit) {
onPathsChangedListener = listener
}
fun loadPaths(newPaths: List<Pair<Path, Boolean>>) {
paths.clear()
paths.addAll(newPaths)
invalidate()
}
fun getCurrentPaths(): List<Pair<Path, Boolean>> {
return paths.toList()
}
fun clearCanvas() {
paths.clear()
currentPath = null
scaleFactor = 1f
translateX = 0f
translateY = 0f
invalidate()
onPathsChangedListener?.invoke(paths.toList())
}
}
DrawingWidgetProvider.kt
package com.example.notees
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.graphics.*
import android.os.Bundle
import android.widget.RemoteViews
class DrawingWidgetProvider : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
val drawingManager = DrawingManager(context)
val paths = drawingManager.loadDrawing()
for (appWidgetId in appWidgetIds) {
updateWidget(context, appWidgetManager, appWidgetId, paths)
}
}
private fun updateWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
paths: List<Pair<Path, Boolean>>
) {
// Vytvoření bitmapy s kresbou
val bitmap = createDrawingBitmap(paths)
// Intent pro spuštění aplikace
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingIntent = PendingIntent.getActivity(
context,
appWidgetId,
intent,
PendingIntent.FLAG_IMMUTABLE
)
// Vytvoření a nastavení RemoteViews
val views = RemoteViews(context.packageName, R.layout.drawing_widget).apply {
// Nastavení click listeneru
setOnClickPendingIntent(R.id.widget_layout, pendingIntent)
// Nastavení bitmapy s kresbou
bitmap?.let { setImageViewBitmap(R.id.widget_preview, it) }
}
// Aktualizace widgetu
appWidgetManager.updateAppWidget(appWidgetId, views)
}
private fun createDrawingBitmap(paths: List<Pair<Path, Boolean>>): Bitmap? {
if (paths.isEmpty()) return null
// Vytvoření bitmapy s průhledným pozadím
val width = 1000 // Šířka bitmapy
val height = 1000 // Výška bitmapy
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
// Nastavení průhledného pozadí
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
// Paint pro kreslení
val paint = Paint().apply {
color = Color.WHITE // Bílá barva pro lepší viditelnost
isAntiAlias = true
strokeWidth = 4f
style = Paint.Style.STROKE
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
}
// Nalezení hranic kresby pro správné škálování
val bounds = RectF()
paths.forEach { (path, isEraser) ->
if (!isEraser) {
val pathBounds = RectF()
path.computeBounds(pathBounds, true)
bounds.union(pathBounds)
}
}
// Výpočet škálování pro přizpůsobení velikosti
val margin = 50f
val scaleX = (width - 2 * margin) / bounds.width()
val scaleY = (height - 2 * margin) / bounds.height()
val scale = minOf(scaleX, scaleY)
// Transformační matice pro správné umístění kresby
val matrix = Matrix()
matrix.setScale(scale, scale)
matrix.postTranslate(
-bounds.left * scale + margin,
-bounds.top * scale + margin
)
// Vykreslení všech cest
paths.forEach { (path, isEraser) ->
if (!isEraser) {
val transformedPath = Path()
path.transform(matrix, transformedPath)
canvas.drawPath(transformedPath, paint)
}
}
return bitmap
}
override fun onAppWidgetOptionsChanged(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
newOptions: Bundle
) {
val drawingManager = DrawingManager(context)
val paths = drawingManager.loadDrawing()
updateWidget(context, appWidgetManager, appWidgetId, paths)
}
}
DrawingWidgetView.kt
package com.example.notees
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.util.AttributeSet
import android.view.View
class DrawingWidgetView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paths = mutableListOf<Pair<Path, Boolean>>()
private val penPaint = Paint().apply {
color = android.graphics.Color.BLACK
isAntiAlias = true
strokeWidth = 2f
style = Paint.Style.STROKE
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Vykreslení všech cest
for ((path, isEraser) in paths) {
if (!isEraser) { // Ve widgetu zobrazujeme pouze kreslené čáry, ne gumu
canvas.drawPath(path, penPaint)
}
}
}
// Metoda pro aktualizaci dat z DrawingView
fun updatePaths(newPaths: List<Pair<Path, Boolean>>) {
paths.clear()
paths.addAll(newPaths)
invalidate()
}
}
10.2 XML Soubory
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.notees.DrawingView
android:id="@+id/drawingView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/clearButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="Vymazat vše"
android:src="@android:drawable/ic_menu_delete"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
drawing_widget.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="4dp"
android:clickable="true"
android:focusable="true"
android:background="@android:color/transparent">
<ImageView
android:id="@+id/widget_preview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"
android:contentDescription="@string/widget_preview_description" />
</FrameLayout>
drawing_widget_info.xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/drawing_widget"
android:minWidth="180dp"
android:minHeight="180dp"
android:previewImage="@drawable/widget_preview"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="1800000"
android:widgetCategory="home_screen"
android:label="Notees Widget" />
widget_preview.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#80000000" />
</shape>