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, July 13th 2017
 Joshua Lamson - Android Developer @darkmoose117 Custom Drawing with Canvas

Slide 2

Slide 2 text

Why Custom Drawing? 2

Slide 3

Slide 3 text

Why Custom Drawing? • Reduce View Hierarchy Load • Developer Options > Drawing > Show Layout Bounds • 3rd party chart/graph libraries are way too big • Android Framework burdened with generalizing • Custom touch adds that magic quality • Re-use common elements 3

Slide 4

Slide 4 text

Where do we start? 4

Slide 5

Slide 5 text

A Blank Canvas! LITERALLY. • android.graphics.Canvas • An interface for the actual surface being drawn on (Bitmap) • One of 4 Needed Components to Draw in Android • Bitmap to hold your pixels • Canvas to populate that Bitmap • Something to draw (Shape, Path, Text, other Bitmaps) • and a Paint to describe how to draw those things 5

Slide 6

Slide 6 text

How do you get a Canvas? • Can create one with a Bitmap directly, pretty rare • More likely, it will be provided to you by Android when relevant • View.onDraw(Canvas) • ViewGroup.onDraw(Canvas) • Drawable.draw(Canvas) • RecyclerView.ItemDecoration.onDraw(Canvas, …) 6

Slide 7

Slide 7 text

How do you get to Painting? • You’ll create your own Paint objects • Every Paint should have a Color • Can have so much more • Paint.ANTI_ALIAS_FLAG • Text specific stuff (Text Paint) • Stroke/Fill behaviour • Shaders 7

Slide 8

Slide 8 text

Navigating the Canvas (0, 0) IS TOP-LEFT • Canvas has its own coordinates • Position of thing it’s in doesn’t matter • Some methods use rotational coordinates • 0º/360º is at 3 o’clock • Also needs a radius • Arcs always drawn clockwise 8 Image Src: https://cdn.tutsplus.com/net/uploads/legacy/916_canvas1/1.jpg

Slide 9

Slide 9 text

Example: Bar Chart 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 9

Slide 10

Slide 10 text

10 Bar Chart | Initializing Paint Objects // in Constructor mBarPaint = new Paint();
 mBarPaint.setStyle(Paint.Style.FILL);
 mBarPaint.setColor(barColor);
 
 mGridPaint = new Paint();
 mGridPaint.setStyle(Paint.Style.STROKE);
 mGridPaint.setColor(gridColor);
 mGridPaint.setStrokeWidth(gridThicknessInPx);
 
 mGuidelinePaint = new Paint();
 mGuidelinePaint.setStyle(Paint.Style.STROKE);
 mGuidelinePaint.setColor(guidelineColor);
 mGuidelinePaint.setStrokeWidth(guidelineThicknessInPx);

Slide 11

Slide 11 text

11 Bar Chart | Initializing Paint Objects // in Constructor mBarPaint = new Paint();
 mBarPaint.setStyle(Paint.Style.FILL);
 mBarPaint.setColor(barColor);
 
 mGridPaint = new Paint();
 mGridPaint.setStyle(Paint.Style.STROKE);
 mGridPaint.setColor(gridColor);
 mGridPaint.setStrokeWidth(gridThicknessInPx);
 
 mGuidelinePaint = new Paint();
 mGuidelinePaint.setStyle(Paint.Style.STROKE);
 mGuidelinePaint.setColor(guidelineColor);
 mGuidelinePaint.setStrokeWidth(guidelineThicknessInPx);

Slide 12

Slide 12 text

12 Bar Chart | Initializing Paint Objects // in Constructor mBarPaint = new Paint();
 mBarPaint.setStyle(Paint.Style.FILL);
 mBarPaint.setColor(barColor);
 
 mGridPaint = new Paint();
 mGridPaint.setStyle(Paint.Style.STROKE);
 mGridPaint.setColor(gridColor);
 mGridPaint.setStrokeWidth(gridThicknessInPx);
 
 mGuidelinePaint = new Paint();
 mGuidelinePaint.setStyle(Paint.Style.STROKE);
 mGuidelinePaint.setColor(guidelineColor);
 mGuidelinePaint.setStrokeWidth(guidelineThicknessInPx);

Slide 13

Slide 13 text

13 Bar Chart | Override onDraw(Canvas) @Override
 protected void onDraw(Canvas canvas) {
 final int height = getHeight();
 final int width = getWidth();
 final float gridLeft = mPadding;
 final float gridBottom = height - mPadding;
 final float gridTop = mPadding;
 final float gridRight = width - mPadding; ... }

Slide 14

Slide 14 text

14 Bar Chart | Override onDraw(Canvas) @Override
 protected void onDraw(Canvas canvas) {
 final int height = getHeight();
 final int width = getWidth();
 final float gridLeft = mPadding;
 final float gridBottom = height - mPadding;
 final float gridTop = mPadding;
 final float gridRight = width - mPadding; ... }

