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.


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

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. :

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.