About the author

Guo Xiaoxing, programmer and guitarist, is mainly engaged in the infrastructure of Android platform. Welcome to exchange technical questions. You can go to my Github to raise an issue or send an email to [email protected] to communicate with me.

The article directories

  • A View of
  • The second Paint
    • 2.1 Color Processing
    • 2.2 Word Processing
    • 2.3 Special Treatment
  • Three Canvas
    • 3.1 Interface Drawing
    • 3.2 Range cutting
    • 3.3 Geometric Transformation
  • The four Path
    • 4.1 Adding graphs
    • 4.3 Drawing lines (Lines or curves)
    • 4.3 Auxiliary Settings and calculations

For the first time in this series, see the introduction and the table of contents for more articles.

The article source

  • DrawView
  • WaveView
  • RippleLayout
  • LabelImageView

This article also provides three comprehensive complete examples to assist understanding.

  • View rendering – image label effect implementation
  • Canvas drawing – Water ripple effect implementation
  • Application of second-order Bezier curve – realization of pouring water into a cup








For the first time in this series, see the introduction and the table of contents for more articles.

In this article we will analyze the practice of View drawing.

A simple custom View

public class DrawView extends View {

    Paint paint = new Paint();

    public DrawView(Context context) {
        super(context);
    }

    public DrawView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public DrawView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setColor(Color.BLACK);
        canvas.drawCircle(150.150.150, paint); }}Copy the code

It draws a circle on the screen, as shown:

There are a few key points when dealing with drawing:

  • To handle drawing, you need to rewrite the drawing method, which is commonly used in View onDraw(), but you can also use other drawing methods to handle masking.
  • The drawing is done by the Canvas class, which provides the drawXXX() method. Clipping series method clipXXX() and geometric transformation method translate() method, as well as auxiliary drawing Path and Matrix.
  • The custom drawing is the Paint class, which is the brush to draw with and can achieve special drawing effects.

Let’s take a look at this key role.

A View of

Draw (view.draw ()); draw(view.draw ()); draw(view.draw ());

  1. DrawBackground () : Draws the background and cannot be overridden.
  2. OnDraw () : Draws the body.
  3. DispatchDraw () : Draws a child View
  4. OnDrawForeground () : Draws sliding edge gradient, scroll bar, and foreground.

Let’s start with a small example.

If we inherit the View to implement a custom View. The View class onDraw() is an empty implementation, so it doesn’t matter if our drawing code is written before or after super.ondraw (canvas), as follows:

public class DrawView extends View {
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // Draw code before and after super.ondraw (canvas)}}Copy the code

But if we inherit a specific control, such as a TextView. We need to think about the drawing logic of our TextView.

public class DrawView extends TextView {
    @Override
    protected void onDraw(Canvas canvas) {

        DrawView is drawn before TextView is drawn, and TextView can overwrite the DrawView
        super.onDraw(canvas);
        DrawView is drawn later than the TextView. DrawView can overwrite the TextView}}Copy the code
  • So, first of all, the DrawView will draw before the TextView draws, and whatever the TextView draws will overwrite the DrawView
  • I’ll write it later, so the DrawView will draw later than the TextView, and whatever the DrawView draws will overwrite the TextView

What you do depends on your actual needs. For example, if you want to add a background to your TextView, you write it in front of super.ondraw (canvas), and if you want to add some embellish to the TextView, you write it behind super.ondraw (canvas).

So let’s write an example to understand that.

For example,

public class LabelImageView extends AppCompatImageView {

    /** * The length of the trapezoid from the top left corner */
    private static final int LABEL_LENGTH = 100;
    /** * The length of the trapezoidal hypotenuse */
    private static final int LABEL_HYPOTENUSE_LENGTH = 100;

    private Paint textPaint;
    private Paint backgroundPaint;
    private Path pathText;
    private Path pathBackground;


    public LabelImageView(Context context) {
        super(context);
        init();
    }

    public LabelImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public LabelImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // Calculate the path
        calculatePath(getMeasuredWidth(), getMeasuredHeight());
        canvas.drawPath(pathBackground, backgroundPaint);
        canvas.drawTextOnPath("Hot", pathText, 100, -20, textPaint);
    }

    @Override
    public void onDrawForeground(Canvas canvas) {
        super.onDrawForeground(canvas);
    }

    / * * * calculation path x1 x2 *... Distance (vertical distance of the label from the upper right corner) *.... *.... Y1 *... *... *.. y2 height (vertical height) label *.. *... * /
    private void calculatePath(int measuredWidth, int measuredHeight) {

        int top = 185;
        int right = measuredWidth;

        float x1 = right - LABEL_LENGTH - LABEL_HYPOTENUSE_LENGTH;
        float x2 = right - LABEL_HYPOTENUSE_LENGTH;
        float y1 = top + LABEL_LENGTH;
        float y2 = top + LABEL_LENGTH + LABEL_HYPOTENUSE_LENGTH;

        pathText.reset();
        pathText.moveTo(x1, top);
        pathText.lineTo(right, y2);
        pathText.close();

        pathBackground.reset();
        pathBackground.moveTo(x1, top);
        pathBackground.lineTo(x2, top);
        pathBackground.lineTo(right, y1);
        pathBackground.lineTo(right, y2);
        pathBackground.close();
    }

    private void init(a) {
        pathText = new Path();
        pathBackground = new Path();

        textPaint = new Paint();
        textPaint.setTextSize(50);
        textPaint.setFakeBoldText(true);
        textPaint.setColor(Color.WHITE);

        backgroundPaint = newPaint(); backgroundPaint.setColor(Color.RED); backgroundPaint.setStyle(Paint.Style.FILL); }}Copy the code

