一、从开始数字到结束数字,不断变化
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,默认1s
private 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() {
@Override
public 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 {
@Override
public 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);
}
@Override
protected 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);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
}
@Override
protected 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轴的位置
}
}
@Override
public 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); // milliseconds
colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public 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();
}
@Override
public void onAnimationStart(Animator animator) {
isAnimation = true;
}
@Override
public void onAnimationEnd(Animator animator) {
isAnimation = false;
}
@Override
public void onAnimationCancel(Animator animator) {
isAnimation = false;
}
@Override
public void onAnimationRepeat(Animator animator) {
isAnimation = true;
}
@Override
public 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.CustomSwitch
android: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 = 0
private var checkColor = 0
private var pathLength = 0f
// 默认值
private var circleStrokeWidth = 1f
private var checkStrokeWidth = 1f
private 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.STROKE
strokeWidth = circleStrokeWidth
color = circleColor
}
checkPaint.apply {
style = Paint.Style.STROKE
strokeWidth = checkStrokeWidth
color = checkColor
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
val centerX = w / 2f
val centerY = h / 2f
val radius = w.coerceAtMost(h) / 2f - circleStrokeWidth / 2
initPaths(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.length
measure.setPath(checkPath, false)
val checkLength = measure.length
pathLength = 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 Float
val circleLength = measure.length
if (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.AnimatedCheckView
android: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" />