Ritagliare gli oggetti del canvas

Questo codelab fa parte del corso Advanced Android in Kotlin. Per ottenere il massimo valore da questo corso, ti consigliamo di seguire i codelab in sequenza, ma non è obbligatorio. Tutti i codelab del corso sono elencati nella pagina di destinazione dei codelab Advanced Android in Kotlin.

Introduzione

Ai fini di questo codelab, il ritaglio è un modo per definire le regioni di un'immagine, di un canvas o di una bitmap che vengono disegnate o meno in modo selettivo sullo schermo. Uno degli scopi del ritaglio è ridurre l'overdraw. L'overdraw si verifica quando un pixel sullo schermo viene disegnato più di una volta per visualizzare l'immagine finale. Quando riduci l'overdraw, riduci al minimo il numero di volte in cui viene disegnato un pixel o una regione del display, in modo da massimizzare le prestazioni di disegno. Puoi anche utilizzare il ritaglio per creare effetti interessanti nella progettazione di interfacce utente e nell'animazione.

Ad esempio, quando disegni una pila di carte sovrapposte come mostrato di seguito, anziché disegnare completamente ogni carta dal basso verso l'alto, di solito è più efficiente disegnare solo le porzioni visibili. "Di solito", perché anche le operazioni di ritaglio hanno un costo e, nel complesso, il sistema Android esegue molte ottimizzazioni del disegno.

Per disegnare solo le porzioni visibili delle carte, devi specificare una regione di ritaglio per ogni carta. Ad esempio, nel diagramma seguente, quando a un'immagine viene applicato un rettangolo di ritaglio, viene visualizzata solo la parte all'interno di questo rettangolo.

La regione di ritaglio è in genere un rettangolo, ma può essere qualsiasi forma o combinazione di forme, anche testo. Puoi anche specificare se vuoi includere o escludere la regione all'interno della regione di ritaglio. Ad esempio, puoi creare una regione di ritaglio circolare e visualizzare solo ciò che si trova al di fuori del cerchio.

In questo codelab, sperimenterai vari modi di ritagliare.

Cosa devi già sapere

Devi avere familiarità con:

  • Come creare un'app con un Activity ed eseguirla utilizzando Android Studio.
  • Come creare e disegnare su un Canvas.
  • Come creare un View personalizzato ed eseguire l'override di onDraw() e onSizeChanged().

Obiettivi didattici

  • Come ritagliare gli oggetti per disegnare su un Canvas.
  • Come salvare e ripristinare gli stati di disegno di un canvas.
  • Come applicare trasformazioni a un canvas e a un testo.

In questo lab proverai a:

  • Crea un'app che disegna forme ritagliate sullo schermo, mostrando diversi modi di ritagliare e il risultato sulla visibilità di queste forme.
  • Disegnerai anche del testo tradotto e distorto.

L'app ClippingExample mostra come utilizzare e combinare le forme per specificare quali parti di un canvas vengono visualizzate in una visualizzazione. L'app finale sarà simile a quella dello screenshot seguente.

Creerai questa app da zero, quindi dovrai configurare un progetto, definire dimensioni e stringhe e dichiarare alcune variabili.

Passaggio 1: crea il progetto ClippingExample

  1. Crea un progetto Kotlin denominato ClippingExample con il modello Attività vuota. Utilizza com.example.android come prefisso del nome del pacchetto.
  2. Apri MainActivity.kt.
  3. Nel metodo onCreate(), sostituisci la visualizzazione dei contenuti predefinita e impostala su una nuova istanza di ClippedView. Questa sarà la visualizzazione personalizzata per gli esempi di ritaglio che creerai in seguito.
setContentView(ClippedView(this))
  1. Allo stesso livello di MainActivity.kt, crea un nuovo file e una nuova classe Kotlin per una visualizzazione personalizzata denominata ClippedView che estende View. Assegna la firma mostrata di seguito. Il resto del lavoro sarà contenuto in questo ClippedView. L'annotazione @JvmOverloads indica al compilatore Kotlin di generare overload per questa funzione che sostituiscono i valori predefiniti dei parametri.
class ClippedView @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
}

Passaggio 2: aggiungi dimensioni e risorse stringa

  1. Definisci le dimensioni che utilizzerai per le visualizzazioni ritagliate in un nuovo file di risorse in res/values/dimens.xml. Queste dimensioni predefinite sono codificate e dimensionate per adattarsi a uno schermo piuttosto piccolo.
<?xml version="1.0" encoding="utf-8"?>
<resources>
   <dimen name="clipRectRight">90dp</dimen>
   <dimen name="clipRectBottom">90dp</dimen>
   <dimen name="clipRectTop">0dp</dimen>
   <dimen name="clipRectLeft">0dp</dimen>

   <dimen name="rectInset">8dp</dimen>
   <dimen name="smallRectOffset">40dp</dimen>

   <dimen name="circleRadius">30dp</dimen>
   <dimen name="textOffset">20dp</dimen>
   <dimen name="strokeWidth">4dp</dimen>

   <dimen name="textSize">18sp</dimen>
</resources>

Affinché l'app abbia un bell'aspetto su uno schermo più grande (e per vedere più facilmente i dettagli), puoi creare un file dimens con valori più grandi che si applicano solo agli schermi più grandi.

  1. In Android Studio, fai clic con il tasto destro del mouse sulla cartella values e scegli New > Values resource file.
  2. Nella finestra di dialogo Nuovo file di risorse, chiama il file dimens. In Qualificatori disponibili, seleziona Larghezza schermo più piccola e fai clic sul pulsante >> per aggiungerlo a Qualificatori scelti. Inserisci 480 nella casella Larghezza schermo più piccola e fai clic su Ok.

  1. Il file dovrebbe essere visualizzato nella cartella dei valori come mostrato di seguito.

  1. Se non riesci a visualizzare il file, passa alla visualizzazione File di progetto dell'app. Il percorso completo del nuovo file è il seguente: ClippingExample/app/src/main/res/values-sw480dp/dimens.xml.

  1. Sostituisci i contenuti predefiniti del file values-sw480dp/dimens.xml con le dimensioni riportate di seguito.
<?xml version="1.0" encoding="utf-8"?>
<resources>
   <dimen name="clipRectRight">120dp</dimen>
   <dimen name="clipRectBottom">120dp</dimen>

   <dimen name="rectInset">10dp</dimen>
   <dimen name="smallRectOffset">50dp</dimen>

   <dimen name="circleRadius">40dp</dimen>
   <dimen name="textOffset">25dp</dimen>
   <dimen name="strokeWidth">6dp</dimen>
</resources>
  1. In strings.xml, aggiungi le seguenti stringhe. Questi verranno utilizzati per visualizzare il testo sul canvas.
<string name="clipping">Clipping</string>
<string name="translated">translated text</string>
<string name="skewed">"Skewed and "</string>

Passaggio 3: crea e inizializza un oggetto Paint e un oggetto Path

  1. Torna alla visualizzazione Android del progetto.
  2. In ClippedView definisci una variabile Paint da utilizzare per disegnare. Attiva l'anti-aliasing e utilizza lo spessore del tratto e le dimensioni del testo definiti nelle dimensioni, come mostrato di seguito.
private val paint = Paint().apply {
   // Smooth out edges of what is drawn without affecting shape.
   isAntiAlias = true
   strokeWidth = resources.getDimension(R.dimen.strokeWidth)
   textSize = resources.getDimension(R.dimen.textSize)
}
  1. In ClippedView, crea e inizializza un Path per memorizzare localmente il percorso di ciò che è stato disegnato. Importa android.graphics.Path.
private val path = Path()

Passaggio 4: configura le forme

In questa app vengono visualizzate diverse righe e due colonne di forme ritagliate in vari modi.

Tutti hanno in comune:

  • Un rettangolo grande (quadrato) che funge da contenitore
  • Una linea diagonale che attraversa il rettangolo grande
  • Un cerchio
  • Una breve stringa di testo

In questo passaggio, imposti le dimensioni per queste forme dalle risorse, in modo da doverle recuperare una sola volta quando le utilizzi in un secondo momento.

  1. In ClippedView, sotto path, aggiungi le variabili per le dimensioni di un rettangolo di ritaglio intorno all'intero insieme di forme.
