About the effect of water ripple must have seen a lot of, I will be here again wordy, in order to deepen their impression. Let’s take a look at the renderings first



The implementation of this effect need not think too complicated, to achieve this effect, we also need to understand the PorterDuff and Xfermode



I think you’ve seen this picture a lot of times. These are PorterDuff’s 16 models. The effect would like to be better than you have seen, we will understand how to use one by one.

Porterduff.mode. CLEAR (Drawing will not be committed to canvas)

Porterduff.mode.src (shows the top drawing image)

Porterduff.mode. DST (shows the bottom drawing picture)

Porterduff.mode.src_over (normal rendering display, upper and lower rendering overlay)

Porterduff.mode. DST_OVER (both upper and lower levels are displayed. Lower level upper display)

Porterduff.mode. SRC_IN (Take two layers to draw the intersection. Show upper layer)

Porterduff.mode. DST_IN (Take two layers to draw intersection. Show the lower layer)

Porterduff.mode. SRC_OUT (take upper layer to draw non-intersection part)

Porterduff.mode. DST_OUT (take the lower layer to draw the non-intersection part)

Porterduff.mode. SRC_ATOP (take the lower non-intersection part and the upper intersection part)

Porterduff.mode. DST_ATOP (take the upper non-intersection part and the lower intersection part)

Porterduff.mode.xor (XOR: Remove the intersection part of two layers)

Porterduff.mode. DARKEN (Take the whole area of the two layers and make the intersection part darker)

Porterduff.mode. LIGHTEN (take both layers and LIGHTEN the intersection of colors)

Porterduff.mode. MULTIPLY (Take the color of the intersection part of the two layers after stacking)

Porterduff.mode. SCREEN (Take the entire area of the two layers and make the intersection transparent)

Xfermode has three subclasses:

AvoidXfermode specifies a color and tolerance to force Paint to avoid drawing on it (or only drawing on it).

PixelXorXfermode Applies a simple pixel xor operation when overwriting an existing color.

PorterDuffXfermode is a very powerful transformation mode that allows you to control how Paint interacts with existing Canvas images using either of the image composition diagrams. To apply the transformation mode, use the setXferMode method

paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP));

These can only show some of the composite effects, any of the 16 above, but it is not enough to achieve the water ripple effect, we also need the Bessel curve to achieve the water wave effect. We use the quadTo(x1, y1, x2,y2) method of the Path class, which belongs to the second-order bezier curve. Use a graph to show the second-order bezier curve, where (x1, y1) is the control point, (x2,y2) is the end point, and the starting point is the default Path starting point (0,0). The principle of using Bezier curve to achieve the water wave effect is: draw two ripples through the for loop, and WL represents the length of the water ripple. The four points of ripple -WL, -3/4*WL, -1/2*WL and -1/4*WL are needed to be drawn through the quadTo of path and repeated indefinitely. The effect is actually our usual sine effect. We also need to learn about the use of PATH. Let’s take a look at the renderings



The implementation code is

public class WaveView extends View {
        private Paint mPaint;
        private Path mPath;
    private Point assistPoint;
        public WaveView(Context context) {
            super(context);
        }
        public WaveView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }

        public WaveView(Context context, AttributeSet attrs) {
            super(context, attrs);
            mPath = new Path();
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            assistPoint = new Point(250.350);
            mPaint.setColor(Color.RED);
            mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        }

        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
        }
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            mPath.reset();
            mPath.moveTo(50.300);
            mPath.quadTo(assistPoint.x, assistPoint.y, 450.300);
            canvas.drawPath(mPath, mPaint);
        }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
             case MotionEvent.ACTION_DOWN:
                 case MotionEvent.ACTION_MOVE:
                     assistPoint.x = (int) event.getX();
                     assistPoint.y = (int) event.getY();
                  invalidate();
                  break;
              }
          return true; }}Copy the code

MPath. MoveTo (50, 300); This method moves the starting point to screen coordinates of (50, 300). mPath.quadTo(assistPoint.x, assistPoint.y, 450, 300); This method is the key, the corresponding source code for

public void quadTo(float x1, float y1, float x2, float y2) {
        isSimplePath = false;
        native_quadTo(mNativePath, x1, y1, x2, y2);
    }Copy the code

