Kotlinize your Canvas!

Kotlinize your Canvas!

Given at 360|AnDev 2018.

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

29517118e397011e21547099c69a3da5?s=128

Joshua Lamson

July 20, 2018
Tweet

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
  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
  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
  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
  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
  6. !6 BarChar | Initialization — Constructor class BarChartView @JvmOverloads constructor(

    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
  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() }
  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() }
  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() }
  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() } }
  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() } }
  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() } }
  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() }
  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() } }
  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() } }
  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() } }
  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
  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
  19. Back to Drawing… !19

  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) } }
  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) } }
  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) } }
  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) } }
  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) } }
  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) } }
  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) } }
  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) } }
  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) } }
  29. !29 BarChart | Canvas Extensions private inline fun <T> Canvas.drawEvenlySpacedBars(inputData:

    Array<T>, 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 } }
  30. !30 BarChart | Canvas Extensions private inline fun <T> Canvas.drawEvenlySpacedBars(inputData:

    Array<T>, 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 } }
  31. !31 BarChart | Canvas Extensions private inline fun <T> Canvas.drawEvenlySpacedBars(inputData:

    Array<T>, 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 } }
  32. !32 BarChart | Canvas Extensions private inline fun <T> Canvas.drawEvenlySpacedBars(inputData:

    Array<T>, 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 } }
  33. !33 BarChart | Canvas Extensions private inline fun <T> Canvas.drawEvenlySpacedBars(inputData:

    Array<T>, 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 } }
  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) } }
  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
  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
  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
  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() }
  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() }
  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() }
  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() }
  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) } }
  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) } }
  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) } }
  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) } }
  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) } }
  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) } }
  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) } }
  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) } }
  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) } }
  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) } }
  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) } }
  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
  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