Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

!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() }

Slide 8

Slide 8 text

!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() }

Slide 9

Slide 9 text

!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() }

Slide 10

Slide 10 text

!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() } }

Slide 11

Slide 11 text

!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() } }

Slide 12

Slide 12 text

!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() } }

Slide 13

Slide 13 text

!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() }

Slide 14

Slide 14 text

!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() } }

Slide 15

Slide 15 text

!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() } }

Slide 16

Slide 16 text

!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() } }

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Back to Drawing… !19

Slide 20

Slide 20 text

!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) } }

Slide 21

Slide 21 text

!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) } }

Slide 22

Slide 22 text

!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) } }

Slide 23

Slide 23 text

!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) } }

Slide 24

Slide 24 text

!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) } }

Slide 25

Slide 25 text

!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) } }

Slide 26

Slide 26 text

!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) } }

Slide 27

Slide 27 text

!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) } }

Slide 28

Slide 28 text

!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) } }

Slide 29

Slide 29 text

!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 } }

Slide 30

Slide 30 text

!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 } }

Slide 31

Slide 31 text

!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 } }

Slide 32

Slide 32 text

!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 } }

Slide 33

Slide 33 text

!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 } }

Slide 34

Slide 34 text

!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) } }

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

!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() }

Slide 39

Slide 39 text

!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() }

Slide 40

Slide 40 text

!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() }

Slide 41

Slide 41 text

!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() }

Slide 42

Slide 42 text

!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) } }

Slide 43

Slide 43 text

!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) } }

Slide 44

Slide 44 text

!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) } }

Slide 45

Slide 45 text

!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) } }

Slide 46

Slide 46 text

!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) } }

Slide 47

Slide 47 text

!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) } }

Slide 48

Slide 48 text

!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) } }

Slide 49

Slide 49 text

!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) } }

Slide 50

Slide 50 text

!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) } }

Slide 51

Slide 51 text

!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) } }

Slide 52

Slide 52 text

!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) } }

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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