The first coordinate is the coordinate of the corresponding control point (assistpoint.x, assistpoint.y), and the second coordinate is the terminal coordinate, which is the terminal position coordinate of the horizontal line we see. The moving effect in the image above is caused by the position of the control point moving with the mouse position.

Let’s take a look at another rendering



The implementation code for this graph is

public class WaveViewTest extends View {
    private int width;
    private int height;

    private Path mPath;
    private Paint mPathPaint;

    private int mWaveHight = 150;// The height of the ripples
    private int mWaveWidth = 100;// The width of the water ripple
    private String mWaveColor = "#FFFFFF";

    private int maxProgress = 100;
    private int currentProgress = 0;
    private float currentY;

    public WaveViewTest(Context context) {
        this(context,null.0);
    }
    public WaveViewTest(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public WaveViewTest(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    public void setCurrent(int currentProgress,String currentText) {
        this.currentProgress = currentProgress;
    }
    public void setWaveColor(String mWaveColor){
        this.mWaveColor = mWaveColor;
    }

    private void init() {
        mPath = new Path();
        mPathPaint = new Paint();
        mPathPaint.setAntiAlias(true);
        mPathPaint.setStyle(Paint.Style.FILL);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        width = MeasureSpec.getSize(widthMeasureSpec);
        currentY = height = MeasureSpec.getSize(heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        mPathPaint.setColor(Color.parseColor(mWaveColor));
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        float currentMidY = height*(maxProgress-currentProgress)/maxProgress;
        if(currentY>currentMidY){
            currentY = currentY - (currentY-currentMidY)/10;
        }
        mPath.reset();
        mPath.moveTo(0.300);
        // The number of water ripples in the displayed area
        int waveNum = width/mWaveWidth;
        int num = 0;
        for(int i =0; i<waveNum; i++){ mPath.quadTo(mWaveWidth*(num+1),300-mWaveHight,mWaveWidth*(num+2),300);
            mPath.quadTo(mWaveWidth*(num+3),300+mWaveHight,mWaveWidth*(num+4),300);
            num+=4; } canvas.drawPath(mPath, mPathPaint); }}Copy the code

One more rendering and implementation code

public class WaveViewTest extends View {
    private int width;
    private int height;
    private Path mPath;
    private Paint mPathPaint;
    private String mWaveColor = "#FF49C12E";


    public WaveViewTest(Context context) {
        this(context,null.0);
    }
    public WaveViewTest(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public WaveViewTest(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }
    private void init() {
        mPath = new Path();
        mPathPaint = new Paint();
        mPathPaint.setAntiAlias(true);
        mPathPaint.setStyle(Paint.Style.FILL);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        width = MeasureSpec.getSize(widthMeasureSpec);
        height = MeasureSpec.getSize(heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        mPathPaint.setColor(Color.parseColor(mWaveColor));
        mPath.reset();
        mPath.moveTo(0.0);
        mPath.lineTo(0,height);
        mPath.lineTo(width,height);
        mPath.lineTo(width,0); mPath.close(); canvas.drawPath(mPath, mPathPaint); }}Copy the code



The image above is a static image we see, but it is very close to our implementation. The implementation code is

public class WaveViewTest extends View {
    private int width;
    private int height;
    private Path mPath;
    private Paint mPathPaint;

    private float mWaveWidth = 100f;// The width of the water ripple
    private String mWaveColor = "#FF49C12E";

    public WaveViewTest(Context context) {
        this(context,null.0);
    }
    public WaveViewTest(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public WaveViewTest(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }
    private void init() {
        mPath = new Path();
        mPathPaint = new Paint();
        mPathPaint.setAntiAlias(true);
        mPathPaint.setStyle(Paint.Style.FILL);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        width = MeasureSpec.getSize(widthMeasureSpec);
        height = MeasureSpec.getSize(heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(createImage(), 0.0.null);

    }
    private Bitmap createImage()
    {
        mPathPaint.setColor(Color.parseColor(mWaveColor));
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        Bitmap bmp = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bmp);
        mPath.reset();
        mPath.moveTo(0.300);
        // The number of water ripples in the displayed area
        int waveNum = width/((int)mWaveWidth);
        int num = 0;
        for(int i =0; i<waveNum; i++){ mPath.quadTo(mWaveWidth*(num+1),300-150,mWaveWidth*(num+2),300);
            mPath.quadTo(mWaveWidth*(num+3),300+150,mWaveWidth*(num+4),300);
            num+=4;
        }
        mPath.lineTo(width,height);
        mPath.lineTo(0,height);
        mPath.close();
        canvas.drawPath(mPath, mPathPaint);
        returnbmp; }}Copy the code



This effect is actually generated by the above figure using the for loop to generate the movement distance. The code is as follows

public class WaveView extends View {
    private int width;
    private int height;

    private Path mPath;
    private Paint mPathPaint;

    private float mWaveHight = 30f;
    private float mWaveWidth = 100f;// The width of the water ripple
    private String mWaveColor = "#FFFFFF";
    private  int  mWaveSpeed = 30;


    private int maxProgress = 100;
    private int currentProgress = 0;
    private float currentY;

    private float distance = 0;
    private int RefreshGap = 10;

    private static final int INVALIDATE = 0X777;
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case INVALIDATE:
                    invalidate();
                    sendEmptyMessageDelayed(INVALIDATE,RefreshGap);
                    break; }}};public WaveView(Context context) {
        this(context,null.0);
    }
    public WaveView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public WaveView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    public void setCurrent(int currentProgress,String currentText) {
        this.currentProgress = currentProgress;
    }
    public void setWaveColor(String mWaveColor){
        this.mWaveColor = mWaveColor;
    }

    private void init() {
        mPath = new Path();
        mPathPaint = new Paint();
        mPathPaint.setAntiAlias(true);
        mPathPaint.setStyle(Paint.Style.FILL);
        handler.sendEmptyMessageDelayed(INVALIDATE,100);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        width = MeasureSpec.getSize(widthMeasureSpec);
        currentY = height = MeasureSpec.getSize(heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
            canvas.drawBitmap(createImage(), 0.0.null);
    }
    private Bitmap createImage()
    {
        mPathPaint.setColor(Color.parseColor(mWaveColor));
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        Bitmap bmp = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);

        Canvas canvas = new Canvas(bmp);

        float currentMidY = height*(maxProgress-currentProgress)/maxProgress;
        if(currentY>currentMidY){
            currentY = currentY - (currentY-currentMidY)/10;
        }
        mPath.reset();
        // The reason for 0-distance is that the origin increases upwards
        mPath.moveTo(0-distance,currentY);
        // The number of water ripples in the displayed area
        int waveNum = width/((int)mWaveWidth);
        int num = 0;
        for(int i =0; i<waveNum; i++){ mPath.quadTo(mWaveWidth*(num+1)-distance,currentY-mWaveHight,mWaveWidth*(num+2)-distance,currentY);
            mPath.quadTo(mWaveWidth*(num+3)-distance,currentY+mWaveHight,mWaveWidth*(num+4)-distance,currentY);
            num+=4;
        }
        distance +=mWaveWidth/mWaveSpeed;
        distance = distance%(mWaveWidth*4);
        mPath.lineTo(width,height);
        mPath.lineTo(0,height);
        mPath.close();
        canvas.drawPath(mPath, mPathPaint);
        returnbmp; }}Copy the code

By comparing the code, you will find that it is actually the animation effect generated by constantly calling the onDraw method through mobile timed refresh and constantly changing distance. If we want to achieve our top animation effect we also need to use PorterDuff and Xfermode, about which we have talked about above. All that is left is the coordination between them using the complete implementation of the code as follows: custom view

public class WaveProgressView extends View {
    private int width;
    private int height;

    private Bitmap backgroundBitmap;

    private Path mPath;
    private Paint mPathPaint;

    private float mWaveHight = 30f;
    private float mWaveWidth = 100f;
    private String mWaveColor = "#FFFFFF";
    private  int  mWaveSpeed = 30;

    private Paint mTextPaint;
    private String currentText = "";
    private String mTextColor = "#FFFFFF";
    private int mTextSize = 35;

    private int maxProgress = 100;
    private int currentProgress = 0;
    private float currentY;

    private float distance = 0;
    private int RefreshGap = 10;

    private static final int INVALIDATE = 0X777;
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case INVALIDATE:
                    invalidate();
                    sendEmptyMessageDelayed(INVALIDATE,RefreshGap);
                    break; }}};public WaveProgressView(Context context) {
        this(context,null.0);
    }
    public WaveProgressView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public WaveProgressView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    public void setCurrent(int currentProgress,String currentText) {
        this.currentProgress = currentProgress;
        this.currentText = currentText;
    }
    public void setWaveColor(String mWaveColor){
        this.mWaveColor = mWaveColor;
    }

