Lze s pomocí A.I. zvládnout vývoj appky pro Android bez jakýchkoliv znalostí?

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

  1. Fixní barva kreslení (černá)
  2. Fixní tloušťka čáry (2f, guma 20f)
  3. Velikost bitmapy pro widget (1000×1000)
  4. Interval aktualizace widgetu (30 min)
  5. 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>

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *