自学内容网 自学内容网

android——自定义控件(不停变化的textview、开关switch、动画效果的打勾)

一、从开始数字到结束数字,不断变化

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" />


原文地址:https://blog.csdn.net/wy313622821/article/details/142813806

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!