So, as you can see, when we inherit a View, we can optionally override the methods that we want to use depending on our needs, and there’s a difference between inserting code before super and inserting code after super.

  • Draw () : before super.draw(), covered by background; After super.draw(), cover foreground;
  • OnDraw () : super.ondraw () before background and body content; After super.ondraw (), between the main content and the subview;
  • DispatchDraw () : super.dispatchdraw () before the main content and the sub-view; Super.dispatchdraw () after the subview and between the foreground;
  • OnDrawForeground () : super. Foreground(); Super. onDrawForeground();

The second Paint

Paint: As the name suggests, Paint allows you to control the drawing behavior.

Paint can be constructed in three ways

public class Paint {
      // Empty constructor
      public Paint(a) {
          this(0);
      }

      // We pass flags to construct Paint. Flags controls the behavior of the Paint, such as anti-aliasing
      public Paint(int flags) {
          mNativePaint = nInit();
          NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativePaint);
          setFlags(flags | HIDDEN_DEFAULT_PAINT_FLAGS);
          // TODO: Turning off hinting has undesirable side effects, we need to
          // revisit hinting once we add support for subpixel positioning
          // setHinting(DisplayMetrics.DENSITY_DEVICE >= DisplayMetrics.DENSITY_TV
          / /? HINTING_OFF : HINTING_ON);
          mCompatScaling = mInvCompatScaling = 1;
          setTextLocales(LocaleList.getAdjustedDefault());
      }

      // Pass another Paint to construct a new Paint
      public Paint(Paint paint) {
          mNativePaint = nInitWithPaint(paint.getNativeInstance());
          NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativePaint); setClassVariablesFrom(paint); }}Copy the code

2.1 Color processing classes

There are three main methods for handling colors in the Paint class.

  • SetShader (Shader Shader) : Used to handle color gradients
  • SetColorFilter (ColorFilter filter) : used for filtering based on color;
  • SetXfermode (Xfermode Xfermode) is used to handle the relationship between the source image and the existing content of the View

setShader(Shader shader)

Shader is a general concept in image field. It provides a set of shader rules.

public Shader setShader(Shader shader)Copy the code

Shaders are implemented by subclasses of Shader:

LinearGradient – LinearGradient

public LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, TileMode tile)Copy the code
  • X0 y0 x1 y1: positions of the two endpoints of the gradient
  • Color0 Color1 is the color of the endpoints
  • Tile: Coloring rule outside the scope of the endpoint, of type TileMode. TileMode has three options: CLAMP, MIRROR, and REPEAT. CLAMP

For example,

// Linear gradient
Shader shader1 = new LinearGradient(0.100.200.100, Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
paint1.setShader(shader1);

Shader shader2 = new LinearGradient(0.600.200.600, Color.RED, Color.BLUE, Shader.TileMode.MIRROR);
paint2.setShader(shader2);

Shader shader3 = new LinearGradient(0.1100.200.1100, Color.RED, Color.BLUE, Shader.TileMode.REPEAT);
paint3.setShader(shader3);

canvas.drawRect(0.100.1000.500, paint1);
canvas.drawRect(0.600.1000.1000, paint2);
canvas.drawRect(0.1100.1000.1500, paint3);Copy the code

SweepGradient – Radiation gradient

public RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, @NonNull TileMode tileMode)Copy the code
  • CenterX centerY: Coordinates of the radiation center
  • Radius: indicates the radiation radius
  • CenterColor: The color of the radiation center
  • EdgeColor: Color of radiant edges
  • TileMode: Color mode outside the radiation range

For example,

// Radiate gradient
Shader shader1 = new RadialGradient(0.100.200, Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
paint1.setShader(shader1);

Shader shader2 = new RadialGradient(0.600.200, Color.RED, Color.BLUE, Shader.TileMode.MIRROR);
paint2.setShader(shader2);

Shader shader3 = new RadialGradient(0.1100.200, Color.RED, Color.BLUE, Shader.TileMode.REPEAT);
paint3.setShader(shader3);

canvas.drawRect(0.100.1000.500, paint1);
canvas.drawRect(0.600.1000.1000, paint2);Copy the code

BitmapShader – BitmapShader

Fill graphics or text with pixels of a bitmap.

 public BitmapShader(@NonNull Bitmap bitmap, TileMode tileX, TileMode tileY)Copy the code
  • Bitmap: A bitmap object used as a template
  • TileX: Horizontal TileMode
  • TileY: Vertical TileMode.

For example,

BitmapShader is a useful class that you can use to crop a variety of images.

// Bitmap coloring
Shader shader1 = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
paint1.setShader(shader1);

// Draw a circle
canvas.drawCircle(500.500.300, paint1);Copy the code

ComposeShader – Group Shader

ComposeShader groups a group of shaders together.

public ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)Copy the code
  • ShaderA, shaderB: Two shaders used successively
  • Mode: Overlay mode of two shaders, i.e. how shaderA and shaderB should be drawn together. Its type is porterduff.mode.

Porterduff. Mode is used to specify the color rendering strategy when two shaders are superimposed. It has a variety of strategies, that is, what Mode is used to synthesize the original image, as follows:

The blue rectangle is the original image and the red circle is the target image.



See the official porterduff.mode documentation for more details.

setColorFilter(ColorFilter filter)

Color filters can output colors according to certain rules, common in a variety of filter effects.

public ColorFilter setColorFilter(ColorFilter filter)Copy the code

We usually use three subclasses of ColorFilter:

LightingColorFilter – Simulates lighting effects

public LightingColorFilter(int mul, int add)Copy the code

Mul and add are ints in the same format as color values, where mul is used to multiply the target pixels and add is used to add the target pixels.

For example,

// Color filter
ColorFilter colorFilter1 = new LightingColorFilter(Color.RED, Color.BLUE);
paint2.setColorFilter(colorFilter1);

canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.drawBitmap(bitmapTimo, null, rect2, paint2);Copy the code

PorterDuffColorFilter – Simulates color blending effects

public PorterDuffColorFilter(@ColorInt int color, @NonNull PorterDuff.Mode mode)Copy the code

PorterDuffColorFilter specifies a color and PorterDuff.Mode to be combined with the source image, that is, in what Mode to be combined with the original image, we have talked about this content above.

For example,

// We use the subclass PorterDuffXfermode when we use Xfermode
Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
canvas.drawBitmap(rectBitmap, 0.0, paint); / / draw
paint.setXfermode(xfermode); / / set Xfermode
canvas.drawBitmap(circleBitmap, 0.0, paint); / / draw circles
paint.setXfermode(null); // Clear Xfermode when it is finishedCopy the code

ColorMatrixColorFilter – Color matrix filter

ColorMatrixColorFilter uses a ColorMatrix ColorMatrix to object images for processing.

public ColorMatrixColorFilter(ColorMatrix matrix)Copy the code

ColorMatrix is a 4×5 matrix

[ a, b, c, d, e,
  f, g, h, i, j,
  k, l, m, n, o,
  p, q, r, s, t ]Copy the code

Through calculation, ColorMatrix can transform the pixels to be drawn as follows:

R '= a*R + b*G + c* b + d* a + e; G '= f*R + G *G + h*B + I *A + j; B '= k*R + l*G + m*B + n*A + o; A '= p*R + q*G + R *B + s*A + t;Copy the code

ColorMatrixColorFilter(can achieve a lot of cool filter effects.

setXfermode(Xfermode xfermode)

SetXfermode (Xfermode Xfermode) method, which is also a way to mix images.

Xfermode refers to how the content you want to draw should be combined with the content of the Canvas’s target position to calculate the final color. But generally speaking, in fact, you want to draw the content as the source image, the existing content in the View as the target image, select a Porterduff.mode as the color processing scheme of the draw content.

summary

We already mentioned porterduff.mode

  • ComposeShader: Blends two Shaders
  • PorterDuffColorFilter: Adds a monochrome ColorFilter
  • Xfermode: Specifies the blending mode of the original image and the target image

All three use Porterduff.mode in different ways, but the principle is the same.

2.2 Word processing classes

Paint has a lot of methods for setting the Paint properties of text, and text is actually treated as images in Android.

  • SetTextSize (float textSize) : sets the textSize
  • SetTypeface (Typeface Typeface) : Sets text font
  • SetFakeBoldText (Boolean fakeBoldText) : Use false bold (not size, but bold at run time)
  • SetStrikeThruText (Boolean strikeThruText) : Whether to add the stripper
  • SetUnderlineText (Boolean underlineText) : Indicates whether to add an underscore
  • SetTextSkewX (float skewX) : Sets the text gradient
  • SetTextScaleX (float scaleX) : Sets horizontal scaling of text
  • SetLetterSpacing (float letterSpacing) : Sets text spacing
  • SetFontFeatureSettings (String Settings) : Use the FONT feature-settings method of the CSS to set the text.
  • SetTextAlign (Paint.Align Align) : Sets the text alignment
  • SetTextLocale (Locale Locale) : Sets the text Local
  • SetHinting (int mode) : Set font Hinting(fine tuning), add Hinting information to the font, so that the vector font can be corrected when the size is too small, so as to improve the display effect.
  • SetSubpixelText (Boolean subpixelText) : setSubpixelText(Boolean subpixelText) : setSubpixelText(Boolean subpixelText) : setSubpixelText(Boolean subpixelText) : setSubpixelText(Boolean subpixelText) : setSubpixelText(Boolean subpixelText) : Set subpixellevel anti-aliasing.

2.3 Special Effects classes

setAntiAlias (boolean aa)

Set anti-aliasing, off by default, to make the drawing of images more rounded. We can also set Paint Paint = new Paint(paint.anti_alias_flag) at initialization; .

setStyle(Paint.Style style)

Set the fill style,

  • FILL d. FILL
  • STROKE mode, draw a line
  • FILL_AND_STROKE mode, fill + draw

If it’s underscore mode, we can do a couple of things for lines.

SetStrokeWidth (float width) – Sets the line thickness

SetStrokeCap (paint.cap Cap) – Sets the shape of the thread head, default to BUTT

  • UTT flat
  • ROUND ROUND head
  • SQUARE, SQUARE head

SetStrokeJoin (paint.join Join) – Sets the shape of the corner. The default for the MITER

  • MITER Angle
  • BEVEL boxer
  • ROUND ROUND

SetStrokeMiter (float Miter)- Sets the maximum value of extension wires for miter corners

setDither(boolean dither)

Set image shake.

Dithering refers to the practice of deliberately inserting noise into the image to make it more realistic to the naked eye by regularly disturbing the image when drawing from a higher color depth (the number of available colors) to a lower color depth area.

Of course, this effect is useful for low colors, such as ARGB_4444 or RGB_565, but with Android’s default color depth of 32-bit ARGB_8888, it’s less effective.

setFilterBitmap(boolean filter)

Sets whether to draw a Bitmap using bilinear filtering.

The nearest neighbor interpolation filter is used by default when the image is enlarged and drawn. This algorithm is simple, but it will appear Mosaic phenomenon. If bilinear filtering is turned on, the resulting image will appear smoother.

etPathEffect(PathEffect effect)

Sets the contour effect of the graphic. Android has six kinds of PathEffect:

  • CornerPathEffect: Round the corners
  • DiscretePathEffect: DiscretePathEffect
  • DashPathEffect: Draws dashed lines
  • PathDashPathEffect: Draws dashed lines using the specified Path
  • SumPathEffect: Combine two PathEffect and overlay application.
  • ComposePathEffect: Combine two PathEffect, overlay application.

CornerPathEffect(float radius)

  • Float radius Radius of a corner

DiscretePathEffect(float segmentLength, float deviation)

  • Float segmentLength: The length used to splice each line segment,
  • Float deviation: Deviation value

DashPathEffect(float[] intervals, float phase)

  • Float [] intervals: specifies the format of dashed lines, the elements in the array must be even (at least 2), according to “line length, blank length, line length, blank length”… In order of
  • Float phase: Offset of the dashed line

PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style)

  • Path Shape: The Path to draw
  • Float advance: The interval between the start of two adjacent Path segments
  • Float phase: Offset of the dashed line
  • PathDashPathEffect.Style Style: Specifies the transform mode of the shape when the turn changes