private val clipRectRight = resources.getDimension(R.dimen.clipRectRight)
private val clipRectBottom = resources.getDimension(R.dimen.clipRectBottom)
private val clipRectTop = resources.getDimension(R.dimen.clipRectTop)
private val clipRectLeft = resources.getDimension(R.dimen.clipRectLeft)
  1. Aggiungi variabili per l'inset di un rettangolo e l'offset di un rettangolo piccolo.
private val rectInset = resources.getDimension(R.dimen.rectInset)
private val smallRectOffset = resources.getDimension(R.dimen.smallRectOffset)
  1. Aggiungi una variabile per il raggio di un cerchio. Questo è il raggio del cerchio disegnato all'interno del rettangolo.
private val circleRadius = resources.getDimension(R.dimen.circleRadius)
  1. Aggiungi un offset e una dimensione del testo per il testo disegnato all'interno del rettangolo.
private val textOffset = resources.getDimension(R.dimen.textOffset)
private val textSize = resources.getDimension(R.dimen.textSize)

Passaggio 4: configura le posizioni di righe e colonne

Le forme per questa app vengono visualizzate in due colonne e quattro righe, determinate dai valori delle dimensioni configurate sopra. I calcoli non fanno parte di questo codelab, ma dai un'occhiata mentre copi il codice fornito in questo passaggio.

  1. Configura le coordinate per due colonne.
private val columnOne = rectInset
private val columnTwo = columnOne + rectInset + clipRectRight
  1. Aggiungi le coordinate per ogni riga, inclusa l'ultima riga per il testo trasformato.
private val rowOne = rectInset
private val rowTwo = rowOne + rectInset + clipRectBottom
private val rowThree = rowTwo + rectInset + clipRectBottom
private val rowFour = rowThree + rectInset + clipRectBottom
private val textRow = rowFour + (1.5f * clipRectBottom)
  1. Esegui l'app. L'app dovrebbe aprirsi con una schermata bianca vuota sotto il nome dell'app.

In onDraw(), chiami i metodi per disegnare sette rettangoli ritagliati diversi, come mostrato nello screenshot dell'app di seguito. I rettangoli sono tutti disegnati allo stesso modo; l'unica differenza è la posizione sullo schermo e le regioni di ritaglio definite.

L'algoritmo utilizzato per disegnare i rettangoli funziona come mostrato nel diagramma e nella spiegazione di seguito. In sintesi, disegni una serie di rettangoli spostando l'origine di Canvas. A livello concettuale, questa operazione prevede i seguenti passaggi:

(1) Innanzitutto, traduci Canvas nel punto in cui vuoi disegnare il rettangolo. In altre parole, anziché calcolare dove devono essere disegnati il rettangolo successivo e tutte le altre forme, sposti l'origine Canvas, ovvero il suo sistema di coordinate.

(2) Quindi, disegna il rettangolo nella nuova origine del canvas. ovvero disegni le forme nella stessa posizione nel sistema di coordinate tradotto. Questo è molto più semplice e leggermente più efficiente.

(3) Infine, ripristini Canvas nel suo Origin originale.

Ecco l'algoritmo da implementare:

  1. In onDraw(), chiama una funzione per riempire Canvas con il colore di sfondo grigio e disegnare le forme originali.
  2. Chiama una funzione per ogni rettangolo ritagliato e il testo da disegnare.

Per ogni rettangolo o testo:

  1. Salva lo stato attuale di Canvas in modo da poterlo ripristinare.
  2. Sposta il Origin del canvas nella posizione in cui vuoi disegnare.
  3. Applica forme e tracciati di ritaglio.
  4. Disegna il rettangolo o il testo.
  5. Ripristina lo stato di Canvas.

Passaggio: esegui l'override di onDraw()

  1. Esegui l'override di onDraw() come mostrato nel codice seguente. Chiami una funzione per ogni forma che disegni, che implementerai in un secondo momento.
 override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        drawBackAndUnclippedRectangle(canvas)
        drawDifferenceClippingExample(canvas)
        drawCircularClippingExample(canvas)
        drawIntersectionClippingExample(canvas)
        drawCombinedClippingExample(canvas)
        drawRoundedRectangleClippingExample(canvas)
        drawOutsideClippingExample(canvas)
        drawSkewedTextExample(canvas)
        drawTranslatedTextExample(canvas)
        // drawQuickRejectExample(canvas)
    }
  1. Crea stub per ciascuna delle funzioni di disegno in modo che il codice continui a essere compilato. Puoi copiare il codice riportato di seguito.
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
}
private fun drawDifferenceClippingExample(canvas: Canvas){
}
private fun drawCircularClippingExample(canvas: Canvas){
}
private fun drawIntersectionClippingExample(canvas: Canvas){
}
private fun drawCombinedClippingExample(canvas: Canvas){
}
private fun drawRoundedRectangleClippingExample(canvas: Canvas){
}
private fun drawOutsideClippingExample(canvas: Canvas){
}
private fun drawTranslatedTextExample(canvas: Canvas){
}
private fun drawSkewedTextExample(canvas: Canvas){
}
private fun drawQuickRejectExample(canvas: Canvas){
}

L'app disegna lo stesso rettangolo e le stesse forme sette volte: la prima senza ritaglio, le altre sei con vari tracciati di ritaglio applicati. Il metodo drawClippedRectangle() fattorizza il codice per disegnare un rettangolo, come mostrato di seguito.

Passaggio 1: crea il metodo drawClippedRectangle()

  1. Crea un metodo drawClippedRectangle() che accetta un argomento canvas di tipo Canvas.
private fun drawClippedRectangle(canvas: Canvas) {
}
  1. All'interno del metodo drawClippedRectangle(), imposta i limiti del rettangolo di ritaglio per l'intera forma. Applica un rettangolo di ritaglio che vincola il disegno solo al quadrato.
canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight,clipRectBottom
)

Il metodo Canvas.clipRect(...) riduce la regione dello schermo in cui le operazioni di disegno future possono scrivere. Imposta i limiti di ritaglio in modo che corrispondano all'intersezione spaziale del rettangolo di ritaglio corrente e del rettangolo passato a clipRect(). Esistono molte varianti del metodo clipRect() che accettano forme diverse per le regioni e consentono operazioni diverse sul rettangolo di ritaglio.

  1. Riempi canvas con il colore bianco. Sì. L'intera tela, perché non stai disegnando rettangoli, ma stai ritagliando. A causa del rettangolo di ritaglio, viene riempita solo la regione definita dal rettangolo di ritaglio, creando un rettangolo bianco. Il resto della superficie rimane grigio.
canvas.drawColor(Color.WHITE)
  1. Cambia il colore in rosso e disegna una linea diagonale all'interno del rettangolo di ritaglio.
paint.color = Color.RED
canvas.drawLine(
   clipRectLeft,clipRectTop,
   clipRectRight,clipRectBottom,paint
)
  1. Imposta il colore su verde e disegna un cerchio all'interno del rettangolo di ritaglio.
paint.color = Color.GREEN
canvas.drawCircle(
   circleRadius,clipRectBottom - circleRadius,
   circleRadius,paint
)
  1. Imposta il colore su blu e disegna il testo allineato al bordo destro del rettangolo di ritaglio. Utilizza canvas.drawText() per disegnare il testo.
paint.color = Color.BLUE
// Align the RIGHT side of the text with the origin.
paint.textSize = textSize
paint.textAlign = Paint.Align.RIGHT
canvas.drawText(
   context.getString(R.string.clipping),
   clipRectRight,textOffset,paint
)

Passaggio 2: implementa il metodo drawBackAndUnclippedRectangle()

  1. Per vedere il metodo drawClippedRectangle() in azione, disegna il primo rettangolo non ritagliato implementando il metodo drawBackAndUnclippedRectangle() come mostrato di seguito. Salva canvas, esegui la traslazione nella prima riga e nella prima colonna, disegna chiamando drawClippedRectangle() e poi ripristina canvas allo stato precedente.
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
   canvas.drawColor(Color.GRAY)
   canvas.save()
   canvas.translate(columnOne,rowOne)
   drawClippedRectangle(canvas)
   canvas.restore()
}
  1. Esegui l'app. Dovresti vedere il primo rettangolo bianco con il cerchio, la linea rossa e il testo su uno sfondo grigio.

