Analysis of the First Paper by Common Methods of Canvas

1. Image distortion

A drawBitmapMesh method is provided in Canvas, through which the distortion effect of bitmap can be achieved. The following is an analysis of this method:

The method signature is as follows:
public void drawBitmapMesh(@NonNull Bitmap bitmap, int meshWidth, int meshHeight,
            @NonNull float[] verts, int vertOffset, @Nullable int[] colors, int colorOffset,
            @Nullable Paint paint)
1 > This method cuts bitmap horizontally and vertically into mesh Width and mesh Height parts, respectively, so that the bitmap is cut into mesh.
As shown in Figure 1;
2 > There are meshWidth + 1 * (meshHeight + 1) grid intersection coordinates, and verts array is used to save grid intersection coordinates.
vertOffset indicates that the verts array saves the coordinates of grid intersections from the first few elements, so the verts array is at least as long as
(meshWidth + 1)*(meshHeight + 1)*2+ vertOffset;
3 > Colors is used to save the color specified for the intersection of the grid, which multiplies the corresponding color in the bitmap
 (Multied refers to Figure 2). colorOffset represents the color of the colors array that is saved from the first few elements as the specified color at the intersection of the grid.
So the length of the colors array is at least (meshWidth + 1)* (meshHeight + 1) + colorOffset, and colors can be null;
4 > paint represents the brush used to draw bitmap, which can be null.

Note: This method is supported when the API level is greater than or equal to 18 hardware speedup.


Figure 1


Figure 2

Achieve water ripple effect:
First, look at the ripple effect from the perspective of overlooking.


Top view of water ripple

In the picture above, a wave-length ripple is drawn. The distance between the wave crest and the source is the radius of the wave, and the wavelength is the distance between adjacent valleys/peaks. In order to make the image feel the fluctuation, within the range of the wave crest (the blue area in the picture above), the point on the inside is offset inward, and the point on the outside is offset outward. Then, the effect of the water ripple can be seen from a horizontal perspective. :

Water ripple from horizontal perspective

From the above figure, it can be seen that the nearer the vertex is from the peak, the greater the migration distance will be, and vice versa, the smaller the migration distance will be; then the cosine function can be used to calculate the migration distance.

Let's first show the final results:




Implementation steps:
1 > Customize ippleView inherited from View, parameter initialization:

// Bitmap for realizing water ripple effect
private Bitmap meshBitmap = null;
// Number of rows in a grid
private static final int MESH_WIGHT = 20;
// Column Number of Meshes
private static final int MESH_HEIGHT = 20;
// Lattice Number of Meshes
private static final int MESH_COUNT = (MESH_WIGHT + 1) * (MESH_HEIGHT + 1);

// Save the original coordinates of grid intersections
private final float[] originVerts = new float[MESH_COUNT * 2];
// Preserving coordinates of grid intersection transformed
private final float[] targetVerts = new float[MESH_COUNT * 2];

//Half the width of the water wave
private float rippleWidth = 100f;
//Water Wave Diffusion Velocity
private float rippleSpeed = 15f;
//Radius of water wave
private float rippleRadius;
//Is Water Wave Painting Executing
private boolean isRippling = false;

public RippleView(Context context) {
    super(context);
    initData(context, null);
}

public RippleView(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
}

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

private void initData(Context context, @Nullable AttributeSet attrs) {
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RippleView);
    Drawable drawable = ta.getDrawable(R.styleable.RippleView_ripple_view_image);
    if (null == drawable || !(drawable instanceof BitmapDrawable)) {
        throw new IllegalArgumentException("ripple_view_image only support images!");
    }
    meshBitmap = ((BitmapDrawable) drawable).getBitmap();
    ta.recycle();
    int width = meshBitmap.getWidth();
    int height = meshBitmap.getHeight();
    int index = 0;
    for (int row = 0; row <= MESH_HEIGHT; row++) {
        float y = height * row / MESH_HEIGHT;
        for (int col = 0; col <= MESH_WIGHT; col++) {
            float x = width * col / MESH_WIGHT;
            originVerts[index * 2] = targetVerts[index * 2] = x;
            originVerts[index * 2 + 1] = targetVerts[index * 2 + 1] = y;
            index++;
        }
    }
}

The annotations above should be very clear, so I won't repeat them.

2 > When the finger touches the bitmap, the onTouchEvent method is called back:

@Override
public boolean onTouchEvent(MotionEvent event) {
    final int action = MotionEventCompat.getActionMasked(event);
    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            showRipple(event.getX(), event.getY());
            break;
        }
    }
    return true;
}

private void showRipple(final float touchPointX, final float touchPointY) {
    if (isRippling) {
        return;
    }
    //The refresh times are calculated according to the diffusion velocity of water wave and the diagonal distance of bitmap to ensure the complete disappearance of water ripple.
    int viewLength = (int) getLength(meshBitmap.getWidth(), meshBitmap.getHeight());
    final int count = (int) ((viewLength + rippleWidth * 2) / rippleSpeed);
    final ValueAnimator valueAnimator = ValueAnimator.ofInt(1, count);
    valueAnimator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationStart(Animator animation) {
            super.onAnimationStart(animation);
            isRippling = true;
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            isRippling = false;
            valueAnimator.removeAllUpdateListeners();
            valueAnimator.removeAllListeners();
        }
    });
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            int animatorValue = (int) animation.getAnimatedValue();
            rippleRadius = animatorValue * rippleSpeed;
            warp(touchPointX, touchPointY);
        }
    });
    valueAnimator.setDuration(count * 10);
    valueAnimator.start();
}