Slide 15

Slide 15 text

15 Bar Chart | Override onDraw(Canvas) @Override
 protected void onDraw(Canvas canvas) {
 final int height = getHeight();
 final int width = getWidth();
 final float gridLeft = mPadding;
 final float gridBottom = height - mPadding;
 final float gridTop = mPadding;
 final float gridRight = width - mPadding; ... }

Slide 16

Slide 16 text

16 Bar Chart | Override onDraw(Canvas) @Override
 protected void onDraw(Canvas canvas) {
 ... // Draw Grid Lines
 canvas.drawLine(gridLeft, gridBottom, gridLeft, gridTop, mGridPaint);
 canvas.drawLine(gridLeft, gridBottom, gridRight, gridBottom, mGridPaint); ... }

Slide 17

Slide 17 text

17 Bar Chart | Override onDraw(Canvas) @Override
 protected void onDraw(Canvas canvas) {
 ... // Draw Grid Lines
 canvas.drawLine(gridLeft, gridBottom, gridLeft, gridTop, mGridPaint);
 canvas.drawLine(gridLeft, gridBottom, gridRight, gridBottom, mGridPaint); ... }

Slide 18

Slide 18 text

18 Bar Chart | Override onDraw(Canvas) @Override
 protected void onDraw(Canvas canvas) {
 ... // Draw Grid Lines
 canvas.drawLine(gridLeft, gridBottom, gridLeft, gridTop, mGridPaint);
 canvas.drawLine(gridLeft, gridBottom, gridRight, gridBottom, mGridPaint); ... }

Slide 19

Slide 19 text

19 Bar Chart | Override onDraw(Canvas) @Override
 protected void onDraw(Canvas canvas) {
 ... 
 // Draw guide lines
 float guideLineSpacing = (gridBottom - gridTop) / 10f;
 float y;
 for (int i = 0; i < 10; i++) {
 y = gridTop + i * guideLineSpacing;
 canvas.drawLine(gridLeft, y, gridRight, y, mGuidelinePaint);
 } ... }

Slide 20

Slide 20 text

20 Bar Chart | Override onDraw(Canvas) @Override
 protected void onDraw(Canvas canvas) {
 ... 
 // Draw guide lines
 float guideLineSpacing = (gridBottom - gridTop) / 10f;
 float y;
 for (int i = 0; i < 10; i++) {
 y = gridTop + i * guideLineSpacing;
 canvas.drawLine(gridLeft, y, gridRight, y, mGuidelinePaint);
 } ... }

Slide 21

Slide 21 text

21 Bar Chart | Override onDraw(Canvas) @Override
 protected void onDraw(Canvas canvas) {
 ... 
 // Draw guide lines
 float guideLineSpacing = (gridBottom - gridTop) / 10f;
 float y;
 for (int i = 0; i < 10; i++) {
 y = gridTop + i * guideLineSpacing;
 canvas.drawLine(gridLeft, y, gridRight, y, mGuidelinePaint);
 } ... }

Slide 22

Slide 22 text

22 Bar Chart | Override onDraw(Canvas) @Override
 protected void onDraw(Canvas canvas) {
 ... 
 // Draw guide lines
 float guideLineSpacing = (gridBottom - gridTop) / 10f;
 float y;
 for (int i = 0; i < 10; i++) {
 y = gridTop + i * guideLineSpacing;
 canvas.drawLine(gridLeft, y, gridRight, y, mGuidelinePaint);
 } ... }

Slide 23

Slide 23 text

23 Bar Chart | Override onDraw(Canvas) @Override
 protected void onDraw(Canvas canvas) {
 ...
 // Draw Bars
 float totalColumnSpacing = spacing * (dataCount + 1);
 float columnWidth = (gridRight - gridLeft - totalColumnSpacing) / dataCount;
 float columnLeft = gridLeft + spacing;
 float columnRight = columnLeft + columnWidth;
 for (float percentage : data) {
 // Calculate top of column based on percentage.
 float top = gridTop + gridHeight * (1f - percentage);
 canvas.drawRect(columnLeft, top, columnRight, gridBottom, mBarPaint);
 
 // Shift over left/right column bounds
 columnLeft = columnRight + mColumnSpacing;
 columnRight = columnLeft + columnWidth;
 } }

Slide 24

Slide 24 text

24 Bar Chart | Override onDraw(Canvas) @Override
 protected void onDraw(Canvas canvas) {
 ...
 // Draw Bars
 float totalColumnSpacing = spacing * (dataCount + 1);
 float columnWidth = (gridRight - gridLeft - totalColumnSpacing) / dataCount;
 float columnLeft = gridLeft + spacing;
 float columnRight = columnLeft + columnWidth;
 for (float percentage : data) {
 // Calculate top of column based on percentage.
 float top = gridTop + gridHeight * (1f - percentage);
 canvas.drawRect(columnLeft, top, columnRight, gridBottom, mBarPaint);
 
 // Shift over left/right column bounds
 columnLeft = columnRight + mColumnSpacing;
 columnRight = columnLeft + columnWidth;
 } }