Nei seguenti esempi di metodi di ritaglio, applichi varie combinazioni di regioni di ritaglio per ottenere effetti grafici e imparare a combinare le regioni di ritaglio per creare qualsiasi forma ti serva.

Ognuno di questi metodi segue lo stesso pattern.

  1. Salva lo stato attuale del canvas: canvas.save()

Il contesto dell'attività mantiene uno stack di stati di disegno. Gli stati del disegno sono costituiti dalla matrice di trasformazione corrente e dalla regione di ritaglio corrente. Puoi salvare lo stato attuale, eseguire azioni che modificano lo stato del disegno (ad esempio la traslazione o la rotazione del canvas) e poi ripristinare lo stato del disegno salvato. Nota: questo è simile al comando "stash" in git.

Quando il disegno include trasformazioni, concatenare e annullare le trasformazioni invertendole è soggetto a errori. Ad esempio, se traduci, allunghi e poi ruoti, la situazione si complica rapidamente. Salva invece lo stato del canvas, applica le trasformazioni, disegna e poi ripristina lo stato precedente.

Ad esempio, puoi definire una regione di ritaglio e salvare lo stato. Poi traduci la tela, aggiungi una regione di ritaglio e ruota. Dopo aver disegnato, puoi ripristinare lo stato di ritaglio originale e procedere con una diversa trasformazione di traslazione e distorsione, come mostrato nel diagramma.

  1. Traduci l'origine del canvas nelle coordinate di riga/colonna: canvas.translate()

È molto più semplice spostare l'origine del canvas e disegnare la stessa cosa in un nuovo sistema di coordinate che spostare tutti gli elementi da disegnare. Suggerimento: puoi utilizzare la stessa tecnica per ruotare gli elementi.

  1. Applica le trasformazioni a path, se presenti.
  2. Applica ritaglio: canvas.clipPath(path)
  3. Disegna le forme: drawClippedRectangle() or drawText()
  4. Ripristina lo stato precedente del canvas: canvas.restore()

Passaggio 1: implementa drawDifferenceClippingExample(canvas)

Aggiungi il codice per disegnare il secondo rettangolo, che utilizza la differenza tra due rettangoli di ritaglio per creare un effetto cornice.

Utilizza il codice riportato di seguito, che esegue le seguenti operazioni:

  1. Salva la tela.
  2. Sposta l'origine del canvas nello spazio aperto della prima riga, seconda colonna, a destra del primo rettangolo.
  3. Applica due rettangoli di ritaglio. L'operatore DIFFERENCE sottrae il secondo rettangolo dal primo.
  1. Chiama il metodo drawClippedRectangle() per disegnare il canvas modificato.
  2. Ripristina lo stato del canvas.
private fun drawDifferenceClippingExample(canvas: Canvas) {
   canvas.save()
   // Move the origin to the right for the next rectangle.
   canvas.translate(columnTwo,rowOne)
   // Use the subtraction of two clipping rectangles to create a frame.
   canvas.clipRect(
       2 * rectInset,2 * rectInset,
       clipRectRight - 2 * rectInset,
       clipRectBottom - 2 * rectInset
   )
   // The method clipRect(float, float, float, float, Region.Op
   // .DIFFERENCE) was deprecated in API level 26. The recommended
   // alternative method is clipOutRect(float, float, float, float),
   // which is currently available in API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O){
       canvas.clipRect(
           4 * rectInset,4 * rectInset,
           clipRectRight - 4 * rectInset,
           clipRectBottom - 4 * rectInset,
            Region.Op.DIFFERENCE
       )
   } else {
       canvas.clipOutRect(
           4 * rectInset,4 * rectInset,
           clipRectRight - 4 * rectInset,
           clipRectBottom - 4 * rectInset
       )
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}
  1. Esegui l'app e dovrebbe avere questo aspetto.

Passaggio 2: implementa drawCircularClippingExample(canvas)

Successivamente, aggiungi il codice per disegnare un rettangolo che utilizza una regione di ritaglio circolare creata da un percorso circolare, rimuovendo (non disegnando) essenzialmente il cerchio e mostrando quindi lo sfondo grigio.

