Mobile Era 2019 - Tips for Building Custom Views on Android with Canvas APIs

Mobile Era 2019 - Tips for Building Custom Views on Android with Canvas APIs

Have you ever wanted to draw something custom beyond the standard views like a Bar Chart or an Advanced Image Viewer? In this talk, we will cover the basics of drawing onto a Canvas to create your own custom view. We will also cover some of the more advanced things you can do with the Canvas, such as using Shaders and Matrices to achieve magical effects.


Rebecca Franks

November 07, 2019


  1. Tips for Building Custom Views on Android with Canvas APIs

    @riggaroo Rebecca Franks
  2. Tips for using Canvas APIs @riggaroo Rebecca Franks

  3. What is a Custom View?

  4. 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
  5. 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
  6. 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) } }
  7. Fully Custom Component Manually draw the component on screen Handle

    your own Gestures/Touch events etc Difficulty Level: PRO
  8. 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 } }
  9. What is a Canvas?

  10. Blank Space to draw onto

  11. Canvas Based on top of SkCanvas - SKIA Most

    methods are “similar” on other platforms - ie Web has a <canvas> element
  12. 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
  13. 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
  14. What can I draw on a Canvas?

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

  16. Canvas#drawBitmap(…) Can be quite confusing & easy to stretch a

    bitmap - Make sure RectF is correct - Normalise your canvas values
  17. Other Canvas APIs drawRect(pixelPointRect, whitePaint) drawCircle(x, y, SIZE_MAGNIFIER, paint)

  18. Paint

  19. 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 }
  20. Paint Typeface information, shadowLayer information private val textPaint = Paint().apply

    { isAntiAlias = true textSize = fontSize letterSpacing = letterSpace typeface = newTypeface setShadowLayer(blurValue, x, y, Color.BLACK) }
  21. Paint with BitmapShader

  22. 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)
  23. None
  24. Tip: Use KTX for cleaner code

  25. 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?
  26. canvas.translate(200f, 300f) canvas.drawCircle(...) // drawn on the translated canvas

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

  28. val translateCheckpoint = canvas.translate(200f, 300f) canvas.drawCircle(150f, 150f, RADIUS, circlePaint)

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

    rectPaint) } } With KTX
  30. Tip: Use Matrices to make Canvas Operations Easier

  31. Matrix 3*3 matrix for transforming canvas or co-ordinates - Scale,

    Skew, Rotate, Translate, Perspective
  32. Matrix val newMatrix = Matrix() // onDraw… newMatrix.postRotate(20f) canvas.withMatrix(newMatrix) {

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

    null, rect, paint) }
  34. 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
  35. Tip: How to work with two Coordinate Systems

  36. 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.
  37. 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 } }
  38. 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]) }
  39. Tip: Check that the Canvas APIs you are using work

    across different API versions
  40. 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)
  41. Paint#setShadowLayer API 26 (Hardware acc) API 28 - Hardware acc

    API 26 - Software acc
  42. 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) }
  43. Supported hardware methods from API level

  44. Test your Canvas code on all different API levels ✅

  45. When should you use a Custom View / Fragment /

  46. 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 <include> 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
  47. Looking to the Future: Jetpack Compose?

  48. Jetpack Compose • Declarative UI toolkit for Android • Inspired

    by React, Flutter, Vue… • Pre-alpha - ie not production ready • Unbundled from platform
  49. Jetpack Compose import androidx.compose.* import androidx.ui.core.* @Composable fun Greeting(name: String)

    { Text ("Hello $name!") }
  50. Canvas with Jetpack Compose @Composable fun DrawRectangle(color: Color) { val

    paint = Paint() paint.color = color Draw { canvas, parentSize -> canvas.drawRect(parentSize.toRect(), paint) } }
  51. 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…
  52. Thank you! Rebecca Franks @riggaroo