English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
The implementation effect is as follows:
Implementation idea:
1How to achieve the effect of water level rising in a circle: Use Paint's setXfermode attribute with PorterDuff.Mode.SRC_IN to draw the intersection of the rectangle where the progress is located and the circle.
2How to achieve the water ripple effect: using Bezier curves, dynamically changing the wave peak, to achieve the effect of 'as the progress increases, the water ripple gradually becomes smaller'
Without further ado, let's look at the code.
Firstly, let's talk about the custom attribute values, what custom attribute values are available?
The circle background color: circle_color, the progress color: progress_color, the color of the progress text: text_color, the size of the progress text: text_size, and the last one: the maximum ripple height: ripple_topheight
<declare-styleable name="WaterProgressView"> <attr name="circle_color" format="color"/><!--Circle color--> <attr name="progress_color" format="color"/><!--Progress color--> <attr name="text_color" format="color"/><!--Text color--> <attr name="text_size" format="dimension"/><!--Text size--> <attr name="ripple_topheight" format="dimension"/><!--The maximum height of the water page ripple--> </declare-styleable>
Below is part of the code for the custom View: WaterProgressView:
Member variables
public class WaterProgressView extends ProgressBar { //The default circle background color public static final int DEFAULT_CIRCLE_COLOR = 0xff00cccc; //The default progress color public static final int DEFAULT_PROGRESS_COLOR = 0xff00CC;66; //The default text color public static final int DEFAULT_TEXT_COLOR = 0xffffffff; //The default text size public static final int DEFAULT_TEXT_SIZE = 18; //The default wave peak height public static final int DEFAULT_RIPPLE_TOPHEIGHT = 10; private Context mContext; private Canvas mPaintCanvas; private Bitmap mBitmap; //The brush used to draw the circle private Paint mCirclePaint; //The color of the brush used to draw the circle private int mCircleColor; //The brush used to draw the progress private Paint mProgressPaint; //The color of the brush used to draw the progress private int mProgressColor ; //The path to draw the progress private Path mProgressPath; //The maximum value of the Bezier curve peak private int mRippleTop = 10; //The brush for the progress text private Paint mTextPaint; //The color of the progress text private int mTextColor; private int mTextSize = 18; //Target progress, which is the progress of the task handled during double-click, and it will affect the amplitude of the curve private int mTargetProgress = 50; //Listen to double-click and single-click events private GestureDetector mGestureDetector; }
Get custom attribute values:
private void getAttrValue(AttributeSet attrs) { TypedArray ta = mContext.obtainStyledAttributes(attrs, R.styleable.WaterProgressView); mCircleColor = ta.getColor(R.styleable.WaterProgressView_circle_color, DEFAULT_CIRCLE_COLOR); mProgressColor = ta.getColor(R.styleable.WaterProgressView_progress_color, DEFAULT_PROGRESS_COLOR); mTextColor = ta.getColor(R.styleable.WaterProgressView_text_color, DEFAULT_TEXT_COLOR); mTextSize = (int) ta.getDimension(R.styleable.WaterProgressView_text_size, DesityUtils.sp2px(mContext,DEFAULT_TEXT_SIZE)); mRippleTop = (int)ta.getDimension(R.styleable.WaterProgressView_ripple_topheight,DesityUtils.dp2px(mContext,DEFAULT_RIPPLE_TOPHEIGHT)); ta.recycle(); }
Define the constructor, note that mProgressPaint.setXfermode
//This constructor is called when the class is instantiated public WaterProgressView(Context context) { this(context,null); } //This constructor is called when the custom View is defined in the xml file public WaterProgressView(Context context, AttributeSet attrs) { this(context, attrs,0); } public WaterProgressView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.mContext = context; getAttrValue(attrs); //Initialize the related properties of the pen initPaint(); mProgressPath = new Path(); } private void initPaint() { //Initialize the paint for drawing circles mCirclePaint = new Paint(); mCirclePaint.setColor(mCircleColor); mCirclePaint.setStyle(Paint.Style.FILL); mCirclePaint.setAntiAlias(true); mCirclePaint.setDither(true); //Initialize the paint for drawing progress mProgressPaint = new Paint(); mProgressPaint.setColor(mProgressColor); mProgressPaint.setAntiAlias(true); mProgressPaint.setDither(true); mProgressPaint.setStyle(Paint.Style.FILL); //In fact, mProgressPaint also draws a rectangle, and when the xfermode is set to PorterDuff.Mode.SRC_IN, the display is the intersection of the circle and the progress rectangle, which is a semicircle mProgressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); //Initialize the paint for drawing progress text mTextPaint = new Paint(); mTextPaint.setColor(mTextColor); mTextPaint.setStyle(Paint.Style.FILL); mTextPaint.setAntiAlias(true); mTextPaint.setDither(true); mTextPaint.setTextSize(mTextSize); }
onMeasure() method code:
@Override protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //When using it, it is necessary to explicitly define the size of the View, that is, use the measurement mode of MeasureSpec.EXACTLY int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(width, height); //Initialize the Bitmap, so that all drawCircle, drawPath, and drawText draw on the canvas where the bitmap is located, and then draw the bitmap on the canvas of the onDraw method. //Therefore, the width and height of this bitmap need to be reduced by the padding of left, top, right, and bottom. mBitmap = Bitmap.createBitmap(width-getPaddingLeft()-getPaddingRight(),height- getPaddingTop()-getPaddingBottom(), Bitmap.Config.ARGB_8888); mPaintCanvas = new Canvas(mBitmap); }
Next is the core part, the code in onDraw. First, we draw Circle, progress bar, and progress text onto the custom canvas bitmap, and then draw this bitmap onto the canvas in the onDraw method. Drawing Circle and drawText should be no problem, the key point is how to draw the progress bar. Since there is a water ripple effect and a curve, we use drawPath.
The process of drawPath is as follows:
The code for ratio is as follows, where ratio is the percentage of the current progress of the total progress.
float ratio = getProgress();*1.0f/getMax();
Since the coordinates extend in the positive direction from point B downwards and to the right, the coordinates of point A are (width, (1-ratio)*height), where width is the width of the bitmap and height is the height of the bitmap. First, we move mProgressPath to point A, and then determine the various key points of the path counterclockwise from point A, as shown in the figure. The code is as follows:
int rightTop = (int) ((1-ratio)*height); mProgressPath.moveTo(width, rightTop); mProgressPath.lineTo(width, height); mProgressPath.lineTo(0, height); mProgressPath.lineTo(0, rightTop);
So far, mProgressPath has already moved to point C, and water wave ripples need to be formed between points A and C. Therefore, a Bezier curve needs to be drawn between points A and C.
We set the highest point of the wave peak to10, then the length of one wave segment is40, it is necessary to draw width*1.0f/40, such a curve. The code to draw the curve is as follows:
int count = (int) Math.ceil(width*1.0f/(10 *4)); for(int i=0; i<count; i++) { mProgressPath.rQuadTo(10,10,2* 10,0); mProgressPath.rQuadTo(10,-10,2* 10,0); } mProgressPath.close(); mPaintCanvas.drawPath(mProgressPath,mProgressPaint);
This allows us to draw a progress bar with a rising water surface and ripples. However, we still need to implement that as the water surface rises and gets closer to the target progress, the water ripples should become smaller. Therefore, we should:10The variable mRippleTop is defined as the maximum wave peak value at the initial stage, and then 'top' is defined as the real-time wave peak value of the curve as it continuously approaches the target progress, where mTargetProgress is the target progress. This is because there is a target progress to achieve the effect of the water surface gradually leveling off as the current progress continuously approaches the target progress:
float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop;
Therefore, the code of drawPath is updated as follows:
float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop; for(int i=0; i<count; i++) { mProgressPath.rQuadTo(mRippleTop,top,2* mRippleTop,0); mProgressPath.rQuadTo(mRippleTop,-top,2* mRippleTop,0); }
So we can truly implement a water surging progress bar.
But how to implement the effect shown in the figure, where the water surface rises from 0% to the target progress during double-tap, and the water surface surges continuously at the target progress during single-tap?
Let's talk about the implementation of the double-tap effect first: it is simple, define a Handler, when a double-tap occurs, handler.postDelayed(runnable, time), progress+1In the runnable, invalidate() is used to continuously update the progress until the current progress reaches mTargetProgress.
The code is as follows
/** * Implement double-tap animation */ private void startDoubleTapAnimation() { setProgress(0); doubleTapHandler.postDelayed(doubleTapRunnable,60); } private Handler doubleTapHandler = new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); } }; //Double-tap handling thread, interval60ms to send data once private Runnable doubleTapRunnable = new Runnable() { @Override public void run() { if(getProgress() < mTargetProgress) { invalidate(); setProgress(getProgress());+1); doubleTapHandler.postDelayed(doubleTapRunnable,60); } doubleTapHandler.removeCallbacks(doubleTapRunnable); } } };
The double-tap effect has been implemented, so how to implement the single-tap effect? When a single tap occurs, the water surface needs to surge continuously for a period of time, the ripples gradually decrease, and then the water surface becomes flat. We can define a variable mSingleTapAnimationCount as the number of times the water surface surges, and then define a Handler to send an update interface message at regular intervals like the double-tap handling, mSingleTapAnimationCount-- Then we alternately make the wave peak at the beginning positive and negative, which can achieve the effect of water surging.
The core code is as follows:
private void startSingleTapAnimation() { isSingleTapAnimation = true; singleTapHandler.postDelayed(singleTapRunnable,200); } private Handler singleTapHandler = new Handler() {}} @Override public void handleMessage(Message msg) { super.handleMessage(msg); } }; //Single tap handling thread, every200ms to send data once private Runnable singleTapRunnable = new Runnable() { @Override public void run() { if (mSingleTapAnimationCount > 0) { invalidate(); mSingleTapAnimationCount--; singleTapHandler.postDelayed(singleTapRunnable,200); } singleTapHandler.removeCallbacks(singleTapRunnable); //Whether a single tap animation is in progress isSingleTapAnimation = false; //Reset the number of times the single tap animation runs to50 times mSingleTapAnimationCount = 50; } } };
Modify the code in onDraw accordingly, because the drawing logic of the curve part in drawPath is different when single tapping and double tapping, so we define a variable isSingleTapAnimation to distinguish whether a single tap animation or a double tap animation is in progress.
The modified code is as follows:
//Draw progress mProgressPath.reset(); //Start drawing path from the top right corner int rightTop = (int) ((1-ratio)*height); mProgressPath.moveTo(width, rightTop); mProgressPath.lineTo(width, height); mProgressPath.lineTo(0, height); mProgressPath.lineTo(0, rightTop); //Draw Bezier curve to form a wavy line int count = (int) Math.ceil(width*1.0f/(mRippleTop *4)); //Not in single tap animation state if (!isSingleTapAnimation && getProgress() > 0) { float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop; for(int i=0; i<count; i++) { mProgressPath.rQuadTo(mRippleTop,-top,2* mRippleTop,0); mProgressPath.rQuadTo(mRippleTop,top,2* mRippleTop,0); } } //Single tap animation state, to amplify the effect, scale up mRippleTop2times //At the same time, the curve is as shown in the figure when it is even, and it is exactly the opposite when it is odd float top = (mSingleTapAnimationCount*1.0f/50)*10; //Odd and even number curve switching ==0) {2else { for(int i=0; i<count; i++) { mProgressPath.rQuadTo(mRippleTop *2,top*2,2* mRippleTop,0); mProgressPath.rQuadTo(mRippleTop *2,-top*2,2* mRippleTop,0); } } for(int i=0; i<count; i++) { mProgressPath.rQuadTo(mRippleTop *2,-top*2,2* mRippleTop,0); mProgressPath.rQuadTo(mRippleTop *2,top*2,2* mRippleTop,0); } } } mProgressPath.close(); mPaintCanvas.drawPath(mProgressPath,mProgressPaint);
Basically, the important code and core logic are listed above.
Note:
1When drawCircle is called, consider the padding, the width and height of the circle are width and height minus the padding value, the code is as follows:
//Custom bitmap width and height int width = getWidth()-getPaddingLeft()-getPaddingRight(); int height = getHeight()-getPaddingTop()-getPaddingBottom(); //Drawing a circle mPaintCanvas.drawCircle(width/2,height/2,height/2,mCirclePaint);
2When drawText is called, it does not start drawing from the middle of the text's height, but from the baseline.
How to obtain the height coordinate of the baseline?
Paint.FontMetrics metrics = mTextPaint.getFontMetrics(); //Since ascent is above the baseline, ascent is a negative number. descent+ascent is negative, so it is subtraction rather than addition float baseLine = height*1.0f/2 - (metrics.descent+metrics.ascent)/2;
The entire code for drawText is as follows:
//Progress text drawing String text = ((int)(ratio*100))+"%"; //Get the width of the text float textWidth = mTextPaint.measureText(text); Paint.FontMetrics metrics = mTextPaint.getFontMetrics(); //descent+ascent is negative, so it is subtraction rather than addition float baseLine = height*1.0f/2 - (metrics.descent+metrics.ascent)/2; mPaintCanvas.drawText(text, width/2-textWidth/2, baseLine, mTextPaint);
3Because we need to consider padding, remember to translate the canvas in onDraw to (getPaddingLeft(), getPaddingTop()).
canvas.translate(getPaddingLeft(), getPaddingTop()); canvas.drawBitmap(mBitmap, 0, 0, null);
Remember to draw the custom bitmap onto the canvas in onDraw. The progress bar for the custom water level rising effect has been written up to here.
Summary
That's all for this article. I hope the content of this article will be helpful to everyone's learning or work, and everyone can leave a message for communication if they have any questions.
Statement: The content of this article is from the Internet, and the copyright belongs to the original author. The content is contributed and uploaded by Internet users spontaneously. This website does not own the copyright, has not been manually edited, and does not assume any relevant legal liability. If you find any content suspected of copyright infringement, please send an email to: notice#oldtoolbag.com (Please replace # with @ when sending an email to report abuse, and provide relevant evidence. Once verified, this site will immediately delete the infringing content.)