/**
 * Obtain diagonal distance according to width and height
 *
 * @param width  wide
 * @param height high
 * @return distance
 */
private float getLength(float width, float height) {
    return (float) Math.sqrt(width * width + height * height);
}

From the above code, we can see that when clicking on the bitmap, the showRipple method will be called. This method mainly does two things:
In order to achieve the effect of gradual diffusion of water ripple until it disappears completely, the refresh times should be calculated first.




If the water ripple can disappear completely in the extreme case (the four vertices of the bitmap are the click points), the ideal effect can be achieved. The above figure is the effect map drawn under the simulated extreme condition. The smallest circle area in the middle is the initial state of the water ripple, and the outermost circle area is the end state of the water ripple. Therefore, the number of refreshes is as follows:

// Get the diagonal length of the bitmap
int viewLength = (int) getLength(meshBitmap.getWidth(), meshBitmap.getHeight());
// The diagonal length plus a wavelength is the moving distance of the water ripple, and then divided by the velocity of the wave until the number of refreshes is reached.
final int count = (int) ((viewLength + rippleWidth * 2) / rippleSpeed);

<2> The warp method is called by animation to refresh.

3 > Warp method source code is as follows:

/**
 * Computing the coordinates of grid intersections after image transformation
 *
 * @param touchPointX Touch point x coordinates
 * @param touchPointY Touch point y coordinates
 */
private void warp(float touchPointX, float touchPointY) {
    for (int i = 0; i < MESH_COUNT * 2; i += 2) {
        float originVertX = originVerts[i];
        float originVertY = originVerts[i + 1];
        float length = getLength(originVertX - touchPointX, originVertY - touchPointY);
        // Judging whether the grid intersection is in the water ripple area
        if (length > rippleRadius - rippleWidth && length < rippleRadius + rippleWidth) {
            PointF point = getRipplePoint(touchPointX, touchPointY, originVertX, originVertY);
            targetVerts[i] = point.x;
            targetVerts[i + 1] = point.y;
        } else {
            targetVerts[i] = originVerts[i];
            targetVerts[i + 1] = originVerts[i + 1];
        }
    }
    invalidate();
}

warp method traverses all grid intersections, and then determines whether the grid intersections are in the ripple area. If the grid intersections are in the ripple area, the coordinates of the offset grid intersections will be obtained by getRipplePoint method, otherwise no processing will be done. The source code for the getRipplePoint method is as follows:

/**
 * Obtaining offset coordinates of grid intersection points
 *
 * @param touchPointX Touch point x coordinates
 * @param touchPointY Touch point y coordinates
 * @param originVertX The original x-coordinate of the vertex to be offset
 * @param originVertY Original y coordinates of vertices to be offset
 * @return Post-migration coordinates
 */
private PointF getRipplePoint(float touchPointX, float touchPointY, float originVertX, float originVertY) {
    float length = getLength(originVertX - touchPointX, originVertY - touchPointY);
    //Angle between offset point and touch point
    float angle = (float) Math.atan(Math.abs((originVertY - touchPointY) / (originVertX - touchPointX)));
    //By calculating the offset distance of straight line through cosine function, the water ripple will be more vivid.
    float rate = (length - rippleRadius) / rippleWidth;
    float offset = (float) Math.cos(rate) * 10f;
    //Calculate offset distances in both horizontal and vertical directions
    float offsetX = offset * (float) Math.cos(angle);
    float offsetY = offset * (float) Math.sin(angle);
    //Coordinates after migration
    float targetX;
    float targetY;
    if (length < rippleRadius + rippleWidth && length > rippleRadius) {
        //Off-peak migration coordinates
        if (originVertX > touchPointX) {
            targetX = originVertX + offsetX;
        } else {
            targetX = originVertX - offsetX;
        }
        if (originVertY > touchPointY) {
            targetY = originVertY + offsetY;
        } else {
            targetY = originVertY - offsetY;
        }
    } else {
        //Migration coordinates in wave crest
        if (originVertX > touchPointX) {
            targetX = originVertX - offsetX;
        } else {
            targetX = originVertX + offsetX;
        }
        if (originVertY > touchPointY) {
            targetY = originVertY - offsetY;
        } else {
            targetY = originVertY + offsetY;
        }
    }
    return new PointF(targetX, targetY);
}

The getRipplePoint method does two main things:
<1> The migration distances of grid intersections in both horizontal and vertical directions are calculated by the coordinates of contact points and grid intersections. The following is a process chart for calculating migration distances of grid intersections located outside the wave crest.




Combined with the above figure, it should be easy to understand how to calculate the offset distances of grid intersections in both horizontal and vertical directions.
<2> The migration distances of grid intersections in both horizontal and vertical directions are obtained, and then the coordinates after migration are calculated according to whether the grid intersections are inside or outside the wave crest.

The warp method obtains the offset coordinates returned by the getRipplePoint method and saves them in the targetVerts array. The next step is to refresh the interface. The onDraw method is called:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawBitmapMesh(meshBitmap, MESH_WIGHT, MESH_HEIGHT, targetVerts, 0, null, 0, null);
}

warp will be animated many times until the ripple disappears completely, thus achieving the effect of fluctuation.

It won't move. That's all for today.

Added by patsman77 on Fri, 07 Jun 2019 00:17:18 +0300