private fun drawCircularClippingExample(canvas: Canvas) {

   canvas.save()
   canvas.translate(columnOne, rowTwo)
   // Clears any lines and curves from the path but unlike reset(),
   // keeps the internal data structure for faster reuse.
   path.rewind()
   path.addCircle(
       circleRadius,clipRectBottom - circleRadius,
       circleRadius,Path.Direction.CCW
   )
   // The method clipPath(path, Region.Op.DIFFERENCE) was deprecated in
   // API level 26. The recommended alternative method is
   // clipOutPath(Path), which is currently available in
   // API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
       canvas.clipPath(path, Region.Op.DIFFERENCE)
   } else {
       canvas.clipOutPath(path)
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}

Passaggio 3: implementa drawIntersectionClippingExample(canvas)

Poi aggiungi il codice per disegnare l'intersezione di due rettangoli di ritaglio nella seconda riga e colonna.

Tieni presente che l'aspetto di questa regione varia a seconda della risoluzione dello schermo. Sperimenta con la dimensione smallRectOffset per modificare le dimensioni della regione visibile. Un valore di smallRectOffset più piccolo comporta una regione più grande sullo schermo.

private fun drawIntersectionClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnTwo,rowTwo)
   canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight - smallRectOffset,
       clipRectBottom - smallRectOffset
   )
   // The method clipRect(float, float, float, float, Region.Op
   // .INTERSECT) was deprecated in API level 26. The recommended
   // alternative method is clipRect(float, float, float, float), which
   // is currently available in API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
       canvas.clipRect(
           clipRectLeft + smallRectOffset,
           clipRectTop + smallRectOffset,
           clipRectRight,clipRectBottom,
           Region.Op.INTERSECT
       )
   } else {
       canvas.clipRect(
           clipRectLeft + smallRectOffset,
           clipRectTop + smallRectOffset,
           clipRectRight,clipRectBottom
       )
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}

Passaggio 4: implementa drawCombinedClippingExample(canvas)

Successivamente, combina le forme, un cerchio e un rettangolo, e disegna un percorso per definire una regione di ritaglio.

private fun drawCombinedClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnOne, rowThree)
   path.rewind()
   path.addCircle(
       clipRectLeft + rectInset + circleRadius,
       clipRectTop + circleRadius + rectInset,
       circleRadius,Path.Direction.CCW
   )
   path.addRect(
       clipRectRight / 2 - circleRadius,
       clipRectTop + circleRadius + rectInset,
       clipRectRight / 2 + circleRadius,
       clipRectBottom - rectInset,Path.Direction.CCW
   )
   canvas.clipPath(path)
   drawClippedRectangle(canvas)
   canvas.restore()
}

Passaggio 5: implementa drawRoundedRectangleClippingExample(canvas)

Successivamente, aggiungi un rettangolo arrotondato, una forma di ritaglio di uso comune.

  1. Al livello superiore, crea e inizializza una variabile rettangolo. RectF è una classe che contiene le coordinate del rettangolo in virgola mobile.
private var rectF = RectF(
   rectInset,
   rectInset,
   clipRectRight - rectInset,
   clipRectBottom - rectInset
)
  1. Implementa la funzione drawRoundedRectangleClippingExample(). La funzione addRoundRect() accetta un rettangolo, i valori per i raggi degli angoli x e y e la direzione in cui avvolgere il contorno del rettangolo arrotondato. Path.Direction specifica l'orientamento delle forme chiuse (ad es. rettangoli, ovali) quando vengono aggiunte a un percorso. CCW sta per senso antiorario.
private fun drawRoundedRectangleClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnTwo,rowThree)
   path.rewind()
   path.addRoundRect(
       rectF,clipRectRight / 4,
       clipRectRight / 4, Path.Direction.CCW
   )
   canvas.clipPath(path)
   drawClippedRectangle(canvas)
   canvas.restore()
}

Passaggio 6: implementa drawOutsideClippingExample(canvas)

Taglia l'esterno intorno al rettangolo raddoppiando i rientri del rettangolo di ritaglio.