Slide 25

Slide 25 text

25 Bar Chart | Override onDraw(Canvas) @Override
 protected void onDraw(Canvas canvas) {
 ...
 // Draw Bars
 float totalColumnSpacing = spacing * (dataCount + 1);
 float columnWidth = (gridRight - gridLeft - totalColumnSpacing) / dataCount;
 float columnLeft = gridLeft + spacing;
 float columnRight = columnLeft + columnWidth;
 for (float percentage : data) {
 // Calculate top of column based on percentage.
 float top = gridTop + gridHeight * (1f - percentage);
 canvas.drawRect(columnLeft, top, columnRight, gridBottom, mBarPaint);
 
 // Shift over left/right column bounds
 columnLeft = columnRight + mColumnSpacing;
 columnRight = columnLeft + columnWidth;
 } }

Slide 26

Slide 26 text

26 Bar Chart | Override onDraw(Canvas) @Override
 protected void onDraw(Canvas canvas) {
 ...
 // Draw Bars
 float totalColumnSpacing = spacing * (dataCount + 1);
 float columnWidth = (gridRight - gridLeft - totalColumnSpacing) / dataCount;
 float columnLeft = gridLeft + spacing;
 float columnRight = columnLeft + columnWidth;
 for (float percentage : data) {
 // Calculate top of column based on percentage.
 float top = gridTop + gridHeight * (1f - percentage);
 canvas.drawRect(columnLeft, top, columnRight, gridBottom, mBarPaint);
 
 // Shift over left/right column bounds
 columnLeft = columnRight + mColumnSpacing;
 columnRight = columnLeft + columnWidth;
 } }

Slide 27

Slide 27 text

27 Bar Chart | Override onDraw(Canvas) @Override
 protected void onDraw(Canvas canvas) {
 ...
 // Draw Bars
 float totalColumnSpacing = spacing * (dataCount + 1);
 float columnWidth = (gridRight - gridLeft - totalColumnSpacing) / dataCount;
 float columnLeft = gridLeft + spacing;
 float columnRight = columnLeft + columnWidth;
 for (float percentage : data) {
 // Calculate top of column based on percentage.
 float top = gridTop + gridHeight * (1f - percentage);
 canvas.drawRect(columnLeft, top, columnRight, gridBottom, mBarPaint);
 
 // Shift over left/right column bounds
 columnLeft = columnRight + mColumnSpacing;
 columnRight = columnLeft + columnWidth;
 } }

Slide 28

Slide 28 text

28 Bar Chart | Override onDraw(Canvas) @Override
 protected void onDraw(Canvas canvas) {
 ...
 // Draw Bars
 float totalColumnSpacing = spacing * (dataCount + 1);
 float columnWidth = (gridRight - gridLeft - totalColumnSpacing) / dataCount;
 float columnLeft = gridLeft + spacing;
 float columnRight = columnLeft + columnWidth;
 for (float percentage : data) {
 // Calculate top of column based on percentage.
 float top = gridTop + gridHeight * (1f - percentage);
 canvas.drawRect(columnLeft, top, columnRight, gridBottom, mBarPaint);
 
 // Shift over left/right column bounds
 columnLeft = columnRight + mColumnSpacing;
 columnRight = columnLeft + columnWidth;
 } }

Slide 29

Slide 29 text

Bar Chart: Reviewing Steps • Create Paint Objects in constructor • Override onDraw(Canvas) • Keep it LIGHT • Draw each component • Axis • Guidelines • Bars 29

Slide 30

Slide 30 text

Extra Credit: Animation mAnimator = new ValueAnimator();
 mAnimator.setDuration(500); mAnimator.setInterpolator(new AccelerateInterpolator());
 mAnimator.addUpdateListener(this); mAnimator.setFloatValues(0f, 1f);
 mAnimator.start(); @Override
 public void onAnimationUpdate(ValueAnimator animation) {
 // Get our interpolated float from the animation.
 mAnimatingFraction = animation.getAnimatedFraction();
 
 // MUST CALL THIS to ensure View re-draws;
 invalidate();
 } 30

Slide 31

Slide 31 text

31 Bar Chart | Override onDraw(Canvas) @Override
 protected void onDraw(Canvas canvas) {
 ...
 // Draw Bars
 float totalColumnSpacing = spacing * (dataCount + 1);
 float columnWidth = (gridRight - gridLeft - totalColumnSpacing) / dataCount;
 float columnLeft = gridLeft + spacing;
 float columnRight = columnLeft + columnWidth;
 for (float percentage : data) {
 // Calculate top of column based on percentage.
 float top = gridTop + gridHeight * (1f - (percentage * mAnimatingFraction)); canvas.drawRect(columnLeft, top, columnRight, gridBottom, mBarPaint);
 
 // Shift over left/right column bounds
 columnLeft = columnRight + mColumnSpacing;
 columnRight = columnLeft + columnWidth;
 } }

Slide 32

Slide 32 text

ViewGroups are Views • setWillNotDraw(false); • Will Not Draw = false means you WILL draw • Has onDraw(Canvas) like View. • Draws BEFORE children • dispatchDraw(Canvas) makes children draw • Need to override to draw over children
 • Also has onDrawForeground(Canvas) added in API 23 32

Slide 33

Slide 33 text

Drawables • These are likely familiar • Drawable PNGs • XML drawables • R.drawable._ and @drawable/_ • All of those return some kind of Drawable • An abstraction for something that can be drawn • All have a draw(Canvas) method • MUST have bounds (may be intrinsic) 33

Slide 34

Slide 34 text

Drawable Methods WHAT YOU NEED TO IMPLEMENT • setBounds(Rect) • onBoundsChange(Rect) • getIntrinsicWidth()/Height() • setAlpha() • setColorFilter() • getOpacity() • PixelFormat.OPAQUE, TRANSPARENT, TRANSLUCENT • draw(Canvas)! 34

Slide 35

Slide 35 text

Example: Letter Drawable • Break into discrete chunks • Colored circle • White letter • What dimens are defined? • Radius of circle • Text size • Text is centered 35

Slide 36

Slide 36 text

36 Letter Drawable | In Constructor public LetterDrawable(Context context, String letter) {
 mLetter = letter;
 
 mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
 mCirclePaint.setColor(iconColor);
 
 mLetterPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
 mLetterPaint.setColor(Color.WHITE);
 mLetterPaint.setTextSize(letterTextSize);
 mLetterPaint.setTextAlign(Paint.Align.CENTER);
 
 // Text draws from the baselineAdd some top padding to center vertically.
 Rect textMathRect = new Rect();
 mLetterPaint.getTextBounds(mLetter, 0, 1, textMathRect);
 mLetterTop = textMathRect.height() / 2f;
 }

Slide 37

Slide 37 text

37 Letter Drawable | In Constructor public LetterDrawable(Context context, String letter) {
 mLetter = letter;
 
 mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
 mCirclePaint.setColor(iconColor);
 
 mLetterPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
 mLetterPaint.setColor(Color.WHITE);
 mLetterPaint.setTextSize(letterTextSize);
 mLetterPaint.setTextAlign(Paint.Align.CENTER);
 
 // Text draws from the baselineAdd some top padding to center vertically.
 Rect textMathRect = new Rect();
 mLetterPaint.getTextBounds(mLetter, 0, 1, textMathRect);
 mLetterTop = textMathRect.height() / 2f;
 }

Slide 38

Slide 38 text

38 Letter Drawable | In Constructor public LetterDrawable(Context context, String letter) {
 mLetter = letter;
 
 mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
 mCirclePaint.setColor(iconColor);
 
 mLetterPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
 mLetterPaint.setColor(Color.WHITE);
 mLetterPaint.setTextSize(letterTextSize);
 mLetterPaint.setTextAlign(Paint.Align.CENTER);
 
 // Text draws from the baselineAdd some top padding to center vertically.
 Rect textMathRect = new Rect();
 mLetterPaint.getTextBounds(mLetter, 0, 1, textMathRect);
 mLetterTop = textMathRect.height() / 2f;
 }

Slide 39

Slide 39 text

39 Letter Drawable | In Constructor public LetterDrawable(Context context, String letter) {
 mLetter = letter;
 
 mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
 mCirclePaint.setColor(iconColor);
 
 mLetterPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
 mLetterPaint.setColor(Color.WHITE);
 mLetterPaint.setTextSize(letterTextSize);
 mLetterPaint.setTextAlign(Paint.Align.CENTER);
 
 // Text draws from the baselineAdd some top padding to center vertically.
 Rect textMathRect = new Rect();
 mLetterPaint.getTextBounds(mLetter, 0, 1, textMathRect);
 mLetterTop = textMathRect.height() / 2f;
 }

Slide 40

Slide 40 text

40 Letter Drawable | In Constructor @Override
 public void draw(@NonNull Canvas canvas) {
 canvas.drawCircle( bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2f, mCirclePaint);
 canvas.drawText(mLetter, bounds.exactCenterX(), mLetterTop + bounds.exactCenterY(),
 mLetterPaint);
 }

Slide 41

Slide 41 text

41 Letter Drawable | In Constructor @Override
 public void draw(@NonNull Canvas canvas) {
 canvas.drawCircle( bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2f, mCirclePaint);
 canvas.drawText(mLetter, bounds.exactCenterX(), mLetterTop + bounds.exactCenterY(),
 mLetterPaint);
 }

Slide 42

Slide 42 text

42 Letter Drawable | In Constructor @Override
 public void draw(@NonNull Canvas canvas) {
 canvas.drawCircle( bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2f, mCirclePaint);
 canvas.drawText(mLetter, bounds.exactCenterX(), mLetterTop + bounds.exactCenterY(),
 mLetterPaint);
 }

Slide 43

Slide 43 text

Drawing Complex Text onto a Canvas 43

Slide 44

Slide 44 text

Writing Complex Text on a Canvas • Canvas.drawText(…) • Fine for small things, like labels • Often doesn’t have needed fine-tune controls
 • What about heavily formatted text? • Enter android.text.Layout & android.text.Spannable 44

Slide 45

Slide 45 text

StaticLayout + SpannableString • SpannableString • Styling the string themselves • Bold/Italics, Color, Highlighting, Links • “Spread your Wings with Spannables” - Stacy Devino • Tomorrow @ 10AM in Mt. Evans • StaticLayout • Controls layout of entire text. • Ellipsizing, text width/wrapping, break strategies, text alignment • Also has many convenience methods for text before drawing • But, there’s no Canvas.drawStaticLayout()? • Like drawable, has it’s own draw(Canvas) method 45

Slide 46

Slide 46 text

Example: Simple Email • Break into Discreet Chunks • Icon (LetterDrawable) • People List • Subject and Preview • Together? • What’s defined? • Icon size/spacing • From: 1 line • Subject/Preview: up to 2 lines 46

Slide 47

Slide 47 text

47 Email | In Constructor public EmailView(Context context, AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr); 
 initDimens(); // Padding, Icon Size
 initPaints(); // From TextPaint, Preview TextPaint
 }

Slide 48

Slide 48 text

48 Email | In Constructor public EmailView(Context context, AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr); 
 initDimens(); // Padding, Icon Size
 initPaints(); // From TextPaint, Preview TextPaint
 }

Slide 49

Slide 49 text

49 Email | In Constructor public EmailView(Context context, AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr); 
 initDimens(); // Padding, Icon Size
 initPaints(); // From TextPaint, Preview TextPaint
 }

Slide 50

Slide 50 text

50 Email | onNewEmail: LetterDrawable private void onNewEmail(Email email) {
 mIconDrawable = new LetterDrawable(getContext(), email.getPreviewLetter());
 mIconDrawable.setBounds(sPadding, sPadding, sPadding + sIconSize, sPadding + sIconSize); ... }

Slide 51

Slide 51 text

51 Email | onNewEmail: LetterDrawable private void onNewEmail(Email email) {
 mIconDrawable = new LetterDrawable(getContext(), email.getPreviewLetter());
 mIconDrawable.setBounds(sPadding, sPadding, sPadding + sIconSize, sPadding + sIconSize); ... }

Slide 52

Slide 52 text

52 Email | onNewEmail: From Layout private void onNewEmail(Email email) { ... 
 final int textWidth = getWidth()
 - 2 * sPadding - sIconSize // left content & padding
 - sPadding; // content and padding on right
 final CharSequence ellipsizedFrom = TextUtils.commaEllipsize( email.getCommaSeparatedFromList(),
 sFromPaint, textWidth, "+1", "+%d");
 mFromLayout = new StaticLayout(ellipsizedFrom, sFromPaint,
 textWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 1f, false); ... }

Slide 53

Slide 53 text

53 Email | onNewEmail: From Layout private void onNewEmail(Email email) { ... 
 final int textWidth = getWidth()
 - 2 * sPadding - sIconSize // left content & padding
 - sPadding; // content and padding on right
 final CharSequence ellipsizedFrom = TextUtils.commaEllipsize( email.getCommaSeparatedFromList(),
 sFromPaint, textWidth, "+1", "+%d");
 mFromLayout = new StaticLayout(ellipsizedFrom, sFromPaint,
 textWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 1f, false); ... }

Slide 54

Slide 54 text

54 Email | onNewEmail: From Layout private void onNewEmail(Email email) { ... 
 final int textWidth = getWidth()
 - 2 * sPadding - sIconSize // left content & padding
 - sPadding; // content and padding on right
 final CharSequence ellipsizedFrom = TextUtils.commaEllipsize( email.getCommaSeparatedFromList(),
 sFromPaint, textWidth, "+1", "+%d");
 mFromLayout = new StaticLayout(ellipsizedFrom, sFromPaint,
 textWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 1f, false); ... }

Slide 55

Slide 55 text

55 Email | onNewEmail: Preview Layout private void onNewEmail(Email email) { ... 
 String previewText = email.subject + " \u2014 " + email.body; SpannableString preview = new SpannableString(previewText);
 preview.setSpan(new StyleSpan(Typeface.BOLD),
 0, Math.min(previewText.length(), mEmail.subject.length()),
 Spanned.SPAN_INCLUSIVE_EXCLUSIVE); ... }

Slide 56

Slide 56 text