SumPathEffect(PathEffect first, PathEffect second)

  • PathEffect First: PathEffect applied simultaneously
  • PathEffect Second: PathEffect applied simultaneously

ComposePathEffect(PathEffect outerpe, PathEffect innerpe)

  • PathEffect outerpe: indicates the PathEffect used later
  • PathEffect innerpe: The PathEffect used first

For example,

// Shape outline effect
// Draw rounded corners
PathEffect cornerPathEffect = new CornerPathEffect(20);
paint1.setStyle(Paint.Style.STROKE);
paint1.setStrokeWidth(5);
paint1.setPathEffect(cornerPathEffect);

// Draw sharp corners
PathEffect discretePathEffect = new DiscretePathEffect(20.5);
paint2.setStyle(Paint.Style.STROKE);
paint2.setStrokeWidth(5);
paint2.setPathEffect(discretePathEffect);

// Draw a dotted line
PathEffect dashPathEffect = new DashPathEffect(new float[] {20.10.5.10}, 0);
paint3.setStyle(Paint.Style.STROKE);
paint3.setStrokeWidth(5);
paint3.setPathEffect(dashPathEffect);

// Use path to draw dotted lines
Path path = new Path();// Draw a triangle to fill the dotted line
path.lineTo(40.40);
path.lineTo(0.40);
path.close();
PathEffect pathDashPathEffect = new PathDashPathEffect(path, 40.0, PathDashPathEffect.Style.TRANSLATE);
paint4.setStyle(Paint.Style.STROKE);
paint4.setStrokeWidth(5);
paint4.setPathEffect(pathDashPathEffect);Copy the code

setShadowLayer(float radius, float dx, float dy, int shadowColor)

Set the shadow layer to be below the target layer.

  • Float radius: shadow radius
  • Float dx: Shadow offset
  • Float dy: Shadow offset
  • Int shadowColor: shadowColor

For example,

paint1.setTextSize(200);
paint1.setShadowLayer(10.0.0, Color.RED);
canvas.drawText("Android".80.300 ,paint1);Copy the code

Note: setShadowLayer() only supports rendering of text with hardware acceleration enabled. Rendering outside of text must be disabled to render shadows normally. If shadowColor is translucent, the opacity of the shadow will use the opacity of shadowColor itself. If shadowColor is opaque, the opacity of the shadow is paint opacity.

setMaskFilter(MaskFilter maskfilter)

Set the layer Mask layer to be on the upper layer of the target.

MaskFilter has two subclasses:

  • BlurMaskFilter: Blurring effect
  • BlurMaskFilter: Embossed effect

For example,

blur

  • BlurMaskFilter.Blur.NORMAL
  • BlurMaskFilter.Blur.SOLD
  • BlurMaskFilter.Blur.INNER
  • BlurMaskFilter.Blur.OUTTER

Are:










// Set the mask layer to be on the upper layer of the target
// Turn off hardware acceleration
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
MaskFilter blurMaskFilter = new BlurMaskFilter(200, BlurMaskFilter.Blur.NORMAL);
paint2.setMaskFilter(blurMaskFilter);

canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.drawBitmap(bitmapTimo, null, rect2, paint2);Copy the code

Note: If hardware acceleration is enabled, setMaskFilter(MaskFilter MaskFilter) only supports text rendering. If hardware acceleration is disabled, shadows can be drawn normally. Close the hardware acceleration can be called the setLayerType (View. LAYER_TYPE_SOFTWARE, null) or in the Activity tag set android: hardwareAccelerated = “false”.

Three Canvas

Canvas realizes the drawing of Android 2D graphics, and the underlying implementation is based on Skia.

3.1 Interface Drawing

Canvas provides a variety of object drawing methods, which generally start with drawXXX(). Objects drawn include:

  • Arcs
  • Color (Argb, Color)
  • Bitmap
  • Round (Circle)
  • Point (Point)
  • Line (Line)
  • Rectangle (Rect)
  • Picture:
  • RoundRect
  • Text
  • Vertices
  • Path

Most of the methods here are very simple, so let’s describe the more complicated methods in the next video.

arc

public void drawArc(float left, float top, float right, float bottom, float startAngle,
        float sweepAngle, boolean useCenter, @NonNull Paint paint) {
    native_drawArc(mNativeCanvasWrapper, left, top, right, bottom, startAngle, sweepAngle,
            useCenter, paint.getNativeInstance());
}Copy the code
  • Float left, float top, float right, float bottom:
  • Float startAngle: the starting Angle of an arc. The right of the X axis in the Android coordinate system is 0 degrees, clockwise is a positive Angle, and counterclockwise is a negative Angle.
  • Float sweepAngle: The Angle of the arc.
  • Boolean useCenter: Whether to connect to the center of the circle. If it’s not connected to the center of the circle it’s an arc, if it’s connected to the center of the circle it’s a fan.

For example,

