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

Custom Drawing with Canvas

Custom Drawing with Canvas

Companion Project: https://bitbucket.org/darkmoose117/custom-drawing-with-canvas

When you’re creating a new View to display some complex data, it’s easy for the number of View objects you’re using to get out of control. Between elaborate shapes, shaders/colors, and text in specific places and orientations, using only Android Views and layouts can turn your layout into a dense, complicated mess. Luckily, there’s a better way to add all your customizations. What you need is a blank Canvas! (Literally, android.graphics.Canvas). In this session, we’ll delve into the details of using a Canvas to display anything you need in a truly custom way. You’ll leave this talk knowing how to:
– Paint custom shapes, Paths, and text onto Canvas
– Override a View’s draw methods to draw on it’s Canvas
– Override a ViewGroup’s draw methods to draw over, under, and around it’s children
– Overriding a Drawable’s draw methods to create custom re-usable Drawables
– Update your View’s state in response to touches and state changes
– And how to do all of the this while keeping your Views performant and and your app buttery-smooth.

29517118e397011e21547099c69a3da5?s=128

Joshua Lamson

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

  3. 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
  4. Where do we start? 4

  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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);
  11. 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);
  12. 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);
  13. 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; ... }
  14. 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; ... }
  15. 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; ... }
  16. 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); ... }
  17. 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); ... }
  18. 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); ... }
  19. 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);
 } ... }
  20. 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);
 } ... }
  21. 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);
 } ... }
  22. 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);
 } ... }
  23. 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;
 } }
  24. 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;
 } }
  25. 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;
 } }
  26. 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;
 } }
  27. 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;
 } }
  28. 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;
 } }
  29. Bar Chart: Reviewing Steps • Create Paint Objects in constructor

    • Override onDraw(Canvas) • Keep it LIGHT • Draw each component • Axis • Guidelines • Bars 29
  30. 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
  31. 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;
 } }
  32. 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
  33. 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
  34. Drawable Methods WHAT YOU NEED TO IMPLEMENT • setBounds(Rect) •

    onBoundsChange(Rect) • getIntrinsicWidth()/Height() • setAlpha() • setColorFilter() • getOpacity() • PixelFormat.OPAQUE, TRANSPARENT, TRANSLUCENT • draw(Canvas)! 34
  35. Example: Letter Drawable • Break into discrete chunks • Colored

    circle • White letter • What dimens are defined? • Radius of circle • Text size • Text is centered 35
  36. 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;
 }
  37. 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;
 }
  38. 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;
 }
  39. 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;
 }
  40. 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);
 }
  41. 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);
 }
  42. 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);
 }
  43. Drawing Complex Text onto a Canvas 43

  44. 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
  45. 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
  46. 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
  47. 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
 }
  48. 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
 }
  49. 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
 }
  50. 50 Email | onNewEmail: LetterDrawable private void onNewEmail(Email email) {


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


    mIconDrawable = new LetterDrawable(getContext(), email.getPreviewLetter());
 mIconDrawable.setBounds(sPadding, sPadding, sPadding + sIconSize, sPadding + sIconSize); ... }
  52. 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); ... }
  53. 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); ... }
  54. 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); ... }
  55. 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); ... }
  56. 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); ... }
  57. 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); ... }
  58. 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(); }
  59. 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(); }
  60. 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(); }
  61. 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(); }
  62. 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();
 }
  63. 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();
 }
  64. 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(); }
  65. 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();
 }
  66. 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();
 }
  67. 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
  68. 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
  69. Something More… Animated. 69

  70. 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
  71. Flying Heart Code 71

  72. 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); }
  73. 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); }
  74. 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); }
  75. 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); }
  76. 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 } }
  77. 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 } }
  78. 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 } }
  79. 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 } }
  80. 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();
 }
  81. 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();
 }
  82. 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();
 }
  83. 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();
 }
  84. 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();
 }
  85. 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
  86. 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
  87. 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