一、从开始数字到结束数字,不断变化
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.util.AttributeSet;
import android.view.animation.AccelerateDecelerateInterpolator;import androidx.appcompat.widget.AppCompatTextView;import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.DecimalFormat;/*** FileName: NumberAnimTextView* Date: 2020/12/14* Description: TextView动画,从开始到结束,数字不断变化* History:* <author> <time> <version> <desc>*/public class NumberAnimTextView extends AppCompatTextView {private String mNumStart = "0"; //private String mNumEnd; //private long mDuration = 1000; // 动画持续时间 ms,默认1sprivate String mPrefixString = ""; // 前缀private String mPostfixString = ""; // 后缀public NumberAnimTextView(Context context) {super(context);}public NumberAnimTextView(Context context, AttributeSet attrs) {super(context, attrs);}public NumberAnimTextView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}public void setNumberString(String number) {setNumberString("0", number);}public void setNumberString(String numberStart, String numberEnd) {mNumStart = numberStart;mNumEnd = numberEnd;start();
// if (checkNumString(numberStart, numberEnd)) {
// // 数字合法 开始数字动画
// start();
// } else {
// // 数字不合法 直接调用 setText 设置最终值
// setText(mPrefixString + numberEnd + mPostfixString);
// }}public void setDuration(long mDuration) {this.mDuration = mDuration;}public void setPrefixString(String mPrefixString) {this.mPrefixString = mPrefixString;}public void setPostfixString(String mPostfixString) {this.mPostfixString = mPostfixString;}private boolean isInt; // 是否是整数/*** 校验数字的合法性** @param numberStart 开始的数字* @param numberEnd 结束的数字* @return 合法性*/private boolean checkNumString(String numberStart, String numberEnd) {try {new BigInteger(numberStart);new BigInteger(numberEnd);isInt = true;} catch (Exception e) {isInt = false;e.printStackTrace();}try {BigDecimal start = new BigDecimal(numberStart);BigDecimal end = new BigDecimal(numberEnd);return end.compareTo(start) >= 0;} catch (Exception e) {e.printStackTrace();return false;}}private void start() {ValueAnimator animator = ValueAnimator.ofObject(new BigDecimalEvaluator(), new BigDecimal(mNumStart), new BigDecimal(mNumEnd));animator.setDuration(mDuration);animator.setInterpolator(new AccelerateDecelerateInterpolator());animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator valueAnimator) {BigDecimal value = (BigDecimal) valueAnimator.getAnimatedValue();setText(mPrefixString + format(value) + mPostfixString);}});animator.start();}/*** 格式化 BigDecimal ,小数部分时保留两位小数并四舍五入** @param bd BigDecimal* @return 格式化后的 String*/private String format(BigDecimal bd) {String pattern;if (isInt) {pattern = "#,###";} else {pattern = "#,##0.00";}DecimalFormat df = new DecimalFormat(pattern);return df.format(bd);}class BigDecimalEvaluator implements TypeEvaluator {@Overridepublic Object evaluate(float fraction, Object startValue, Object endValue) {BigDecimal start = (BigDecimal) startValue;BigDecimal end = (BigDecimal) endValue;BigDecimal result = end.subtract(start);return result.multiply(new BigDecimal("" + fraction)).add(start);}}
}
设置开始数字和结束数字即可:setNumberString("12345", "1234567")
二、自定义开关switch
import android.animation.Animator;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;import androidx.annotation.Nullable;import com.fslihua.my_application_1.R;/*** 自定义开关Switch*/
public class CustomSwitch extends View implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener{private final String TAG = CustomSwitch.class.getSimpleName();//默认的宽高比例private static final float DEFAULT_WIDTH_HEIGHT_PERCENT = 0.45f;//动画最大的比例private static final float ANIMATION_MAX_FRACTION = 1;private int mWidth,mHeight;//画跑道型背景private Paint mBackgroundPain;//画背景上的字private Paint mDisaboleTextPaint;//开启private Paint mEnableTextPaint;//关闭//画白色圆点private Paint mSlidePaint;//是否正在动画private boolean isAnimation;private ValueAnimator mValueAnimator;private float mAnimationFraction;private String openText;private String closeText;private int mOpenColor = Color.GREEN;private int mCloseColor = Color.GRAY;private int mCurrentColor = Color.GRAY;//监听private OnCheckedChangeListener mCheckedChangeListener;private boolean isChecked;public CustomSwitch(Context context) {super(context);init();}public CustomSwitch(Context context, @Nullable AttributeSet attrs) {super(context, attrs);TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomSwitch);openText = typedArray.getString(R.styleable.CustomSwitch_openText);closeText = typedArray.getString(R.styleable.CustomSwitch_closeText);mOpenColor = typedArray.getColor(R.styleable.CustomSwitch_openColor, Color.GREEN);mCloseColor = typedArray.getColor(R.styleable.CustomSwitch_closeColor, Color.GRAY);mCurrentColor = mCloseColor;
// mWidth = typedArray.getInteger(R.styleable.CustomSwitch_customWidth,1);
// mHeight = typedArray.getInteger(R.styleable.CustomSwitch_customHeight,1);typedArray.recycle();init();}public CustomSwitch(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomSwitch);openText = typedArray.getString(R.styleable.CustomSwitch_openText);closeText = typedArray.getString(R.styleable.CustomSwitch_closeText);mOpenColor = typedArray.getColor(R.styleable.CustomSwitch_openColor, Color.GREEN);mCloseColor = typedArray.getColor(R.styleable.CustomSwitch_closeColor, Color.GRAY);mCurrentColor = mCloseColor;
// mWidth = typedArray.getInteger(R.styleable.CustomSwitch_customWidth,1);
// mHeight = typedArray.getInteger(R.styleable.CustomSwitch_customHeight,1);typedArray.recycle();init();}private void init(){Log.e(TAG,"init()被调用");mBackgroundPain = new Paint();mBackgroundPain.setAntiAlias(true);mBackgroundPain.setDither(true);mBackgroundPain.setColor(Color.GRAY);
// 开启的文字样式mDisaboleTextPaint = new Paint();mDisaboleTextPaint.setAntiAlias(true);mDisaboleTextPaint.setDither(true);mDisaboleTextPaint.setStyle(Paint.Style.STROKE);mDisaboleTextPaint.setColor(Color.WHITE);mDisaboleTextPaint.setTextAlign(Paint.Align.CENTER);
// 关闭的文字样式mEnableTextPaint = new Paint();mEnableTextPaint.setAntiAlias(true);mEnableTextPaint.setDither(true);mEnableTextPaint.setStyle(Paint.Style.STROKE);mEnableTextPaint.setColor(Color.parseColor("#7A88A0"));mEnableTextPaint.setTextAlign(Paint.Align.CENTER);mSlidePaint = new Paint();mSlidePaint.setColor(Color.WHITE);mSlidePaint.setAntiAlias(true);mSlidePaint.setDither(true);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int width = MeasureSpec.getSize(widthMeasureSpec);int height = (int) (width*DEFAULT_WIDTH_HEIGHT_PERCENT);setMeasuredDimension(width,height);}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mWidth = w;mHeight = h;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);drawBackground(canvas);drawSlide(canvas);}private void drawSlide(Canvas canvas){float distance = mWidth - mHeight;
// Log.e(TAG,"distance = " + distance);
// Log.e(TAG,"mAnimationFraction = " + mAnimationFraction);
// canvas.drawCircle(mHeight/2+distance*mAnimationFraction,mHeight/2,mHeight/3,mSlidePaint);canvas.drawCircle(mHeight/2+distance*mAnimationFraction,mHeight/2, mHeight/2.5f,mSlidePaint);
// canvas.drawCircle(mHeight/2+distance*mAnimationFraction, (float) (mHeight/2.2), (float) (mHeight/2.2),mSlidePaint);}private void drawBackground(Canvas canvas){Path path = new Path();RectF rectF = new RectF(0,0,mHeight,mHeight);path.arcTo(rectF,90,180);rectF.left = mWidth-mHeight;rectF.right = mWidth;path.arcTo(rectF,270,180);path.close();mBackgroundPain.setColor(mCurrentColor);canvas.drawPath(path,mBackgroundPain);// mDisaboleTextPaint.setTextSize(mHeight/2);mDisaboleTextPaint.setTextSize(mHeight/2.2f);
// mEnableTextPaint.setTextSize(mHeight/2);mEnableTextPaint.setTextSize(mHeight/2.2f);Paint.FontMetrics fontMetrics = mDisaboleTextPaint.getFontMetrics();float top = fontMetrics.top;float bottom = fontMetrics.bottom;
// 基线位置int baseLine = (int) (mHeight/2 + (bottom-top)*0.3);if (!TextUtils.isEmpty(openText)){//启用mDisaboleTextPaint.setAlpha((int) (255*mAnimationFraction));canvas.drawText(openText,mWidth*0.3f,baseLine,mDisaboleTextPaint);}if (!TextUtils.isEmpty(closeText)){//启用mEnableTextPaint.setAlpha((int) (255*(1-mAnimationFraction)));canvas.drawText(closeText,mWidth*0.7f,baseLine,mEnableTextPaint); //第二个值改变x轴的位置}}@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()){case MotionEvent.ACTION_DOWN:return true;case MotionEvent.ACTION_CANCEL:break;case MotionEvent.ACTION_UP:if (isAnimation){return true;}if (isChecked){startCloseAnimation();isChecked = false;if (mCheckedChangeListener!=null){mCheckedChangeListener.onCheckedChanged(false);}}else {startOpeAnimation();isChecked = true;if (mCheckedChangeListener!=null){mCheckedChangeListener.onCheckedChanged(true);}}return true;}return super.onTouchEvent(event);}private void startOpeAnimation(){mValueAnimator = ValueAnimator.ofFloat(0.0f, ANIMATION_MAX_FRACTION);mValueAnimator.setDuration(500);mValueAnimator.addUpdateListener(this);mValueAnimator.addListener(this);mValueAnimator.start();startColorAnimation();}private void startCloseAnimation(){mValueAnimator = ValueAnimator.ofFloat(ANIMATION_MAX_FRACTION, 0.0f);mValueAnimator.setDuration(500);mValueAnimator.addUpdateListener(this);mValueAnimator.addListener(this);mValueAnimator.start();startColorAnimation();}private void startColorAnimation(){int colorFrom = isChecked?mOpenColor:mCloseColor; //mIsOpen为true则表示要启动关闭的动画int colorTo = isChecked? mCloseColor:mOpenColor;ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);colorAnimation.setDuration(500); // millisecondscolorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animator) {mCurrentColor = (int)animator.getAnimatedValue();}});colorAnimation.start();}//设置监听public void setOnCheckedChangeListener(OnCheckedChangeListener listener){mCheckedChangeListener = listener;}public boolean isChecked() {return isChecked;}public void setChecked(boolean checked) {isChecked = checked;if (isChecked){mCurrentColor = mOpenColor;mAnimationFraction = 1.0f;}else {mCurrentColor = mCloseColor;mAnimationFraction = 0.0f;}invalidate();}@Overridepublic void onAnimationStart(Animator animator) {isAnimation = true;}@Overridepublic void onAnimationEnd(Animator animator) {isAnimation = false;}@Overridepublic void onAnimationCancel(Animator animator) {isAnimation = false;}@Overridepublic void onAnimationRepeat(Animator animator) {isAnimation = true;}@Overridepublic void onAnimationUpdate(ValueAnimator valueAnimator) {mAnimationFraction = (float) valueAnimator.getAnimatedValue();invalidate();}public interface OnCheckedChangeListener{void onCheckedChanged(boolean isChecked);}
}
attrs.xml代码
<?xml version="1.0" encoding="utf-8"?>
<resources><!-- 开关按钮CustomSwitch的样式定义 --><declare-styleable name="CustomSwitch"><attr name="closeText" format="string" /><attr name="openText" format="string" /><attr name="closeColor" format="color" /><attr name="openColor" format="color" /><attr name="customWidth" format="integer" /><attr name="customHeight" format="integer" /></declare-styleable>
</resources>
activity的xml代码:
<com.fslihua.my_application_1.cumstom_ui.CustomSwitchandroid:id="@+id/customSwitch"android:layout_marginTop="20dp"android:layout_marginStart="20dp"android:layout_width="50dp"android:layout_height="25dp"app:closeColor="#6A6A6A"app:closeText="关"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" />
三、包含动画效果的打勾
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PathMeasure
import android.util.AttributeSet
import android.util.Log
import android.util.TypedValue
import android.view.View
import android.view.animation.DecelerateInterpolator
import com.fslihua.my_application_1.R/*** 创建时间:2023/11/9* 类说明:包含动画效果的打勾*/
class AnimatedCheckView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {private val circlePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)private val checkPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)private var circlePath = Path()private var checkPath = Path()private var circleColor = 0private var checkColor = 0private var pathLength = 0f// 默认值private var circleStrokeWidth = 1fprivate var checkStrokeWidth = 1fprivate val animator = ValueAnimator().apply {interpolator = DecelerateInterpolator()addUpdateListener {invalidate()}}private var animateTime: Float = 2f // 2秒init {context.theme.obtainStyledAttributes(attrs, R.styleable.AnimatedCheckView, 0, 0).apply {try {circleColor = getColor(R.styleable.AnimatedCheckView_circleColor, Color.BLACK)checkColor = getColor(R.styleable.AnimatedCheckView_checkColor, Color.BLACK)circleStrokeWidth = getDimension(R.styleable.AnimatedCheckView_circleStrokeWidth, dp2px(context, circleStrokeWidth).toFloat())checkStrokeWidth = getDimension(R.styleable.AnimatedCheckView_checkStrokeWidth, dp2px(context, checkStrokeWidth).toFloat())animateTime = getFloat(R.styleable.AnimatedCheckView_animateTime, animateTime)Log.i("AnimatedCheckView", "circleStrokeWidth: $circleStrokeWidth, checkStrokeWidth: $checkStrokeWidth" )} finally {recycle()}}circlePaint.apply {style = Paint.Style.STROKEstrokeWidth = circleStrokeWidthcolor = circleColor}checkPaint.apply {style = Paint.Style.STROKEstrokeWidth = checkStrokeWidthcolor = checkColor}}override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)val centerX = w / 2fval centerY = h / 2fval radius = w.coerceAtMost(h) / 2f - circleStrokeWidth / 2initPaths(centerX, centerY, radius)animator.apply {setFloatValues(0f, pathLength)duration = (animateTime * 1000).toLong()start()}Log.i("AnimatedCheckView", "w: $w, h: $h, centerX: $centerX, centerY: $centerY, radius: $radius" )}private fun initPaths(centerX: Float, centerY: Float, radius: Float) {circlePath.addCircle(centerX, centerY, radius, Path.Direction.CW)checkPath.apply {moveTo(centerX - radius / 2 - radius / 15, centerY + radius / 15)lineTo(centerX - radius / 2, centerY)lineTo(centerX - radius / 8, centerY + radius / 3)lineTo(centerX + radius / 2, centerY - radius / 3)}val measure = PathMeasure(circlePath, false)val circleLength = measure.lengthmeasure.setPath(checkPath, false)val checkLength = measure.lengthpathLength = circleLength + checkLength}@SuppressLint("DrawAllocation")override fun onDraw(canvas: Canvas) {super.onDraw(canvas)val measure = PathMeasure(circlePath, false)val dst = Path()val animatedValue = animator.animatedValue as Floatval circleLength = measure.lengthif (animatedValue < circleLength) {measure.getSegment(0f, animatedValue, dst, true)canvas.drawPath(dst, circlePaint)} else {measure.getSegment(0f, circleLength, dst, true)canvas.drawPath(dst, circlePaint)measure.nextContour()measure.setPath(checkPath, false)dst.rewind()measure.getSegment(0f, animatedValue - circleLength, dst, true)canvas.drawPath(dst, checkPaint)}}fun dp2px(context: Context, dpVal: Float): Int {return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dpVal, context.resources.displayMetrics).toInt()}
}
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="AnimatedCheckView"><attr name="circleColor" format="color" /><attr name="checkColor" format="color" /><attr name="circleStrokeWidth" format="dimension" /><attr name="checkStrokeWidth" format="dimension" /><attr name="animateTime" format="float" /></declare-styleable>
</resources>
activity中的代码:
<com.fslihua.my_application_1.cumstom_ui.AnimatedCheckViewandroid:id="@+id/ac_success"android:layout_width="55dp"android:layout_height="55dp"android:layout_marginTop="64dp"app:animateTime="0.7"app:checkColor="#3ACFA5"app:checkStrokeWidth="2dp"app:circleColor="#3ACFA5"app:circleStrokeWidth="2dp"app:layout_constraintStart_toEndOf="@+id/customSwitch"app:layout_constraintTop_toTopOf="parent" />