paint.setStyle(Paint.Style.FILL);// Fill mode
canvas.drawArc(200.100.800.500, -110.100.true, paint);
canvas.drawArc(200.100.800.500.20.140.false, paint);
paint.setStyle(Paint.Style.STROKE);// Line mode
paint.setStrokeWidth(5);
canvas.drawArc(200.100.800.500.180.60.false, paint);Copy the code

The bitmap

  • Public void drawBitmap(@nonNULL Bitmap Bitmap, float left, float Top, @Nullable Paint Paint) – Draws a Bitmap
  • **public void drawBitmapMesh(@NonNull Bitmap bitmap, int meshWidth, int meshHeight,
           @NonNull float[] verts, int vertOffset, @nullable int[] colors, int colorOffset, @nullable Paint) -Copy the code

The first method is simply to start drawing a bitmap at the specified coordinates. Let’s focus on the second method, which is not very common (probably a more computationally complex pot 😓), but that doesn’t make it less powerful.

The drawBitmapMesh() method divides the bitmap into several grids, and then distorts each grid. Let’s take a look at the parameters of this method:

  • @nonnull Bitmap Bitmap: source Bitmap
  • Int meshWidth: How many grids to divide the source bitmap horizontally
  • Int meshHeight: The number of vertical partitions of the source bitmap
  • @nonnull float[] verts: an array of grid vertex coordinates, recording the coordinates of each vertex of the distorted image, size = (meshWidth+1) (meshHeight+1) 2 + vertOffset
  • Int vertOffset is more than the number of elements the array is distorted from
  • @nullable int[] colors: Sets the color of the grid vertices. This color will overlay with the color of the corresponding pixel of the bitmap. The array size is (meshWidth+1) * (meshHeight+1) + colorOffset
  • Int colorOffset: records that colors start with several array elements
  • Nullable Paint Paint

Let’s create a water ripple effect using the drawBitmapMesh() method.

For example,

/** * Use canvas. drawBitmapMeshC() to distort the image and simulate the effect of water wave. * <p> * For more information, you can visit https://github.com/guoxiaoxing or contact me by * [email protected] * *@author guoxiaoxing
 * @since2017/9/12 3:44 PM */
public class RippleLayout extends FrameLayout {

    /** * horizontal and vertical grid tree */
    private final int MESH_WIDTH = 20;
    private final int MESH_HEIGHT = 20;

    /** * number of vertices */
    private final int VERTS_COUNT = (MESH_WIDTH + 1) * (MESH_HEIGHT + 1);

    /** ** array */
    private final float[] originVerts = new float[VERTS_COUNT * 2];

    /** * the converted coordinate array */
    private final float[] targetVerts = new float[VERTS_COUNT * 2];

    /** * The image of the current space */
    private Bitmap bitmap;

    /** * half the width of the water wave */
    private float rippleWidth = 100f;

    /** * the speed of water wave propagation */
    private float rippleRadius = 15f;

    /** * water wave radius */
    private float rippleSpeed = 15f;

    /** * Whether the water wave animation is in progress */
    private boolean isRippling;

    public RippleLayout(@NonNull Context context) {
        super(context);
    }

    public RippleLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public RippleLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        if(isRippling && bitmap ! =null) {
            canvas.drawBitmapMesh(bitmap, MESH_WIDTH, MESH_HEIGHT, targetVerts, 0.null.0.null);
        } else {
            super.dispatchDraw(canvas); }}@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                showRipple(ev.getX(), ev.getY());
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    /** * display water wave animation **@paramOriginX originX coordinates *@paramOriginY the originY coordinate */
    public void showRipple(final float originX, final float originY) {
        if (isRippling) {
            return;
        }
        initData();
        if (bitmap == null) {
            return;
        }
        isRippling = true;
        // The number of cycles, calculated by the control diagonal distance, to ensure that the water ripples completely disappear
        int viewLength = (int) getLength(bitmap.getWidth(), bitmap.getHeight());
        final int count = (int) ((viewLength + rippleWidth) / rippleSpeed);
        Observable.interval(0.10, TimeUnit.MILLISECONDS)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .take(count + 1)
                .subscribe(new Consumer<Long>() {
                    @Override
                    public void accept(@NonNull Long aLong) throws Exception {
                        rippleRadius = aLong * rippleSpeed;
                        warp(originX, originY);
                        if (aLong == count) {
                            isRippling = false; }}}); }/** * Initializes the Bitmap and corresponding array */
    private void initData(a) {
        bitmap = getCacheBitmapFromView(this);
        if (bitmap == null) {
            return;
        }
        float bitmapWidth = bitmap.getWidth();
        float bitmapHeight = bitmap.getHeight();
        int index = 0;
        for (int height = 0; height <= MESH_HEIGHT; height++) {
            float y = bitmapHeight * height / MESH_HEIGHT;
            for (int width = 0; width <= MESH_WIDTH; width++) {
                float x = bitmapWidth * width / MESH_WIDTH;
                originVerts[index * 2] = targetVerts[index * 2] = x;
                originVerts[index * 2 + 1] = targetVerts[index * 2 + 1] = y;
                index += 1; }}}/** * image conversion **@paramOriginX originX coordinates *@paramOriginY the originY coordinate */
    private void warp(float originX, float originY) {
        for (int i = 0; i < VERTS_COUNT * 2; i += 2) {
            float staticX = originVerts[i];
            float staticY = originVerts[i + 1];
            float length = getLength(staticX - originX, staticY - originY);
            if (length > rippleRadius - rippleWidth && length < rippleRadius + rippleWidth) {
                PointF point = getRipplePoint(originX, originY, staticX, staticY);
                targetVerts[i] = point.x;
                targetVerts[i + 1] = point.y;
            } else {
                / / recovery
                targetVerts[i] = originVerts[i];
                targetVerts[i + 1] = originVerts[i + 1];
            }
        }
        invalidate();
    }

    /** * get the offset coordinates of the water wave **@paramOriginX originX coordinates *@paramOriginY the originY coordinate *@paramStaticX The original x-coordinate of the vertex to be offset *@paramStaticY the original y-coordinate of the vertex to be offset *@returnOffset coordinate */
    private PointF getRipplePoint(float originX, float originY, float staticX, float staticY) {
        float length = getLength(staticX - originX, staticY - originY);
        // The Angle between offset and origin
        float angle = (float) Math.atan(Math.abs((staticY - originY) / (staticX - originX)));
        // Calculate the offset distance
        float rate = (length - rippleRadius) / rippleWidth;
        float offset = (float) Math.cos(rate) * 10f;
        float offsetX = offset * (float) Math.cos(angle);
        float offsetY = offset * (float) Math.sin(angle);
        // Calculate the offset coordinates
        float targetX;
        float targetY;
        if (length < rippleRadius + rippleWidth && length > rippleRadius) {
            // The offset coordinates outside the wave crest
            if (staticX > originX) {
                targetX = staticX + offsetX;
            } else {
                targetX = staticX - offsetX;
            }
            if (staticY > originY) {
                targetY = staticY + offsetY;
            } else{ targetY = staticY - offsetY; }}else {
            // The offset coordinates within the wave crest
            if (staticX > originY) {
                targetX = staticX - offsetX;
            } else {
                targetX = staticX + offsetX;
            }
            if (staticY > originY) {
                targetY = staticY - offsetY;
            } else{ targetY = staticY + offsetY; }}return new PointF(targetX, targetY);
    }

    /** * get the diagonal distance ** based on the width and height@paramWide width *@paramHigh height *@returnDistance * /
    private float getLength(float width, float height) {
        return (float) Math.sqrt(width * width + height * height);
    }

    /** * Get the cache View of the View **@paramView corresponds to view *@returnThe cache View of the corresponding View */
    private Bitmap getCacheBitmapFromView(View view) {
        view.setDrawingCacheEnabled(true);
        view.buildDrawingCache(true);
        final Bitmap drawingCache = view.getDrawingCache();
        Bitmap bitmap;
        if(drawingCache ! =null) {
            bitmap = Bitmap.createBitmap(drawingCache);
            view.setDrawingCacheEnabled(false);
        } else {
            bitmap = null;
        }
        returnbitmap; }}Copy the code