private fun drawOutsideClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnOne,rowFour)
   canvas.clipRect(2 * rectInset,2 * rectInset,
       clipRectRight - 2 * rectInset,
       clipRectBottom - 2 * rectInset)
   drawClippedRectangle(canvas)
   canvas.restore()
}

Passaggio 7: implementa drawTranslatedTextExample(canvas)

Il testo disegnato non è molto diverso da qualsiasi altra forma e puoi applicare trasformazioni al testo. Ad esempio, puoi tradurre il testo traducendo il canvas e disegnando il testo.

  1. Implementa la funzione riportata di seguito.
private fun drawTranslatedTextExample(canvas: Canvas) {
   canvas.save()
   paint.color = Color.GREEN
   // Align the RIGHT side of the text with the origin.
   paint.textAlign = Paint.Align.LEFT
   // Apply transformation to canvas.
   canvas.translate(columnTwo,textRow)
   // Draw text.
   canvas.drawText(context.getString(R.string.translated),
       clipRectLeft,clipRectTop,paint)
   canvas.restore()
}
  1. Esegui l'app per visualizzare il testo tradotto.

Passaggio 8: implementa drawSkewedTextExample(canvas)

Puoi anche inclinare il testo. ovvero distorcerlo in vari modi.

  1. Crea la funzione riportata di seguito in ClippedView.
private fun drawSkewedTextExample(canvas: Canvas) {
   canvas.save()
   paint.color = Color.YELLOW
   paint.textAlign = Paint.Align.RIGHT
   // Position text.
   canvas.translate(columnTwo, textRow)
   // Apply skew transformation.
   canvas.skew(0.2f, 0.3f)
   canvas.drawText(context.getString(R.string.skewed),
       clipRectLeft, clipRectTop, paint)
   canvas.restore()
}
  1. Esegui l'app per visualizzare il testo distorto disegnato prima del testo tradotto.

Il metodo quickReject() Canvas consente di verificare se un rettangolo o un percorso specificato si trova completamente al di fuori delle regioni attualmente visibili, dopo l'applicazione di tutte le trasformazioni.

Il metodo quickReject() è incredibilmente utile quando crei disegni più complessi e devi farlo il più rapidamente possibile. Con quickReject(), puoi decidere in modo efficiente quali oggetti non devi disegnare e non è necessario scrivere la tua logica di intersezione.

  • Il metodo quickReject() restituisce true se il rettangolo o il percorso non sarebbe visibile sullo schermo. Per le sovrapposizioni parziali, devi comunque eseguire il controllo.
  • EdgeType è AA (Antialiased: tratta i bordi arrotondandoli, perché potrebbero essere antialiased) o BW (Black-White: tratta i bordi arrotondandoli al limite del pixel più vicino) per l'arrotondamento al pixel più vicino.

Esistono diverse versioni di quickReject(), che puoi trovare anche nella documentazione.

boolean

quickReject(float left, float top, float right, float bottom, Canvas.EdgeType type)

boolean

quickReject(RectF rect, Canvas.EdgeType type)

boolean

quickReject(Path path, Canvas.EdgeType type)

In questo esercizio, disegnerai una nuova riga sotto il testo e all'interno di clipRect, come prima.

  • Chiami per prima quickReject() con un rettangolo inClipRectangle che si sovrappone a clipRect. Quindi quickReject() restituisce false, clipRect viene riempito con BLACK e viene disegnato il rettangolo inClipRectangle.

  • Poi modifica il codice e chiama quickReject(), con notInClipRectangle. quickReject() ora restituisce il valore True, clipRect è riempito con WHITE e notInClipRectangle non viene disegnato.

Quando hai disegni complessi, questo può dirti rapidamente quali forme si trovano completamente al di fuori della regione di ritaglio e per quali potresti dover eseguire calcoli e disegni aggiuntivi, perché si trovano parzialmente o completamente all'interno della regione di ritaglio.

Passaggio: sperimenta con quickReject()

  1. Al livello principale, crea una variabile per le coordinate Y di una riga aggiuntiva.
   private val rejectRow = rowFour + rectInset + 2*clipRectBottom
  1. Aggiungi la seguente funzione drawQuickRejectExample() a ClippedView. Leggi il codice, perché contiene tutto ciò che devi sapere per utilizzare quickReject().
