Slide 1

Slide 1 text

Tips for Building Custom Views on Android with Canvas APIs @riggaroo Rebecca Franks madewithover.com

Slide 2

Slide 2 text

Tips for using Canvas APIs @riggaroo Rebecca Franks madewithover.com

Slide 3

Slide 3 text

What is a Custom View?

Slide 4

Slide 4 text

What? UI Component not part of the “stock standard” controls. When? Standard Android Framework Views don’t have what you need Want a grouping of views for easier use

Slide 5

Slide 5 text

Compound View (Group of Views) Extend a ViewGroup (ie LinearLayout, ConstraintLayout etc) Override one or two things / group a bunch of existing components together Difficulty Level: Intermediate

Slide 6

Slide 6 text

Compound View class CompoundView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : LinearLayout(context, attrs, defStyleAttr) { init { inflate(context, R.layout.compound_view_example, this) } }

Slide 7

Slide 7 text

Fully Custom Component Manually draw the component on screen Handle your own Gestures/Touch events etc Difficulty Level: PRO

Slide 8

Slide 8 text

Fully Custom Component class CustomView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { // Called when the view should render its content. override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) // DRAW STUFF HERE } }

Slide 9

Slide 9 text

What is a Canvas?

Slide 10

Slide 10 text

Blank Space to draw onto

Slide 11

Slide 11 text

Canvas Based on top of SkCanvas - SKIA https://skia.org/ Most methods are “similar” on other platforms - ie Web has a element

Slide 12

Slide 12 text

Canvas - Co-ordinate System Top left is [0,0]- the starting point of that Canvas. All elements are placed relative to [0,0] Uses pixel values - not dp New Canvas commands draw over previously drawn items 0 X axis Y axis 0 y x width height

Slide 13

Slide 13 text

How do I use a Canvas? You need 4 things: 1. Bitmap - to hold the pixels 2. Canvas - host drawing calls 3. Draw Commands - describe what to draw 4. Paint - describe how to draw

Slide 14

Slide 14 text

What can I draw on a Canvas?

Slide 15

Slide 15 text

Canvas#drawBitmap(…) canvas.drawBitmap(bitmap, null, destRect, paint)

Slide 16

Slide 16 text

Canvas#drawBitmap(…) Can be quite confusing & easy to stretch a bitmap - Make sure RectF is correct - Normalise your canvas values

Slide 17

Slide 17 text

Other Canvas APIs drawRect(pixelPointRect, whitePaint) drawCircle(x, y, SIZE_MAGNIFIER, paint)

Slide 18

Slide 18 text

Paint

Slide 19

Slide 19 text

Paint Holds style and colour information for drawing text, bitmaps, paths etc. private val textPaint = Paint().apply { isAntiAlias = true color = Color.RED style = Paint.Style.STROKE }

Slide 20

Slide 20 text

Paint Typeface information, shadowLayer information private val textPaint = Paint().apply { isAntiAlias = true textSize = fontSize letterSpacing = letterSpace typeface = newTypeface setShadowLayer(blurValue, x, y, Color.BLACK) }

Slide 21

Slide 21 text

Paint with BitmapShader

Slide 22

Slide 22 text

Paint with BitmapShader Draw parts of a bitmap val bitmapShader = BitmapShader(backingBitmap, TileMode.CLAMP, TileMode.CLAMP) val dropperPaint = Paint().apply { shader = bitmapShader } drawingMatrix.postScale(SCALE, SCALE, point.x, point.y) dropperPaint.shader?.setLocalMatrix(drawingMatrix) //.. in onDraw drawCircle(point.x, point.y, SIZE_OF_MAGNIFIER, dropperPaint)

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

Tip: Use KTX for cleaner code

Slide 25

Slide 25 text

implementation "androidx.core:core-ktx:1.2.0-beta01" Core KTX is a set of common extensions provided by the Android Team at Google. What is KTX?

Slide 26

Slide 26 text

canvas.save() canvas.translate(200f, 300f) canvas.drawCircle(...) // drawn on the translated canvas canvas.restore() Without KTX

Slide 27

Slide 27 text

canvas.withTranslate(200f, 300f) { drawCircle(...) } With KTX

Slide 28

Slide 28 text

val translateCheckpoint = canvas.save() canvas.translate(200f, 300f) canvas.drawCircle(150f, 150f, RADIUS, circlePaint) val rotateCheckpoint = canvas.save() canvas.rotate(45f) canvas.drawRect(rect, rectPaint) canvas.restoreToCount(rotateCheckpoint) canvas.restoreToCount(translateCheckpoint) Without KTX

Slide 29

Slide 29 text

canvas.withTranslation(200f, 300f) { drawCircle(150f, 150f, RADIUS, circlePaint) withRotation(45f) { drawRect(rect, rectPaint) } } With KTX

Slide 30

Slide 30 text

Tip: Use Matrices to make Canvas Operations Easier

Slide 31

Slide 31 text

Matrix 3*3 matrix for transforming canvas or co-ordinates - Scale, Skew, Rotate, Translate, Perspective

Slide 32

Slide 32 text

Matrix val newMatrix = Matrix() // onDraw… newMatrix.postRotate(20f) canvas.withMatrix(newMatrix) { drawBitmap(bitmap, null, rect, paint) }

Slide 33

Slide 33 text

Perspective drawing matrix.setPolyToPoly(src, 0, dst, 0, points) canvas.withMatrix(newMatrix) { drawBitmap(bitmap, null, rect, paint) }

Slide 34

Slide 34 text

val src = floatArrayOf( 0f, 0f, // top left point width, 0f, // top right point width, height, // bottom right point 0f, height // bottom left point ) val dst = floatArrayOf( 50f, -200f, // top left point width, -200f, // top right point width, height +200f, // bottom right point 0f, height // bottom left point ) Perspective drawing

Slide 35

Slide 35 text

Tip: How to work with two Coordinate Systems

Slide 36

Slide 36 text

Working with sizes and points is difficult Measured Width of View = 1080 Measured Height of View = 2180 My data class sizes are in the range: 0 - 100 or 0 to Custom Size All points inside are also in that range.

Slide 37

Slide 37 text

Scale/Translate Canvas early on in rendering Measured Width of View = 1080 Measured Height of View = 2180 val (scale, x, y) = size.fitCenter(canvas.size()) canvas.withTranslation(x, y) { withScale(scale, scale) { // Canvas at this point will now // be in your coordinate system } }

Slide 38

Slide 38 text

Mapping points between two co-ordinate systems fun mapPoint(point: Point): Point { computeMatrix.reset() computeMatrix.postTranslate(20f, 20f) computeMatrix.postRotate(20f, x, y) val arrayPoint = floatArrayOf(point.x, point.y) computeMatrix.mapPoints(arrayPoint) return Point(arrayPoint[0], arrayPoint[1]) }

Slide 39

Slide 39 text

Tip: Check that the Canvas APIs you are using work across different API versions

Slide 40

Slide 40 text

Paint#setShadowLayer Works on drawText() Weird results with drawBitmap() and hardware acceleration Doesn’t work 100% with drawPath() paint.setShadowLayer(20f, 200f, 200f, Color.BLACK)

Slide 41

Slide 41 text

Paint#setShadowLayer API 26 (Hardware acc) API 28 - Hardware acc API 26 - Software acc

Slide 42

Slide 42 text

Difference in Hardware / Software Rendering Creating a Canvas offscreen - always software rendered On screen Canvas - choose hardware or software To disable HA on a view (you probably don’t want to do this): init { setLayerType(LAYER_TYPE_SOFTWARE,null) }

Slide 43

Slide 43 text

https://developer.android.com/guide/topics/graphics/hardware-accel.html Supported hardware methods from API level

Slide 44

Slide 44 text

Test your Canvas code on all different API levels ✅

Slide 45

Slide 45 text

When should you use a Custom View / Fragment / ?

Slide 46

Slide 46 text

Fragment Whole logical component in your app that is mostly standalone Don’t need custom drawing Using Built in components Don’t really need callbacks to other parts (workarounds with ViewModels but not great) When built in components don’t cut it - ie need a Canvas drawing, Custom Gesture Handling Encapsulate logic and expose a callback listener to user of a component Don’t want users to be concerned of the internals Custom View Literally just bunch of layouts grouped in a file Need each individual component available in layout No custom logic around touches / drawing etc Manually handle each components logic yourself

Slide 47

Slide 47 text

Looking to the Future: Jetpack Compose?

Slide 48

Slide 48 text

Jetpack Compose • Declarative UI toolkit for Android • Inspired by React, Flutter, Vue… • Pre-alpha - ie not production ready • Unbundled from platform

Slide 49

Slide 49 text

Jetpack Compose import androidx.compose.* import androidx.ui.core.* @Composable fun Greeting(name: String) { Text ("Hello $name!") }

Slide 50

Slide 50 text

Canvas with Jetpack Compose @Composable fun DrawRectangle(color: Color) { val paint = Paint() paint.color = color Draw { canvas, parentSize -> canvas.drawRect(parentSize.toRect(), paint) } }

Slide 51

Slide 51 text

Final Thoughts - Normalise your canvas coordinate system - Use Matrix class for easier transformations - Double check that the Canvas features are available - Use KTX - Canvas concepts are not disappearing anytime soon…

Slide 52

Slide 52 text

Thank you! Rebecca Franks @riggaroo