The path

public void drawPath(@NonNull Path path, @NonNull Paint paint) {
    if(path.isSimplePath && path.rects ! =null) {
        native_drawRegion(mNativeCanvasWrapper, path.rects.mNativeRegion, paint.getNativeInstance());
    } else{ native_drawPath(mNativeCanvasWrapper, path.readOnlyNI(), paint.getNativeInstance()); }}Copy the code

DrawPath () draws a custom graph whose Path is described by a Path object.

The Path object can describe many shapes, specifically:

  • A straight line
  • Quadratic curve
  • Cubic curve
  • round
  • The ellipse
  • arc
  • rectangular
  • The rounded rectangle

3.2 Range cutting

There are two main types of scope clipping methods in Canvas:

  • ClipReact () : Trim by path
  • ClipPath () : Cut by coordinate

For example,

clipReact

clipPath

// Range cutting
canvas.save();// Save the canvas
canvas.clipRect(200.200.900.900);
canvas.drawBitmap(bitmapTimo, 100.100, paint1);
canvas.restore();// Restore the canvas

canvas.save();// Save the canvas
path.addCircle(500.500.300, Path.Direction.CW);
canvas.clipPath(path);
canvas.drawBitmap(bitmapTimo, 100.100, paint1);
canvas.restore();// Restore the canvasCopy the code

3.3 Geometric Transformation

There are three ways to implement geometric transformations:

  • Canvas: General geometric transformation
  • Matrix: Custom geometry transformation
  • Camera: 3d transformation

Canvas general geometry transformation

Canvas also provides methods for changing the position of objects, including:

  • Translate (float dx, float dy) : translate
  • Rotate (float degrees) : rotates. You can set the rotation point at the origin by default.
  • Scale (float sx, float SY) : scale
  • Skew (float Sx, float SY) : skew

For example,

canvas.save();// Save the canvas
canvas.skew(0.0.5 f);
canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.restore();// Restore the canvas

canvas.save();// Save the canvas
canvas.rotate(45.750.750);
canvas.drawBitmap(bitmapTimo, null, rect2, paint1);
canvas.restore();// Restore the canvasCopy the code

Note: 1 In order not to affect other drawing operations, you need to call canvas.save() to save the canvas before the transformation, and call canvas.restore() to restore the canvas after the transformation. 2 the order of Canvas geometry transformation is reversed. For example, we wrote in the code: Canvas. Skew (0, 0.5f); canvas.rotate(45, 750, 750); The actual call order is canvas.rotate(45, 750, 750); – > canvas. Skew (0, 0.5 f)

Matrix custom geometry transformation

Matrix also realizes four kinds of conventional transformations in Canvas, and its implementation process is as follows:

  1. Create Matrix object;
  2. Calls for the Matrix of the pre/postTranslate/Rotate/Scale/Skew () method to set up the geometry transformation;
  3. Use canvas.setmatrix (matrix) or Canvas.concat(matrix) to apply geometric transformations to Canvas.

Canvas.concat(matrix) : multiply the current transformation matrix of Canvas by matrix, that is, superimpose the transformation in matrix based on the current transformation of Canvas.

For example,

//Matrix geometry transform
canvas.save();// Save the canvas
matrix.preSkew(0.0.5 f);
canvas.concat(matrix);
canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.restore();// Restore the canvas

