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.

Joshua Lamson

July 13, 2017
Tweet

More Decks by Joshua Lamson

Other Decks in Technology

Transcript

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

    Joshua Lamson - Android Developer
    @darkmoose117
    Custom Drawing with Canvas

    View Slide

  2. Why Custom Drawing?
    2

    View Slide

  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

    View Slide

  4. Where do we start?
    4

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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);

    View Slide

  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);

    View Slide

  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);

    View Slide

  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;
    ...
    }

    View Slide

  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;
    ...
    }

    View Slide

  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;
    ...
    }

    View Slide

  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);
    ...
    }

    View Slide

  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);
    ...
    }

    View Slide

  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);
    ...
    }

    View Slide

  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);

    }
    ...
    }

    View Slide

  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);

    }
    ...
    }

    View Slide

  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);

    }
    ...
    }

    View Slide

  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);

    }
    ...
    }

    View Slide

  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;

    }
    }

    View Slide

  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;

    }
    }

    View Slide

  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;

    }
    }

    View Slide

  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;

    }
    }

    View Slide

  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;

    }
    }

    View Slide

  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;

    }
    }

    View Slide

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

    View Slide

  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

    View Slide

  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;

    }
    }

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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;

    }

    View Slide

  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;

    }

    View Slide

  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;

    }

    View Slide

  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;

    }

    View Slide

  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);

    }

    View Slide

  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);

    }

    View Slide

  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);

    }

    View Slide

  43. Drawing Complex Text onto a Canvas
    43

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    }

    View Slide

  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

    }

    View Slide

  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

    }

    View Slide

  50. 50
    Email | onNewEmail: LetterDrawable
    private void onNewEmail(Email email) {

    mIconDrawable = new LetterDrawable(getContext(),
    email.getPreviewLetter());

    mIconDrawable.setBounds(sPadding, sPadding,
    sPadding + sIconSize, sPadding + sIconSize);
    ...
    }

    View Slide

  51. 51
    Email | onNewEmail: LetterDrawable
    private void onNewEmail(Email email) {

    mIconDrawable = new LetterDrawable(getContext(),
    email.getPreviewLetter());

    mIconDrawable.setBounds(sPadding, sPadding,
    sPadding + sIconSize, sPadding + sIconSize);
    ...
    }

    View Slide

  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);
    ...
    }

    View Slide

  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);
    ...
    }

    View Slide

  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);
    ...
    }

    View Slide

  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);
    ...
    }

    View Slide

  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);
    ...
    }

    View Slide

  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);
    ...
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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();

    }

    View Slide

  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();

    }

    View Slide

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

    View Slide

  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();

    }

    View Slide

  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();

    }

    View Slide

  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

    View Slide

  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

    View Slide

  69. Something More… Animated.
    69

    View Slide

  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

    View Slide

  71. Flying Heart Code
    71

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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();

    }

    View Slide

  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();

    }

    View Slide

  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();

    }

    View Slide

  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();

    }

    View Slide

  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();

    }

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide