English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
I saw a IOS component PendulumView on the Internet, which implements the pendulum animation effect. Since the original progress bar is indeed not very good-looking, I want to customize a View to achieve such an effect, which can also be used for the progress bar of the loading page in the future.
No more words, let's show the effect first
The bottom black edge is accidentally recorded during recording and can be ignored.
Since it is a custom View, we will follow the standard process, the first step is to customize the attributes
Custom attributes
Establish an attribute file
In the Android project's res->values directory to create a new attrs.xml file, the file content is as follows:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="PendulumView"> <attr name="globeNum" format="integer"/> <attr name="globeColor" format="color"/> <attr name="globeRadius" format="dimension"/> <attr name="swingRadius" format="dimension"/> </declare-styleable> </resources>
Among them, declare-The name attribute of styleable is used to refer to the attribute file in the code. The name attribute is generally written as the class name of our custom View, which is more intuitive.
Using styleale, the system can complete many constants (int[] array, index constants) for us, simplifying our development work. For example, the R.styleable.PendulumView_golbeNum used in the following code is automatically generated by the system for us.
The globeNum attribute represents the number of balls, globeColor represents the color of the balls, globeRadius represents the radius of the balls, and swingRadius represents the swing radius
Reading attribute values
Reading attribute values in the constructor of a custom view
Attribute values can also be obtained through AttributeSet, but if the attribute value is a reference type, only the ID is obtained, and it is still necessary to continue to parse the ID to obtain the actual attribute value, while TypedArray directly helps us complete the above work.
public PendulumView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //Using TypedArray to read custom attribute values TypedArray ta = context.getResources().obtainAttributes(attrs, R.styleable.PendulumView); int count = ta.getIndexCount(); for (int i = 0; i < count; i++) { int attr = ta.getIndex(i); switch (attr) { case R.styleable.PendulumView_globeNum: mGlobeNum = ta.getInt(attr, 5); break; case R.styleable.PendulumView_globeRadius: mGlobeRadius = ta.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 16, getResources().getDisplayMetrics())); break; case R.styleable.PendulumView_globeColor: mGlobeColor = ta.getColor(attr, Color.BLUE); break; case R.styleable.PendulumView_swingRadius: mSwingRadius = ta.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 16, getResources().getDisplayMetrics())); break; } } ta.recycle(); //To avoid problems when reading next time mPaint = new Paint(); mPaint.setColor(mGlobeColor); }
Rewrite the OnMeasure() method
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); //Height is the radius of the small ball+Swinging radius int height = mGlobeRadius + mSwingRadius; //Width is2*Swinging radius+)(number of small balls-1)*Diameter of the small ball int width = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1) + mSwingRadius; //If the measurement mode is EXACTLY, use the recommended value directly; if not EXACTLY (usually handle the wrap_content case), use the calculated width and height setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize : width, (heightMode == MeasureSpec.EXACTLY) ? heightSize : height); }
Among them
int height = mGlobeRadius + mSwingRadius;
<pre name="code" class="java">int width = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1) + mSwingRadius;
Used to handle the measurement mode of AT_MOST, which is usually set to wrap_content for the width and height of a custom View, at this time, the width and height of the View are calculated through the number of small balls, their radius, and the swinging radius, as shown in the following figure:
with the number of small balls5As an example, the size of View is the red rectangular area in the following figure
Override the onDraw() method
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //Draw other balls except the two small balls on the left and right for (int i = 0; i < mGlobeNum - 2; i++) { canvas.drawCircle(mSwingRadius + (i + 1) * 2 * mGlobeRadius, mSwingRadius, mGlobeRadius, mPaint); } if (mLeftPoint == null || mRightPoint == null) { //Initialize the coordinates of the two small balls on the left and right mLeftPoint = new Point(mSwingRadius, mSwingRadius); mRightPoint = new Point(mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1), mSwingRadius); //Start the swinging animation startPendulumAnimation(); } //Draw the two small balls on the left and right canvas.drawCircle(mLeftPoint.x, mLeftPoint.y, mGlobeRadius, mPaint); canvas.drawCircle(mRightPoint.x, mRightPoint.y, mGlobeRadius, mPaint); }
The onDraw() method is the key to custom View, where the display effect of the View is drawn. The code first draws other balls except the two small balls on the left and right, then judges the coordinates of the two small balls. If it is the first drawing, the coordinate values are empty, then initialize the coordinates of the two small balls and start the animation. Finally, draw the two small balls through the x, y values of mLeftPoint and mRightPoint.
where mLeftPoint and mRightPoint are android.graphics.Point objects, they are simply used to store the x, y coordinate information of the two small balls on the left and right.
Using property animation
public void startPendulumAnimation() { //Using property animation final ValueAnimator anim = ValueAnimator.ofObject(new TypeEvaluator() { @Override public Object evaluate(float fraction, Object startValue, Object endValue) { //The parameter fraction is used to represent the completion degree of the animation, and we calculate the current animation value based on it. double angle = Math.toRadians(90 * fraction); int x = (int) ((mSwingRadius - mGlobeRadius) * Math.sin(angle)); int y = (int) ((mSwingRadius - mGlobeRadius) * Math.cos(angle)); Point point = new Point(x, y); return point; } }, new Point(), new Point()); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { Point point = (Point) animation.getAnimatedValue(); //Obtain the current fraction value float fraction = anim.getAnimatedFraction(); //Determine whether fraction first decreases and then increases, indicating whether it is in the state of about to swing upwards //Switch the ball each time it is about to swing upwards if (lastSlope && fraction > mLastFraction) { isNext = !isNext; } //The animation effect is achieved by continuously changing the x, y coordinates of the left and right balls //Use isNext to determine whether the left ball or the right ball should move if (isNext) { //When the left ball swings, the right ball is placed in the initial position mRightPoint.x = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1); mRightPoint.y = mSwingRadius; mLeftPoint.x = mSwingRadius - point.x; mLeftPoint.y = mGlobeRadius + point.y; } else { //When the right ball swings, the left ball is placed in the initial position mLeftPoint.x = mSwingRadius; mRightPoint.y = mSwingRadius; mRightPoint.x = mSwingRadius + (mGlobeNum - 1) * mGlobeRadius * 2 + point.x; mRightPoint.y = mGlobeRadius + point.y; } invalidate(); lastSlope = fraction < mLastFraction; mLastFraction = fraction; } }); //Set the loop to play indefinitely anim.setRepeatCount(ValueAnimator.INFINITE); //Set the loop mode to play in reverse order anim.setRepeatMode(ValueAnimator.REVERSE); anim.setDuration(200); //Set the interpolator to control the rate of change of the animation anim.setInterpolator(new DecelerateInterpolator()); anim.start(); }
The use of ValueAnimator.ofObject method is to operate on Point objects, making it more vivid and concrete. Moreover, a custom TypeEvaluator object is used through the ofObject method, thereby obtaining the fraction value, which is a decimal from 0-1The decimal representing the change. Therefore, the last two parameters startValue (new Point()), endValue (new Point()) have no actual significance and can be omitted directly. The reason for writing them here is mainly for ease of understanding. Similarly, ValueAnimator.ofFloat(0f) can be used directly. 1f) method to obtain a decimal from 0-1The decimal representing the change.
final ValueAnimator anim = ValueAnimator.ofObject(new TypeEvaluator() { @Override public Object evaluate(float fraction, Object startValue, Object endValue) { //The parameter fraction is used to represent the completion degree of the animation, and we calculate the current animation value based on it. double angle = Math.toRadians(90 * fraction); int x = (int) ((mSwingRadius - mGlobeRadius) * Math.sin(angle)); int y = (int) ((mSwingRadius - mGlobeRadius) * Math.cos(angle)); Point point = new Point(x, y); return point; } }, new Point(), new Point());
Through fraction, we calculate the angle change value of the ball's swing, 0-90 degrees
mSwingRadius-mGlobeRadius represents the length of the green line in the figure, the swinging path, and the path of the ball's center is a circle with (mSwingRadius-mGlobeRadius) as the radius of the arc, and the changing x value is (mSwingRadius-mGlobeRadius)*sin(angle), the changing y value is (mSwingRadius-mGlobeRadius)*cos(angle)
The actual center coordinates of the corresponding ball are (mSwingRadius-x, mGlobeRadius+y)
The motion path of the ball on the right is similar to that on the left, but the direction is different. The actual center coordinates of the ball on the right (mSwingRadius + (mGlobeNum - 1) * mGlobeRadius * 2 + x, mGlobeRadius+y)
The vertical coordinates of the balls on both sides are the same, only the horizontal coordinates are different.
float fraction = anim.getAnimatedFraction(); //Determine whether fraction first decreases and then increases, indicating whether it is in the state of about to swing upwards //Switch the ball each time it is about to swing upwards if (lastSlope && fraction > mLastFraction) { isNext = !isNext; } //Record whether the last fraction is continuously decreasing lastSlope = fraction < mLastFraction; //Record the last fraction mLastFraction = fraction;
These two codes are used to calculate when to switch the moving ball. This animation is set to loop playback, and the loop mode is reverse playback, so one cycle of the animation is the process of the ball being thrown up and then falling down. In this process, the value of fraction changes from 0 to1and then by}}1to 0. So when is the beginning of a new cycle of the animation? It is when the ball is about to be thrown up. At this time, switch the moving ball to achieve the animation effect where the left ball falls down after the right ball throws up, and the right ball falls down after the left ball throws up.
So how do we capture this time point?
The fraction value of the ball increases continuously when the ball is thrown up, and the fraction value decreases continuously when the ball falls down. The moment the ball is about to be thrown up is the moment when the fraction changes from decreasing continuously to increasing continuously. The code records whether the last fraction is decreasing continuously, and then compares whether this fraction is increasing continuously. If both conditions are met, then switch the moving ball.
anim.setDuration(200); //Set the interpolator to control the rate of change of the animation anim.setInterpolator(new DecelerateInterpolator()); anim.start();
Set the duration of the animation to200 milliseconds, readers can change this value to achieve the purpose of modifying the ball's swing speed.
Set the interpolator for the animation, since the ball throw is a process of gradual deceleration, and the fall is a process of gradual acceleration, so use DecelerateInterpolator to achieve the deceleration effect, and it becomes the acceleration effect when played in reverse.
The custom View progress bar with the swing effect is realized by starting the animation! Run it quickly and see the effect!
That's all for this article. I hope it will be helpful to your studies, and I also hope everyone will support the Yelling Tutorial more.
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 responsibility. If you find any content suspected of copyright infringement, please send an email to: notice#oldtoolbag.com (Please replace # with @ when sending an email for reporting, and provide relevant evidence. Once verified, this site will immediately delete the infringing content.)