canvas.save();// Save the canvas
matrix.reset();
matrix.preRotate(45.750.750);
canvas.concat(matrix);
canvas.drawBitmap(bitmapTimo, null, rect2, paint1);
canvas.restore();// Restore the canvasCopy the code

In addition to the four basic geometric transformations, Matrix can also customize geometric transformations.

  • setPolyToPoly(float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount)
  • setRectToRect(RectF src, RectF dst, ScaleToFit stf)

Both of these methods directly set the transformation through the mapping of multiple points, moving the specified point to the given position, so as to produce deformation.

For example,

//Matrix geometry transform
canvas.save();// Save the canvas
matrix.setPolyToPoly(src, 0, dst, 0.2);
canvas.concat(matrix);
canvas.drawBitmap(bitmapTimo, 0.0, paint1);
canvas.restore();// Restore the canvasCopy the code

Camera 3D transformation

Before explaining Camera 3d transformation, we need to understand Camera coordinate system first.

As we mentioned earlier, Canvas uses a two-dimensional coordinate system.

While the Camera uses a three-dimensional coordinate system, here is a bit lazy 😊, to describe it by using Kege’s diagram.

About Camera coordinate system:

  • The first thing you need to notice is the x, y, and z orientations. The z axis is negative.
  • There is a virtual camera on the negative axis of Z (which is the yellow dot in the image), which is used for projection, and the setLocation(float x, float y, float Z) method moves its position.
  • The rotation directions of the X, y and Z axes are also indicated in the figure above.

Let’s say we do an X rotation in Camera coordinates

Camera 3d transformation includes rotation, translation and movement of the Camera.

rotating

  • rotateX(deg)
  • rotateY(deg)
  • rotateZ(deg)
  • rotate(x, y, z)

translation

  • translate(float x, float y, float z)

Move the camera

  • setLocation(float x, float y, float z)

For example,

rotating

//Camera 3d transform
canvas.save();// Save the canvas

camera.save();/ / save the camera
camera.rotateX(45);
canvas.translate(500.750);// Camera also defaults to the origin (0, 0), so we move the canvas to the center of the image (500, 750).
camera.applyToCanvas(canvas);
canvas.translate(-500, -750);// Move the canvas from the center of the image (500, 750) to the origin (0, 0)
camera.restore();/ / restore camera

canvas.drawBitmap(bitmapTimo, null, rect, paint1);
canvas.restore();// Restore the canvasCopy the code

translation

//Camera 3d transform
canvas.save();// Save the canvas

camera.save();/ / save the camera
camera.translate(500.500.500);
canvas.translate(500.750);// Camera also defaults to the origin (0, 0), so we move the canvas to the center of the image (500, 750).
camera.applyToCanvas(canvas);
canvas.translate(-500, -750);// Move the canvas from the center of the image (500, 750) to the origin (0, 0)
camera.restore();/ / restore camera

canvas.drawBitmap(bitmapTimo, null, rect, paint1);
canvas.restore();// Restore the canvasCopy the code

Move the camera

//Camera 3d transform
canvas.save();// Save the canvas

camera.save();/ / save the camera
camera.setLocation(0.0, - 1000);// The image becomes smaller as the camera moves forward
canvas.translate(500.750);// Camera also defaults to the origin (0, 0), so we move the canvas to the center of the image (500, 750).
camera.applyToCanvas(canvas);
canvas.translate(-500, -750);// Move the canvas from the center of the image (500, 750) to the origin (0, 0)
camera.restore();/ / restore camera

canvas.drawBitmap(bitmapTimo, null, rect, paint1);
canvas.restore();// Restore the canvasCopy the code

The four Path

Path describes the drawing Path, which can be used to complete many complex graphics drawing.

Let’s look at the methods in Path.

4.1 Adding graphs

Example: addCircle(float x, float y, float radius, Direction dir)

public void addCircle(float x, float y, float radius, Direction dir) {
    isSimplePath = false;
    native_addCircle(mNativePath, x, y, radius, dir.nativeInt);
}Copy the code

Parameter meanings of this method:

  • Float x: Coordinates of the center x axis
  • Float y: indicates the y axis of the center of a circle
  • Float radius: circle radius
  • Direction dir: Draw the Direction of the circle’s path, clockwise direction.cn and counterclockwise direction.ccn, which are used to determine the filling range when the shapes (Paint.Style is FILL or FILL_AND_STROKE) intersect.

All other methods are similar to this method.

4.2 Drawing lines (Lines or curves)

A straight line

// Draw a line from the current position to the target position. This method uses absolute coordinates with respect to the origin
public void lineTo(float x, float y) {
    isSimplePath = false;
    native_lineTo(mNativePath, x, y);
}

// Draw a line from the current position to the target position. This method uses coordinates relative to the current position
public void rLineTo(float dx, float dy) {
    isSimplePath = false;
    native_rLineTo(mNativePath, dx, dy);
}Copy the code

Current position: The current position refers to the end of the last stolen Path method, starting with (0, 0).

As for the current position, we have another method path.moveto (float x, float y), which can move the current position to a new position.

For example,

paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
path.lineTo(300.400);// Draw a line from the current position (0, 0) to (300, 400)
path.rLineTo(400.0);// Draw a line 400 pixels straight to the right from the current position (300, 400)
canvas.drawPath(path, paint);Copy the code

Bessel curve

Bezier curve: A Bezier curve is a geometric curve. It describes a curve by starting point, control point and end point, mainly used in computer graphics. In simple terms, bezier curves are the transformation of any curve into an exact mathematical formula.