56 Email | onNewEmail: Preview Layout private void onNewEmail(Email email) { ... 
 String previewText = email.subject + " \u2014 " + email.body; SpannableString preview = new SpannableString(previewText);
 preview.setSpan(new StyleSpan(Typeface.BOLD),
 0, Math.min(previewText.length(), mEmail.subject.length()),
 Spanned.SPAN_INCLUSIVE_EXCLUSIVE); ... }

Slide 57

Slide 57 text

57 Email | onNewEmail: Preview Layout private void onNewEmail(Email email) { ... 
 String previewText = email.subject + " \u2014 " + email.body; SpannableString preview = new SpannableString(previewText);
 preview.setSpan(new StyleSpan(Typeface.BOLD),
 0, Math.min(previewText.length(), mEmail.subject.length()),
 Spanned.SPAN_INCLUSIVE_EXCLUSIVE); ... }

Slide 58

Slide 58 text

58 Email | onNewEmail: Preview Layout private void onNewEmail(Email email) { ... mPreviewLayout = StaticLayout.Builder.obtain(preview, 0, preview.length(), sPreviewPaint, textWidth)
 .setAlignment(Layout.Alignment.ALIGN_NORMAL)
 .setEllipsize(TextUtils.TruncateAt.END) .setEllipsizedWidth(textWidth)
 .setMaxLines(2)
 .build(); }

Slide 59

Slide 59 text

59 Email | onNewEmail: Preview Layout private void onNewEmail(Email email) { ... mPreviewLayout = StaticLayout.Builder.obtain(preview, 0, preview.length(), sPreviewPaint, textWidth)
 .setAlignment(Layout.Alignment.ALIGN_NORMAL)
 .setEllipsize(TextUtils.TruncateAt.END) .setEllipsizedWidth(textWidth)
 .setMaxLines(2)
 .build(); }

Slide 60

Slide 60 text

60 Email | onNewEmail: Preview Layout private void onNewEmail(Email email) { ... mPreviewLayout = StaticLayout.Builder.obtain(preview, 0, preview.length(), sPreviewPaint, textWidth)
 .setAlignment(Layout.Alignment.ALIGN_NORMAL)
 .setEllipsize(TextUtils.TruncateAt.END) .setEllipsizedWidth(textWidth)
 .setMaxLines(2)
 .build(); }

Slide 61

Slide 61 text

61 Email | onNewEmail: Preview Layout private void onNewEmail(Email email) { ... mPreviewLayout = StaticLayout.Builder.obtain(preview, 0, preview.length(), sPreviewPaint, textWidth)
 .setAlignment(Layout.Alignment.ALIGN_NORMAL)
 .setEllipsize(TextUtils.TruncateAt.END) .setEllipsizedWidth(textWidth)
 .setMaxLines(2)
 .build(); }

Slide 62

Slide 62 text

62 Email | onNewEmail: Preview Layout @Override
 protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);
 
 mIconDrawable.draw(canvas);
 
 canvas.save();
 canvas.translate(2 * sPadding + sIconSize, sPadding);
 mFromLayout.draw(canvas);
 canvas.translate(0, sFromPaint.getTextSize() + 2);
 mPreviewLayout.draw(canvas);
 canvas.restore();
 }

Slide 63

Slide 63 text

63 Email | onNewEmail: Preview Layout @Override
 protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);
 
 mIconDrawable.draw(canvas);
 
 canvas.save();
 canvas.translate(2 * sPadding + sIconSize, sPadding);
 mFromLayout.draw(canvas);
 canvas.translate(0, sFromPaint.getTextSize() + 2);
 mPreviewLayout.draw(canvas);
 canvas.restore();
 }

Slide 64

Slide 64 text

64 Email | onNewEmail: Preview Layout @Override
 protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);
 
 mIconDrawable.draw(canvas);
 
 canvas.save();
 canvas.translate(2 * sPadding + sIconSize, sPadding);
 mFromLayout.draw(canvas);
 canvas.translate(0, sFromPaint.getTextSize() + sSpacing);
 mPreviewLayout.draw(canvas);
 canvas.restore(); }

Slide 65

Slide 65 text

65 Email | onNewEmail: Preview Layout @Override
 protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);
 
 mIconDrawable.draw(canvas);
 
 canvas.save();
 canvas.translate(2 * sPadding + sIconSize, sPadding);
 mFromLayout.draw(canvas);
 canvas.translate(0, sFromPaint.getTextSize() + 2);
 mPreviewLayout.draw(canvas);
 canvas.restore();
 }

Slide 66

Slide 66 text

66 Email | onNewEmail: Preview Layout @Override
 protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);
 
 mIconDrawable.draw(canvas);
 
 canvas.save();
 canvas.translate(2 * sPadding + sIconSize, sPadding);
 mFromLayout.draw(canvas);
 canvas.translate(0, sFromPaint.getTextSize() + 2);
 mPreviewLayout.draw(canvas);
 canvas.restore();
 }

