Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlinize your Canvas!

Kotlinize your Canvas!

Given at 360|AnDev 2018.

Demo Project Bitbucket: https://bit.ly/2uBTaHf

Joshua Lamson

July 20, 2018
Tweet

More Decks by Joshua Lamson

Other Decks in Technology

Transcript

  1. This presentation contains confidential information intended only for the recipient(s) named above. Any other distribution, re-transmission, copying or disclosure of this message is strictly prohibited.
    If you have received this transmission in error, please notify me immediately by telephone or return email, and delete this presentation from your system.
    360|AnDev 2018

    Joshua Lamson
    @darkmoose117
    Kotlinize Your Canvas

    View full-size slide

  2. Why Custom Drawing
    • Reduce View Hierarchy Load
    • Developer Options > Drawing > Show Layout
    Bounds
    • Framework and 3rd Party libs burdened with
    generalization
    • Custom touch adds magic quality
    • Reuse custom/branded design elements
    !2

    View full-size slide

  3. Where do I start?
    WITH PAINT ON THE CANVAS OF COURSE!
    • android.graphics.Canvas
    • An interface for the actual surface being drawn on (Bitmap)
    • Provided to you
    • View.onDraw(Canvas)
    • Drawable.onDraw(Canvas)
    • Canvas.draw*(…)
    • android.graphics.Paint
    • Dictates how things are drawn
    • Color, stoke style, shaders, etc
    • Also text attributes for TextPaint
    !3

    View full-size slide

  4. How do I Kotlinize it?
    • Cleaner initialization patterns
    • Vals, init{}
    • and also constructors still but who cares about those
    • Extension Functions
    • Also named Parameters
    • Also defaults
    • Functional programming
    • Lambdas everywhere!
    !4

    View full-size slide

  5. Example: Bar Chart
    COMPLEX DATA FROM SIMPLE SHAPES
    • Break into discrete chunks
    • Axis / Grid
    • Percent Guidelines
    • Bars
    • What dimens are defined?
    • Padding outside of Axis
    • Spacing between bars
    • Bars themselves fill rest of width
    !5

    View full-size slide

  6. !6
    BarChar | Initialization — Constructor
    class BarChartView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0)
    : View(context, attrs, defStyleAttr) {

    View full-size slide

  7. !7
    BarChar | Initialization — Val initializers
    class BarChartView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0)
    : View(context, attrs, defStyleAttr) {
    // Paints
    private val barPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.FILL
    color = context.getColorCompat(R.color.bar_color)
    }
    private val axisPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.STROKE
    color = context.getColorCompat(R.color.grid_color)
    strokeWidth =
    resources.getDimensionPixelSize(R.dimen.bar_chart_grid_thickness).toFloat()
    }
    private val gridLinePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.STROKE
    color = context.getColorCompat(R.color.guide_color)
    strokeWidth =
    resources.getDimensionPixelSize(R.dimen.bar_chart_guide_thickness).toFloat()
    }

    View full-size slide

  8. !8
    BarChar | Initialization — Val initializers
    class BarChartView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0)
    : View(context, attrs, defStyleAttr) {
    // Paints
    private val barPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.FILL
    color = context.getColorCompat(R.color.bar_color)
    }
    private val axisPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.STROKE
    color = context.getColorCompat(R.color.grid_color)
    strokeWidth =
    resources.getDimensionPixelSize(R.dimen.bar_chart_grid_thickness).toFloat()
    }
    private val gridLinePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.STROKE
    color = context.getColorCompat(R.color.guide_color)
    strokeWidth =
    resources.getDimensionPixelSize(R.dimen.bar_chart_guide_thickness).toFloat()
    }

    View full-size slide

  9. !9
    BarChar | Initialization — Val initializers
    class BarChartView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0)
    : View(context, attrs, defStyleAttr) {
    // Paints
    private val barPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.FILL
    color = context.getColorCompat(R.color.bar_color)
    }
    private val axisPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.STROKE
    color = context.getColorCompat(R.color.grid_color)
    strokeWidth =
    resources.getDimensionPixelSize(R.dimen.bar_chart_grid_thickness).toFloat()
    }
    private val gridLinePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.STROKE
    color = context.getColorCompat(R.color.guide_color)
    strokeWidth =
    resources.getDimensionPixelSize(R.dimen.bar_chart_guide_thickness).toFloat()
    }

    View full-size slide

  10. !10
    BarChar | Initialization — Val initializers
    class BarChartView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0)
    : View(context, attrs, defStyleAttr) {
    private var animatingFraction: Float = 1f
    // Animator
    private val animator = ValueAnimator().apply {
    interpolator = AccelerateInterpolator()
    duration = 500
    addUpdateListener { animator ->
    // Get our float from the animation. This method returns the Interpolated float.
    animatingFraction = animator.animatedFraction
    // MUST CALL THIS to ensure View re-draws;
    invalidate()
    }
    }

    View full-size slide

  11. !11
    BarChar | Initialization — Val initializers
    class BarChartView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0)
    : View(context, attrs, defStyleAttr) {
    private var animatingFraction: Float = 1f
    // Animator
    private val animator = ValueAnimator().apply {
    interpolator = AccelerateInterpolator()
    duration = 500
    addUpdateListener { animator ->
    // Get our float from the animation. This method returns the Interpolated float.
    animatingFraction = animator.animatedFraction
    // MUST CALL THIS to ensure View re-draws;
    invalidate()
    }
    }

    View full-size slide

  12. !12
    BarChar | Initialization — Val initializers
    class BarChartView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0)
    : View(context, attrs, defStyleAttr) {
    private var animatingFraction: Float = 1f
    // Animator
    private val animator = ValueAnimator().apply {
    interpolator = AccelerateInterpolator()
    duration = 500
    addUpdateListener { animator ->
    // Get our float from the animation. This method returns the Interpolated float.
    animatingFraction = animator.animatedFraction
    // MUST CALL THIS to ensure View re-draws;
    invalidate()
    }
    }

    View full-size slide

  13. !13
    BarChar | Initialization — Val initializers
    class BarChartView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0)
    : View(context, attrs, defStyleAttr) {
    private var animatingFraction: Float = 1f
    // Animator
    private val animator = ValueAnimator().apply {
    interpolator = AccelerateInterpolator()
    duration = 500
    addUpdateListener { animator ->
    // Get our float from the animation. This method returns the Interpolated float.
    animatingFraction = animator.animatedFraction
    // MUST CALL THIS to ensure View re-draws;
    invalidate()
    }
    }
    // Later on Button Click
    animator.apply {
    setFloatValues(0f, 1f)
    start()
    }

    View full-size slide

  14. !14
    BarChar | Initialization — init { }
    class BarChartView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0)
    : View(context, attrs, defStyleAttr) {
    // Dimens
    private var columnSpacing: Float = 0f
    private var padding: Float = 0f
    init {
    context.withStyledAttributes(attrs, R.styleable.BarChartView) {
    columnSpacing = getDimensionPixelOffset(
    R.styleable.BarChartView_android_spacing,
    Math.round(2 * resources.displayMetrics.density)).toFloat()
    padding = getDimensionPixelOffset(
    R.styleable.BarChartView_android_padding, 0).toFloat()
    }
    }

    View full-size slide

  15. !15
    BarChar | Initialization — init { }
    class BarChartView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0)
    : View(context, attrs, defStyleAttr) {
    // Dimens
    private var columnSpacing: Float = 0f
    private var padding: Float = 0f
    init {
    context.withStyledAttributes(attrs, R.styleable.BarChartView) {
    columnSpacing = getDimensionPixelOffset(
    R.styleable.BarChartView_android_spacing,
    Math.round(2 * resources.displayMetrics.density)).toFloat()
    padding = getDimensionPixelOffset(
    R.styleable.BarChartView_android_padding, 0).toFloat()
    }
    }

    View full-size slide

  16. !16
    BarChar | Initialization — init { }
    class BarChartView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0)
    : View(context, attrs, defStyleAttr) {
    // Dimens
    private var columnSpacing: Float = 0f
    private var padding: Float = 0f
    init {
    context.withStyledAttributes(attrs, R.styleable.BarChartView) {
    columnSpacing = getDimensionPixelOffset(
    R.styleable.BarChartView_android_spacing,
    Math.round(2 * resources.displayMetrics.density)).toFloat()
    padding = getDimensionPixelOffset(
    R.styleable.BarChartView_android_padding, 0).toFloat()
    }
    }

    View full-size slide

  17. Initialization Order
    A LITTLE WEIRD, BUT PREDICTABLE
    • With new init pattern, order matters
    • First Constructor Arguments (Child then Parent)
    • Parent Initializers in order (init {} and vals)
    • Parent Constructor
    • Child Initializers in order (init {} and vals)
    • Child Constructor
    • .apply { } makes for clean setup
    !17

    View full-size slide

  18. Extension Functions
    EXTENDING A CLASS WITHOUT INHERITANCE
    • Adding functionality without extending
    @ColorInt inline fun Context.getColorCompat(@ColorRes colorRes: Int) : Int {
    return ContextCompat.getColor(this, colorRes)
    }
    • Scoped into the object you’re extending
    • Similar to apply (this)
    • Android KTX
    • From Jake Wharton’s Google I/O 2018 Talk
    • Personal Opinion:
    • For your own code, GO NUTS
    !18

    View full-size slide

  19. Back to Drawing…
    !19

    View full-size slide

  20. !20
    BarChart | onDraw(…)
    override fun onDraw(canvas: Canvas) {
    grid.set(padding, padding, width - padding, height - padding)
    canvas.apply {
    drawHorizontalGridLines(
    numberOfGridLines = 10,
    left = grid.left,
    right = grid.right,
    paint = gridLinePaint) { index ->
    val gridSpacing = grid.height() / 10f
    grid.top + index * gridSpacing
    }
    drawEvenlySpacedBars(
    inputData = dummyData,
    gridBounds = grid,
    columnSpacing = columnSpacing,
    paint = barPaint) {
    it.value * animatingFraction
    }
    drawBottomLeftAxis(
    gridBounds = grid,
    paint = axisPaint)
    }
    }

    View full-size slide

  21. !21
    BarChart | onDraw(…)
    override fun onDraw(canvas: Canvas) {
    grid.set(padding, padding, width - padding, height - padding)
    canvas.apply {
    drawHorizontalGridLines(
    numberOfGridLines = 10,
    left = grid.left,
    right = grid.right,
    paint = gridLinePaint) { index ->
    val gridSpacing = grid.height() / 10f
    grid.top + index * gridSpacing
    }
    drawEvenlySpacedBars(
    inputData = dummyData,
    gridBounds = grid,
    columnSpacing = columnSpacing,
    paint = barPaint) {
    it.value * animatingFraction
    }
    drawBottomLeftAxis(
    gridBounds = grid,
    paint = axisPaint)
    }
    }

    View full-size slide

  22. !22
    BarChart | onDraw(…)
    override fun onDraw(canvas: Canvas) {
    grid.set(padding, padding, width - padding, height - padding)
    canvas.apply {
    drawHorizontalGridLines(
    numberOfGridLines = 10,
    left = grid.left,
    right = grid.right,
    paint = gridLinePaint) { index ->
    val gridSpacing = grid.height() / 10f
    grid.top + index * gridSpacing
    }
    drawEvenlySpacedBars(
    inputData = dummyData,
    gridBounds = grid,
    columnSpacing = columnSpacing,
    paint = barPaint) {
    it.value * animatingFraction
    }
    drawBottomLeftAxis(
    gridBounds = grid,
    paint = axisPaint)
    }
    }

    View full-size slide

  23. !23
    BarChart | onDraw(…)
    override fun onDraw(canvas: Canvas) {
    grid.set(padding, padding, width - padding, height - padding)
    canvas.apply {
    drawHorizontalGridLines(
    numberOfGridLines = 10,
    left = grid.left,
    right = grid.right,
    paint = gridLinePaint) { index ->
    val gridSpacing = grid.height() / 10f
    grid.top + index * gridSpacing
    }
    drawEvenlySpacedBars(
    inputData = dummyData,
    gridBounds = grid,
    columnSpacing = columnSpacing,
    paint = barPaint) {
    it.value * animatingFraction
    }
    drawBottomLeftAxis(
    gridBounds = grid,
    paint = axisPaint)
    }
    }

    View full-size slide

  24. !24
    BarChart | Canvas Extensions
    private inline fun Canvas.drawHorizontalGridLines(numberOfGridLines: Int,
    left: Float, right: Float,
    paint: Paint,
    heightForIndex: (Int) -> Float) {
    var y: Float
    for (i in 0 until numberOfGridLines) {
    y = heightForIndex(i)
    drawLine(left, y, right, y, paint)
    }
    }

    View full-size slide

  25. !25
    BarChart | Canvas Extensions
    private inline fun Canvas.drawHorizontalGridLines(numberOfGridLines: Int,
    left: Float, right: Float,
    paint: Paint,
    heightForIndex: (Int) -> Float) {
    var y: Float
    for (i in 0 until numberOfGridLines) {
    y = heightForIndex(i)
    drawLine(left, y, right, y, paint)
    }
    }

    View full-size slide

  26. !26
    BarChart | Canvas Extensions
    private inline fun Canvas.drawHorizontalGridLines(numberOfGridLines: Int,
    left: Float, right: Float,
    paint: Paint,
    heightForIndex: (Int) -> Float) {
    var y: Float
    for (i in 0 until numberOfGridLines) {
    y = heightForIndex(i)
    drawLine(left, y, right, y, paint)
    }
    }

    View full-size slide

  27. !27
    BarChart | Canvas Extensions
    private inline fun Canvas.drawHorizontalGridLines(numberOfGridLines: Int,
    left: Float, right: Float,
    paint: Paint,
    heightForIndex: (Int) -> Float) {
    var y: Float
    for (i in 0 until numberOfGridLines) {
    y = heightForIndex(i)
    drawLine(left, y, right, y, paint)
    }
    }

    View full-size slide

  28. !28
    BarChart | onDraw(…)
    override fun onDraw(canvas: Canvas) {
    grid.set(padding, padding, width - padding, height - padding)
    canvas.apply {
    drawHorizontalGridLines(
    numberOfGridLines = 10,
    left = grid.left,
    right = grid.right,
    paint = gridLinePaint) { index ->
    val gridSpacing = grid.height() / 10f
    grid.top + index * gridSpacing
    }
    drawEvenlySpacedBars(
    inputData = dummyData,
    gridBounds = grid,
    columnSpacing = columnSpacing,
    paint = barPaint) {
    it.value * animatingFraction
    }
    drawBottomLeftAxis(
    gridBounds = grid,
    paint = axisPaint)
    }
    }

    View full-size slide

  29. !29
    BarChart | Canvas Extensions
    private inline fun Canvas.drawEvenlySpacedBars(inputData: Array,
    gridBounds: RectF,
    columnSpacing: Float = 0f,
    paint: Paint,
    fractionHeightForData: (T) -> Float) {
    val totalHorizontalSpacing = columnSpacing * (inputData.size + 1)
    val barWidth = (gridBounds.width() - totalHorizontalSpacing) / inputData.size
    var barLeft = gridBounds.left + columnSpacing
    var barRight = barLeft + barWidth
    for (datum in inputData) {
    // Figure out top of column based on INVERSE of percentage. Bigger the percentage,
    // the smaller top is, since 100% goes to 0.
    val top = gridBounds.top + gridBounds.height() * (1f - fractionHeightForData(datum))
    drawRect(barLeft, top, barRight, grid.bottom, paint)
    // Shift over left/right column bounds
    barLeft += columnSpacing + barWidth
    barRight += columnSpacing + barWidth
    }
    }

    View full-size slide

  30. !30
    BarChart | Canvas Extensions
    private inline fun Canvas.drawEvenlySpacedBars(inputData: Array,
    gridBounds: RectF,
    columnSpacing: Float = 0f,
    paint: Paint,
    fractionHeightForData: (T) -> Float) {
    val totalHorizontalSpacing = columnSpacing * (inputData.size + 1)
    val barWidth = (gridBounds.width() - totalHorizontalSpacing) / inputData.size
    var barLeft = gridBounds.left + columnSpacing
    var barRight = barLeft + barWidth
    for (datum in inputData) {
    // Figure out top of column based on INVERSE of percentage. Bigger the percentage,
    // the smaller top is, since 100% goes to 0.
    val top = gridBounds.top + gridBounds.height() * (1f - fractionHeightForData(datum))
    drawRect(barLeft, top, barRight, grid.bottom, paint)
    // Shift over left/right column bounds
    barLeft += columnSpacing + barWidth
    barRight += columnSpacing + barWidth
    }
    }

    View full-size slide

  31. !31
    BarChart | Canvas Extensions
    private inline fun Canvas.drawEvenlySpacedBars(inputData: Array,
    gridBounds: RectF,
    columnSpacing: Float = 0f,
    paint: Paint,
    fractionHeightForData: (T) -> Float) {
    val totalHorizontalSpacing = columnSpacing * (inputData.size + 1)
    val barWidth = (gridBounds.width() - totalHorizontalSpacing) / inputData.size
    var barLeft = gridBounds.left + columnSpacing
    var barRight = barLeft + barWidth
    for (datum in inputData) {
    // Figure out top of column based on INVERSE of percentage. Bigger the percentage,
    // the smaller top is, since 100% goes to 0.
    val top = gridBounds.top + gridBounds.height() * (1f - fractionHeightForData(datum))
    drawRect(barLeft, top, barRight, grid.bottom, paint)
    // Shift over left/right column bounds
    barLeft += columnSpacing + barWidth
    barRight += columnSpacing + barWidth
    }
    }

    View full-size slide

  32. !32
    BarChart | Canvas Extensions
    private inline fun Canvas.drawEvenlySpacedBars(inputData: Array,
    gridBounds: RectF,
    columnSpacing: Float = 0f,
    paint: Paint,
    fractionHeightForData: (T) -> Float) {
    val totalHorizontalSpacing = columnSpacing * (inputData.size + 1)
    val barWidth = (gridBounds.width() - totalHorizontalSpacing) / inputData.size
    var barLeft = gridBounds.left + columnSpacing
    var barRight = barLeft + barWidth
    for (datum in inputData) {
    // Figure out top of column based on INVERSE of percentage. Bigger the percentage,
    // the smaller top is, since 100% goes to 0.
    val top = gridBounds.top + gridBounds.height() * (1f - fractionHeightForData(datum))
    drawRect(barLeft, top, barRight, grid.bottom, paint)
    // Shift over left/right column bounds
    barLeft += columnSpacing + barWidth
    barRight += columnSpacing + barWidth
    }
    }

    View full-size slide

  33. !33
    BarChart | Canvas Extensions
    private inline fun Canvas.drawEvenlySpacedBars(inputData: Array,
    gridBounds: RectF,
    columnSpacing: Float = 0f,
    paint: Paint,
    fractionHeightForData: (T) -> Float) {
    val totalHorizontalSpacing = columnSpacing * (inputData.size + 1)
    val barWidth = (gridBounds.width() - totalHorizontalSpacing) / inputData.size
    var barLeft = gridBounds.left + columnSpacing
    var barRight = barLeft + barWidth
    for (datum in inputData) {
    // Figure out top of column based on INVERSE of percentage. Bigger the percentage,
    // the smaller top is, since 100% goes to 0.
    val top = gridBounds.top + gridBounds.height() * (1f - fractionHeightForData(datum))
    drawRect(barLeft, top, barRight, grid.bottom, paint)
    // Shift over left/right column bounds
    barLeft += columnSpacing + barWidth
    barRight += columnSpacing + barWidth
    }
    }

    View full-size slide

  34. !34
    BarChart | onDraw(…)
    override fun onDraw(canvas: Canvas) {
    grid.set(padding, padding, width - padding, height - padding)
    canvas.apply {
    drawHorizontalGridLines(
    numberOfGridLines = 10,
    left = grid.left,
    right = grid.right,
    paint = gridLinePaint) { index ->
    val gridSpacing = grid.height() / 10f
    grid.top + index * gridSpacing
    }
    drawEvenlySpacedBars(
    inputData = dummyData,
    gridBounds = grid,
    columnSpacing = columnSpacing,
    paint = barPaint) {
    it.value * animatingFraction
    }
    drawBottomLeftAxis(
    gridBounds = grid,
    paint = axisPaint)
    }
    }

    View full-size slide

  35. Extension Functions and Lambdas
    • Enables good “sugar”
    • Taken to the extreme, you can write a Domain Specific Language (DSL)
    • When function is last argument can declare as block outside parens
    • Familiar from scoping functions

    • Very useful for wrapping logic in calls before/after it runs
    • canvas.save/restore()
    • Use inline to avoid object creation for Lambdas
    • Thanks Segun!
    !35

    View full-size slide

  36. Transforming the Canvas
    NOW MUCH EASIER WITH KOTLIN
    • You can’t provide an x/y to your Text Layouts or Paths, so you must move the canvas
    • Can do anything you could do to a Matrix
    • Rotate, Translate, Scale, Skew
    • Must always transform the Canvas back
    • canvas.save() & canvas.restore()
    • Can be saved multiple times & restored to certain save points
    • I think of Stamp/3D Printer. Base moved, paint stays stationary
    !36

    View full-size slide

  37. Example: Flying Heart
    SOMETHING MORE ANIMATED
    • Draws a heart Path that follows the user’s touch
    • “Wobbles” Heart as it’s shown
    • Rotation from -30° to 30°
    • While Canvas.drawShapes have positions, drawPath
    doesn’t
    • Transforms!
    !37

    View full-size slide

  38. !38
    Flying Heart | Initialization
    // . . .
    private val heartSize: Float =
    resources.getDimensionPixelSize(R.dimen.heart_size).toFloat()
    private val heartPath: Path = HeartPathHelper.getHeartOfSize(heartSize.roundToInt())
    // Rotation Animation
    private val rotationAnimator: ValueAnimator = ValueAnimator().apply {
    duration = 250
    setIntValues(-30, 0, 30)
    repeatCount = ValueAnimator.INFINITE
    repeatMode = ValueAnimator.REVERSE
    interpolator = AccelerateDecelerateInterpolator()
    addUpdateListener { invalidate() }
    start()
    pause()
    }

    View full-size slide

  39. !39
    Flying Heart | Initialization
    // . . .
    private val heartSize: Float =
    resources.getDimensionPixelSize(R.dimen.heart_size).toFloat()
    private val heartPath: Path = HeartPathHelper.getHeartOfSize(heartSize.roundToInt())
    // Rotation Animation
    private val rotationAnimator: ValueAnimator = ValueAnimator().apply {
    duration = 250
    setIntValues(-30, 0, 30)
    repeatCount = ValueAnimator.INFINITE
    repeatMode = ValueAnimator.REVERSE
    interpolator = AccelerateDecelerateInterpolator()
    addUpdateListener { invalidate() }
    start()
    pause()
    }

    View full-size slide

  40. !40
    Flying Heart | Initialization
    // . . .
    private val heartSize: Float =
    resources.getDimensionPixelSize(R.dimen.heart_size).toFloat()
    private val heartPath: Path = HeartPathHelper.getHeartOfSize(heartSize.roundToInt())
    // Rotation Animation
    private val rotationAnimator: ValueAnimator = ValueAnimator().apply {
    duration = 250
    setIntValues(-30, 0, 30)
    repeatCount = ValueAnimator.INFINITE
    repeatMode = ValueAnimator.REVERSE
    interpolator = AccelerateDecelerateInterpolator()
    addUpdateListener { invalidate() }
    start()
    pause()
    }

    View full-size slide

  41. !41
    Flying Heart | Initialization
    // . . .
    private val heartSize: Float =
    resources.getDimensionPixelSize(R.dimen.heart_size).toFloat()
    private val heartPath: Path = HeartPathHelper.getHeartOfSize(heartSize.roundToInt())
    // Rotation Animation
    private val rotationAnimator: ValueAnimator = ValueAnimator().apply {
    duration = 250
    setIntValues(-30, 0, 30)
    repeatCount = ValueAnimator.INFINITE
    repeatMode = ValueAnimator.REVERSE
    interpolator = AccelerateDecelerateInterpolator()
    addUpdateListener { invalidate() }
    start()
    pause()
    }

    View full-size slide

  42. !42
    Flying Heart | onTouchEvent(…)
    override fun onTouchEvent(event: MotionEvent): Boolean {
    return when (event.action) {
    MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
    isFingerDown = true
    finger.set(event.x, event.y)
    if (rotationAnimator.isPaused) {
    rotationAnimator.resume()
    }
    invalidate()
    true
    }
    MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
    if (isFingerDown) {
    isFingerDown = false
    rotationAnimator.pause()
    invalidate()
    }
    super.onTouchEvent(event)
    }
    else -> super.onTouchEvent(event)
    }
    }

    View full-size slide

  43. !43
    Flying Heart | onTouchEvent(…)
    override fun onTouchEvent(event: MotionEvent): Boolean {
    return when (event.action) {
    MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
    isFingerDown = true
    finger.set(event.x, event.y)
    if (rotationAnimator.isPaused) {
    rotationAnimator.resume()
    }
    invalidate()
    true
    }
    MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
    if (isFingerDown) {
    isFingerDown = false
    rotationAnimator.pause()
    invalidate()
    }
    super.onTouchEvent(event)
    }
    else -> super.onTouchEvent(event)
    }
    }

    View full-size slide

  44. !44
    Flying Heart | onTouchEvent(…)
    override fun onTouchEvent(event: MotionEvent): Boolean {
    return when (event.action) {
    MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
    isFingerDown = true
    finger.set(event.x, event.y)
    if (rotationAnimator.isPaused) {
    rotationAnimator.resume()
    }
    invalidate()
    true
    }
    MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
    if (isFingerDown) {
    isFingerDown = false
    rotationAnimator.pause()
    invalidate()
    }
    super.onTouchEvent(event)
    }
    else -> super.onTouchEvent(event)
    }
    }

    View full-size slide

  45. !45
    Flying Heart | onTouchEvent(…)
    override fun onTouchEvent(event: MotionEvent): Boolean {
    return when (event.action) {
    MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
    isFingerDown = true
    finger.set(event.x, event.y)
    if (rotationAnimator.isPaused) {
    rotationAnimator.resume()
    }
    invalidate()
    true
    }
    MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
    if (isFingerDown) {
    isFingerDown = false
    rotationAnimator.pause()
    invalidate()
    }
    super.onTouchEvent(event)
    }
    else -> super.onTouchEvent(event)
    }
    }

    View full-size slide

  46. !46
    Flying Heart | onTouchEvent(…)
    override fun onTouchEvent(event: MotionEvent): Boolean {
    return when (event.action) {
    MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
    isFingerDown = true
    finger.set(event.x, event.y)
    if (rotationAnimator.isPaused) {
    rotationAnimator.resume()
    }
    invalidate()
    true
    }
    MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
    if (isFingerDown) {
    isFingerDown = false
    rotationAnimator.pause()
    invalidate()
    }
    super.onTouchEvent(event)
    }
    else -> super.onTouchEvent(event)
    }
    }

    View full-size slide

  47. !47
    Flying Heart | onDraw()
    override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    // If user isn't touching the screen, do nothing.
    if (!isFingerDown) {
    return
    }
    canvas.rotateAndTranslate(
    rotation = (rotationAnimator.animatedValue as Int).toFloat(),
    pivotX = finger.x,
    pivotY = finger.y,
    translationX = finger.x - heartSize / 2f,
    translationY = finger.y - heartSize / 2f) {
    drawPath(heartPath, paint)
    }
    }

    View full-size slide

  48. !48
    Flying Heart | onDraw()
    override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    // If user isn't touching the screen, do nothing.
    if (!isFingerDown) {
    return
    }
    canvas.rotateAndTranslate(
    rotation = (rotationAnimator.animatedValue as Int).toFloat(),
    pivotX = finger.x,
    pivotY = finger.y,
    translationX = finger.x - heartSize / 2f,
    translationY = finger.y - heartSize / 2f) {
    drawPath(heartPath, paint)
    }
    }

    View full-size slide

  49. !49
    Flying Heart | Canvas Transform Extension
    private inline fun Canvas.rotateAndTranslate(rotation: Float = 0f,
    pivotX: Float = 0f,
    pivotY: Float = 0f,
    translationX: Float = 0f,
    translationY: Float = 0f,
    draw: Canvas.() -> Unit) {
    val checkpoint = save()
    rotate(rotation, pivotX, pivotY)
    translate(translationX, translationY)
    try {
    draw()
    } finally {
    restoreToCount(checkpoint)
    }
    }

    View full-size slide

  50. !50
    Flying Heart | Canvas Transform Extension
    private inline fun Canvas.rotateAndTranslate(rotation: Float = 0f,
    pivotX: Float = 0f,
    pivotY: Float = 0f,
    translationX: Float = 0f,
    translationY: Float = 0f,
    draw: Canvas.() -> Unit) {
    val checkpoint = save()
    rotate(rotation, pivotX, pivotY)
    translate(translationX, translationY)
    try {
    draw()
    } finally {
    restoreToCount(checkpoint)
    }
    }

    View full-size slide

  51. !51
    Flying Heart | Canvas Transform Extension
    private inline fun Canvas.rotateAndTranslate(rotation: Float = 0f,
    pivotX: Float = 0f,
    pivotY: Float = 0f,
    translationX: Float = 0f,
    translationY: Float = 0f,
    draw: Canvas.() -> Unit) {
    val checkpoint = save()
    rotate(rotation, pivotX, pivotY)
    translate(translationX, translationY)
    try {
    draw()
    } finally {
    restoreToCount(checkpoint)
    }
    }

    View full-size slide

  52. !52
    Flying Heart | Canvas Transform Extension
    private inline fun Canvas.rotateAndTranslate(rotation: Float = 0f,
    pivotX: Float = 0f,
    pivotY: Float = 0f,
    translationX: Float = 0f,
    translationY: Float = 0f,
    draw: Canvas.() -> Unit) {
    val checkpoint = save()
    rotate(rotation, pivotX, pivotY)
    translate(translationX, translationY)
    try {
    draw()
    } finally {
    restoreToCount(checkpoint)
    }
    }

    View full-size slide

  53. Key Canvas Lessons
    KEEP IT LIGHT
    • Break your view into elements to draw
    • Use reference points to position elements in your View
    • Setup Objects/Values needed up front for quick re-use
    • Keep your draw methods SLIM
    • 16ms is all you get
    • Avoid object allocations
    • Transform your Canvas to position text/paths
    • But ALWAYS save before and restore for others afterwards
    • INVALIDATE() to re-draw!
    !53

    View full-size slide

  54. Questions?
    AND ADDITIONAL READING
    • “Custom Drawing With Canvas” by Joshua Lamson
    • https://bit.ly/2JPV68G
    • “An in-depth look at Kotlin’s initializers” by AJ Alt
    • https://bit.ly/2I73vyU
    • “Android Jetpack: sweetening Kotlin development with Android KTX” by Jake Wharton
    • https://youtu.be/st1XVfkDWqk
    • https://github.com/android/android-ktx
    • Kotlin Language Docs
    • https://kotlinlang.org/docs/reference/extensions.html
    • https://kotlinlang.org/docs/reference/lambdas.html
    • “Mastering Kotlin standard functions: run, with, let, also and apply” by Elye
    • https://bit.ly/2pfS00x
    !54
    Bitbucket Project:
    https://bit.ly/2uBTaHf

    View full-size slide