    private void init() {

        if(null==getBackground()){
            throw new IllegalArgumentException(String.format("background is null."));
        }else{
            backgroundBitmap = getBitmapFromDrawable(getBackground());
        }

        mPath = new Path();
        mPathPaint = new Paint();
        mPathPaint.setAntiAlias(true);
        mPathPaint.setStyle(Paint.Style.FILL);

        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setTextAlign(Paint.Align.CENTER);

        handler.sendEmptyMessageDelayed(INVALIDATE,100);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        width = MeasureSpec.getSize(widthMeasureSpec);
        currentY = height = MeasureSpec.getSize(heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if(backgroundBitmap! =null){

            canvas.drawBitmap(createImage(), 0.0.null); }}private Bitmap createImage()
    {
        mPathPaint.setColor(Color.parseColor(mWaveColor));
        mTextPaint.setColor(Color.parseColor(mTextColor));
        mTextPaint.setTextSize(mTextSize);

        mPathPaint.setColor(Color.parseColor(mWaveColor));
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        Bitmap bmp = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);

        Canvas canvas = new Canvas(bmp);

        float currentMidY = height*(maxProgress-currentProgress)/maxProgress;
        if(currentY>currentMidY){
            currentY = currentY - (currentY-currentMidY)/10;
        }
        mPath.reset();
        // The reason for 0-distance is that the origin increases upwards
        mPath.moveTo(0-distance,currentY);
        // The number of water ripples in the displayed area
        int waveNum = width/((int)mWaveWidth);
        int num = 0;
        for(int i =0; i<waveNum; i++){ mPath.quadTo(mWaveWidth*(num+1)-distance,currentY-mWaveHight,mWaveWidth*(num+2)-distance,currentY);
            mPath.quadTo(mWaveWidth*(num+3)-distance,currentY+mWaveHight,mWaveWidth*(num+4)-distance,currentY);
            num+=4;
        }
        distance +=mWaveWidth/mWaveSpeed;
        distance = distance%(mWaveWidth*4);
        mPath.lineTo(width,height);
        mPath.lineTo(0,height);
        mPath.close();
        canvas.drawPath(mPath, mPathPaint);
        int min = Math.min(width,height);
        backgroundBitmap = Bitmap.createScaledBitmap(backgroundBitmap,min,min,false);

        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP));

        canvas.drawBitmap(backgroundBitmap,0.0,paint);

        canvas.drawText(currentText, width/2, height/2, mTextPaint);
        return bmp;
    }

    private Bitmap getBitmapFromDrawable(Drawable drawable) {
        if (drawable == null) {
            return null;
        }
        if (drawable instanceof BitmapDrawable) {
            return ((BitmapDrawable) drawable).getBitmap();
        }
        try {
            Bitmap bitmap;
            bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(bitmap);
            drawable.setBounds(0.0, canvas.getWidth(), canvas.getHeight());
            drawable.draw(canvas);
            return bitmap;
        } catch (OutOfMemoryError e) {
            return null; }}}Copy the code

MainActivity.class

public class MainActivity extends Activity {

    private WaveProgressView wpv;
    private static final int FLAG_ONE = 0X0001;
    private int max_progress = 100;
    private int progress;
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            progress++;
            switch (msg.what) {
                case FLAG_ONE:
                    if (progress <= max_progress){
                        wpv.setCurrent(progress, progress + "%");
                        sendEmptyMessageDelayed(FLAG_ONE, 100);
                     }else {
                        return;
                    }
                    break; }}};@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }
    private void initView() {
        wpv = (WaveProgressView) findViewById(R.id.wpv);
        wpv.setWaveColor("#FF49C12E");
        handler.sendEmptyMessageDelayed(FLAG_ONE, 1000); }}Copy the code

Layout file


       
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="20dp">
    <com.lyxrobert.waveprogressview.WaveProgressView
        android:id="@+id/wpv"
        android:background="@drawable/bg"
        android:layout_centerInParent="true"
        android:layout_width="230dp"
        android:layout_height="230dp" />
</RelativeLayout>Copy the code

Click download source