private fun drawQuickRejectExample(canvas: Canvas) {
   val inClipRectangle = RectF(clipRectRight / 2,
       clipRectBottom / 2,
       clipRectRight * 2,
       clipRectBottom * 2)

   val notInClipRectangle = RectF(RectF(clipRectRight+1,
       clipRectBottom+1,
       clipRectRight * 2,
       clipRectBottom * 2))

   canvas.save()
   canvas.translate(columnOne, rejectRow)
   canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight,clipRectBottom
   )
   if (canvas.quickReject(
           inClipRectangle, Canvas.EdgeType.AA)) {
       canvas.drawColor(Color.WHITE)
   }
   else {
       canvas.drawColor(Color.BLACK)
       canvas.drawRect(inClipRectangle, paint
       )
   }
       canvas.restore()
}
  1. In onDraw(), rimuovi il commento dalla chiamata di drawQuickRejectExample().
  2. Esegui l'app e vedrai un rettangolo nero, che è la regione di ritaglio riempita, e parti di inClipRectangle, perché i due rettangoli si sovrappongono, quindi quickReject() restituisce false e viene disegnato inClipRectangle.

  1. In drawQuickRejectExample(), modifica il codice per eseguire quickReject() su notInClipRectangle.. Ora quickReject() restituisce true e la regione di ritaglio è riempita di bianco.

Scarica il codice per il codelab completato.

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-clipping


In alternativa, puoi scaricare il repository come file ZIP, decomprimerlo e aprirlo in Android Studio.

Scarica zip

  • Il Context di un'attività mantiene uno stato che conserva le trasformazioni e le regioni di ritaglio per il Canvas.
  • Utilizza canvas.save() e canvas.restore() per disegnare e tornare allo stato originale della tela.
  • Per disegnare più forme su un canvas, puoi calcolarne la posizione oppure spostare (traslare) l'origine della superficie di disegno. Quest'ultimo può semplificare la creazione di metodi di utilità per sequenze di estrazione ripetute.
  • Le regioni di ritaglio possono essere qualsiasi forma, combinazione di forme o percorso.
  • Puoi aggiungere, sottrarre e intersecare regioni di ritaglio per ottenere esattamente la regione che ti serve.
  • Puoi applicare trasformazioni al testo trasformando il canvas.
  • Il metodo quickReject() Canvas consente di verificare se un rettangolo o un percorso specificato si trova completamente al di fuori delle regioni attualmente visibili.

Corso Udacity:

Documentazione per sviluppatori Android:

Consulta anche la serie di articoli sull'architettura grafica per una spiegazione dettagliata di come il framework Android disegna sullo schermo.

Questa sezione elenca i possibili compiti a casa per gli studenti che seguono questo codelab nell'ambito di un corso guidato da un insegnante. Spetta all'insegnante:

  • Assegna i compiti, se richiesto.
  • Comunica agli studenti come inviare i compiti.
  • Valuta i compiti a casa.

Gli insegnanti possono utilizzare questi suggerimenti nella misura che ritengono opportuna e sono liberi di assegnare qualsiasi altro compito a casa che ritengono appropriato.

Se stai seguendo questo codelab in autonomia, sentiti libero di utilizzare questi compiti per casa per mettere alla prova le tue conoscenze.

Rispondi a queste domande

Domanda 1

Quale metodo chiami per escludere in modo efficiente il disegno delle forme?

excludeFromDrawing()

quickReject()

onDraw()

clipRect()

Domanda 2

Canvas.save() e Canvas.restore() salvano e ripristinano quali informazioni?

▢ Colore, spessore della linea e così via.

▢ Solo trasformazioni attuali

▢ Trasformazioni e regione di ritaglio attuali

▢ Solo regione di ritaglio attuale

Domanda 3

Paint.Align specifica:

▢ Come allineare le seguenti forme di disegno

▢ Da quale lato dell'origine viene estratto il testo

▢ Dove è allineato nella regione di ritaglio

▢ Quale lato del testo allineare all'origine

Per i link ad altri codelab di questo corso, consulta la pagina di destinazione dei codelab Advanced Android in Kotlin.