In bezier curves, there are two types of points:

  • Data point: Generally refers to the start and end of a path.
  • Control points: control points determine the bending trajectory of the path. According to the number of control points, Bezier curves can be divided into first-order Bezier curves (0 control points), second-order Bezier curves (1 control point), and third-order Bezier curves (2 control points).

First order Bezier curve

B(t) is the coordinate at time t, where P0 is the starting point and P1 is the ending point.

Second order Bezier curve

Third order Bezier curve

Bezier curves can be simulated using bezier-curve

Let’s take a look at the methods of the Bessel curve provided by the Path class.


// Second order Bezier curve, absolute coordinates, (x1, y1) represents the control point, (x2, y2) represents the end point
public void quadTo(float x1, float y1, float x2, float y2) {
    isSimplePath = false;
    native_quadTo(mNativePath, x1, y1, x2, y2);
}

// Second order Bezier curve, relative coordinates
public void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
    isSimplePath = false;
    native_rQuadTo(mNativePath, dx1, dy1, dx2, dy2);
}

// Third order Bezier curve, absolute coordinates, (x1, y1), (x2, y2) represent control points, (x3, y3) represent end points
public void cubicTo(float x1, float y1, float x2, float y2,
                    float x3, float y3) {
    isSimplePath = false;
    native_cubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
}

// Third order Bezier curve, relative coordinates
public void rCubicTo(float x1, float y1, float x2, float y2,
                     float x3, float y3) {
    isSimplePath = false;
    native_rCubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
}Copy the code

Let’s use bezier curves to make a glass of water.

For example,

/** * The x-coordinate of the control point moves left and right continuously, forming a wave effect. * <p> * For more information, you can visit https://github.com/guoxiaoxing or contact me by * [email protected] * *@author guoxiaoxing
 * @since2017/9/11 6:11 PM */
public class WaveView extends View {

    private static final String TAG = "WaveView";

    /** * Waves start and end off-screen for a more realistic effect */
    private static final float EXTRA_DISTANCE = 200;

    private Path mPath;
    private Paint mPaint;

    /** * control width height */
    private int mWidth;
    private int mHeight;

    /** * control point coordinates */
    private float mControlX;
    private float mControlY;

    /** * wave peak */
    private float mWaveY;

    /** * Whether to move the control point */
    private boolean mMoveControl = true;

    public WaveView(Context context) {
        super(context);
        init();
    }

    public WaveView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;

        mControlY = mHeight - mHeight / 8;
        mWaveY = mHeight - mHeight / 32;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // Waves start off the screen, more realistic
        mPath.moveTo(-EXTRA_DISTANCE, mWaveY);
        // Second order Bezier curve
        mPath.quadTo(mControlX, mControlY, mWidth + EXTRA_DISTANCE, mWaveY);
        // Close the curve
        mPath.lineTo(mWidth, mHeight);
        mPath.lineTo(0, mHeight);
        mPath.close();
        canvas.drawPath(mPath, mPaint);

        //mControlX coordinates in the range -EXTRA_DISTANCE to mWidth + EXTRA_DISTANCE increment and decrement, left and right
        // Create a wave effect
        if (mControlX <= -EXTRA_DISTANCE) {
            mMoveControl = true;
        } else if (mControlX >= mWidth + EXTRA_DISTANCE) {
            mMoveControl = false;
        }
        mControlX = mMoveControl ? mControlX + 20 : mControlX - 20;

        // The water level keeps rising
        if (mControlY >= 0) {
            mControlY -= 2;
            mWaveY -= 2;
        }

        Log.d(TAG, "mControlX: " + mControlX + " mControlY: " + mControlY + " mWaveY: " + mWaveY);

        mPath.reset();
        invalidate();
    }


    private void init(a) {
        mPath = new Path();
        mPaint = new Paint();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        mPaint.setColor(Color.parseColor("#4CAF50")); }}Copy the code

arc


/ / draw arc
public void arcTo(float left, float top, float right, float bottom, float startAngle,
        float sweepAngle, boolean forceMoveTo) {
    isSimplePath = false;
    native_arcTo(mNativePath, left, top, right, bottom, startAngle, sweepAngle, forceMoveTo);
}Copy the code

Let’s look at the arguments to this method:

  • Float left, float top, float right, float bottom:
  • Float startAngle: the starting Angle of an arc. The right of the X axis in the Android coordinate system is 0 degrees, clockwise is a positive Angle, and counterclockwise is a negative Angle.
  • Float sweepAngle: The Angle of the arc.
  • Boolean forceMoveTo) : Whether to leave traces of movement file

Note: This method is missing a Booleusecenter argument from the canvas.drawarc () method, which is also used to draw arcs, because arcTo() is only used to draw arcs.

4.3 Auxiliary Settings and calculations

Public void setFillType(FillType ft) – Sets the filling mode

The fill method is used to set the fill method. There are four fill methods:

  • WINDING: Non-zero WINDING rule
  • EVEN_ODD: indicates the even-odd rule
  • INVERSE_WINDING: The inversion of WINDING
  • INVERSE_EVEN_ODD: Invert of EVEN_ODD

WINDING: Non-zero winding rule, which is based on the principle of non-zero winding number, is based on the drawing Direction of all graphs (clockwise and counter-clockwise described in the Direction mentioned above). For any point on the plane, a ray is injected in any Direction, and 1 is added to each clockwise intersection of the ray. If a counterclockwise intersection is encountered, subtract 1. If the final result is not 0, the point is considered to be inside the graph and dyed. If the result is 0, the point is considered to be outside the graph and not stained.

EVEN_ODD: For any point on the plane, if a ray is shot in any direction and the ray intersects the graph (not the tangent) an odd number of times, the point is inside the graph, then the color is performed. If it is even, it is considered outside the graph and no staining is performed. This is a case of cross staining.