Slide 67

Slide 67 text

Email Example: Reviewing Steps • Create Paint objects & get Dimens • On new data or size change • Build StaticLayouts for each component • Create New LetterDrawable • Override onDraw(Canvas) • Keep it LIGHT • Extra Credit: RecyclerView.ItemDecoration 67

Slide 68

Slide 68 text

Transforming the Canvas • You can’t provide an x/y to your StaticLayout, 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 nested & restored to certain levels • I think of Stamp/3D Printer. Base moved, paint stays stationary 68

Slide 69

Slide 69 text

Something More… Animated. 69

Slide 70

Slide 70 text

Example: Flying Heart • Break into discrete chunks • The Heart itself • Drawable? Shape? Path! • Follows user’s finger • The “wiggle” • What dimens are defined? • The total size of the heart. 70

Slide 71

Slide 71 text

Flying Heart Code 71

Slide 72

Slide 72 text

72 Flying Heart | Constructor public TouchTrackingView(Context context, AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
 mPaint.setStyle(Paint.Style.FILL);
 mPaint.setColor(heartColor);
 mHeartPath = HeartPathHelper.getHeartOfSize(Math.round(heartSize));
 
 mRotationAnimator = new ValueAnimator();
 mRotationAnimator.setDuration(250);
 mRotationAnimator.setIntValues(-30, 0, 30);
 mRotationAnimator.setRepeatCount(ValueAnimator.INFINITE);
 mRotationAnimator.setRepeatMode(ValueAnimator.REVERSE);
 mRotationAnimator.addUpdateListener(this); }

Slide 73

Slide 73 text

73 Flying Heart | Constructor public TouchTrackingView(Context context, AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
 mPaint.setStyle(Paint.Style.FILL);
 mPaint.setColor(heartColor);
 mHeartPath = HeartPathHelper.getHeartOfSize(Math.round(heartSize));
 
 mRotationAnimator = new ValueAnimator();
 mRotationAnimator.setDuration(250);
 mRotationAnimator.setIntValues(-30, 0, 30);
 mRotationAnimator.setRepeatCount(ValueAnimator.INFINITE);
 mRotationAnimator.setRepeatMode(ValueAnimator.REVERSE);
 mRotationAnimator.addUpdateListener(this); }

Slide 74

Slide 74 text

74 Flying Heart | Constructor public TouchTrackingView(Context context, AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
 mPaint.setStyle(Paint.Style.FILL);
 mPaint.setColor(heartColor);
 mHeartPath = HeartPathHelper.getHeartOfSize(Math.round(heartSize));
 
 mRotationAnimator = new ValueAnimator();
 mRotationAnimator.setDuration(250);
 mRotationAnimator.setIntValues(-30, 0, 30);
 mRotationAnimator.setRepeatCount(ValueAnimator.INFINITE);
 mRotationAnimator.setRepeatMode(ValueAnimator.REVERSE);
 mRotationAnimator.addUpdateListener(this); }

Slide 75

Slide 75 text

75 Flying Heart | Constructor public TouchTrackingView(Context context, AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
 mPaint.setStyle(Paint.Style.FILL);
 mPaint.setColor(heartColor);
 mHeartPath = HeartPathHelper.getHeartOfSize(Math.round(heartSize));
 
 mRotationAnimator = new ValueAnimator();
 mRotationAnimator.setDuration(250);
 mRotationAnimator.setIntValues(-30, 0, 30);
 mRotationAnimator.setRepeatCount(ValueAnimator.INFINITE);
 mRotationAnimator.setRepeatMode(ValueAnimator.REVERSE);
 mRotationAnimator.addUpdateListener(this); }

Slide 76

Slide 76 text

76 Flying Heart | onTouchEvent @Override
 public boolean onTouchEvent(MotionEvent event) {
 final int action = event.getAction();
 if (userIsTouchingScreen) {
 // If a finger is down and/or moving, update the location
 mIsFingerDown = true;
 mFingerX = event.getX();
 mFingerY = event.getY();
 
 // If our Animator isn't running, start it.
 if (mRotationAnimator.isPaused()) {
 mRotationAnimator.resume();
 }
 
 // Must call invalidate here
 invalidate(); } else { // Stop drawing & animation } }

Slide 77

Slide 77 text

77 Flying Heart | onTouchEvent @Override
 public boolean onTouchEvent(MotionEvent event) {
 final int action = event.getAction();
 if (userIsTouchingScreen) {
 // If a finger is down and/or moving, update the location
 mIsFingerDown = true;
 mFingerX = event.getX();
 mFingerY = event.getY();
 
 // If our Animator isn't running, start it.
 if (mRotationAnimator.isPaused()) {
 mRotationAnimator.resume();
 }
 
 // Must call invalidate here
 invalidate(); } else { // Stop drawing & animation } }

Slide 78

Slide 78 text

78 Flying Heart | onTouchEvent @Override
 public boolean onTouchEvent(MotionEvent event) {
 final int action = event.getAction();
 if (userIsTouchingScreen) {
 // If a finger is down and/or moving, update the location
 mIsFingerDown = true;
 mFingerX = event.getX();
 mFingerY = event.getY();
 
 // If our Animator isn't running, start it.
 if (mRotationAnimator.isPaused()) {
 mRotationAnimator.resume();
 }
 
 // Must call invalidate here
 invalidate(); } else { // Stop drawing & animation } }

Slide 79

Slide 79 text

79 Flying Heart | onTouchEvent @Override
 public boolean onTouchEvent(MotionEvent event) {
 final int action = event.getAction();
 if (userIsTouchingScreen) {
 // If a finger is down and/or moving, update the location
 mIsFingerDown = true;
 mFingerX = event.getX();
 mFingerY = event.getY();
 
 // If our Animator isn't running, start it.
 if (mRotationAnimator.isPaused()) {
 mRotationAnimator.resume();
 }
 
 // Must call invalidate here
 invalidate(); } else { // Stop drawing & animation } }

Slide 80

Slide 80 text

80 Flying Heart | onDraw @Override
 protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);
 
 // If user isn't touching the screen, do nothing.
 if (!mIsFingerDown) {
 return;
 }
 
 canvas.save();
 canvas.rotate((Integer) mRotationAnimator.getAnimatedValue(), mFingerX, mFingerY);
 canvas.translate(mFingerX - mHeartSize / 2f, mFingerY - mHeartSize / 2f);
 canvas.drawPath(mHeartPath, mPaint);
 canvas.restore();
 }

Slide 81

Slide 81 text

81 Flying Heart | onDraw @Override
 protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);
 
 // If user isn't touching the screen, do nothing.
 if (!mIsFingerDown) {
 return;
 }
 
 canvas.save();
 canvas.rotate((Integer) mRotationAnimator.getAnimatedValue(), mFingerX, mFingerY);
 canvas.translate(mFingerX - mHeartSize / 2f, mFingerY - mHeartSize / 2f);
 canvas.drawPath(mHeartPath, mPaint);
 canvas.restore();
 }

Slide 82

Slide 82 text

82 Flying Heart | onDraw @Override
 protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);
 
 // If user isn't touching the screen, do nothing.
 if (!mIsFingerDown) {
 return;
 }
 
 canvas.save();
 canvas.rotate((Integer) mRotationAnimator.getAnimatedValue(), mFingerX, mFingerY);
 canvas.translate(mFingerX - mHeartSize / 2f, mFingerY - mHeartSize / 2f);
 canvas.drawPath(mHeartPath, mPaint);
 canvas.restore();
 }

Slide 83

Slide 83 text

83 Flying Heart | onDraw @Override
 protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);
 
 // If user isn't touching the screen, do nothing.
 if (!mIsFingerDown) {
 return;
 }
 
 canvas.save();
 canvas.rotate((Integer) mRotationAnimator.getAnimatedValue(), mFingerX, mFingerY);
 canvas.translate(mFingerX - mHeartSize / 2f, mFingerY - mHeartSize / 2f);
 canvas.drawPath(mHeartPath, mPaint);
 canvas.restore();
 }

Slide 84

Slide 84 text

84 Flying Heart | onDraw @Override
 protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);
 
 // If user isn't touching the screen, do nothing.
 if (!mIsFingerDown) {
 return;
 }
 
 canvas.save();
 canvas.rotate((Integer) mRotationAnimator.getAnimatedValue(), mFingerX, mFingerY);
 canvas.translate(mFingerX - mHeartSize / 2f, mFingerY - mHeartSize / 2f);
 canvas.drawPath(mHeartPath, mPaint);
 canvas.restore();
 }

Slide 85

Slide 85 text

Flying Heart: Reviewing Steps • Create Paint objects, get Dimens and make the Path to define our Heart • Create Wiggle animation (-30deg to +30deg) • Override onTouchEvent • Invalidate/Animate when user touches screen • Stop invalidating/animating when they stop
 • Override onDraw (LIGHT!) • Transform Canvas & draw the Path 85

Slide 86

Slide 86 text

Questions? ALSO LINKS • Dave Smith - “Mastering the Android Touch System” • https://youtu.be/EZAoJU-nUyI • Huyen Tue Dao - “Measure, Layout, Draw, Repeat: Custom Views and ViewGroups” • https://youtu.be/fyPwpTWZ3KE • “Spread your Wings with Spannables” - Stacy Devino • Tomorrow @ 10AM in Mt. Evans • Michael Spitsin - “RecyclerView item optimizations” (StaticLayout + Caching) • http://bit.ly/2sTWn1Y 86

Slide 87

Slide 87 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, July 13th 2017
 Joshua Lamson - Android Developer @darkmoose117 Custom Drawing with Canvas