Android 自定义队列动画

一、前言

在Android系统中,总共有三种动画,其中有属性动画、Tween动画和帧动画,基于这三种动画可以演变出更多动画效果。本篇所说的动画是基于自定义View实现的,按原则的话应该属于帧动画,我们知道帧动画也是一种队列动画。所谓队列动画本质上就是有顺序的动画,第一个顺序位的动画总是不晚于下一个动画的出现,照这么说同时出现的也属于队列动画,显然这个结论是正确的,开源动画框架lottie 通过多层layer compose实现了这类顺序动画。

效果预览

(首尾平滑过渡类型)

(非首尾平滑过渡型)

二、顺序动画的原理

  • 顺序执行 : 动画执行不存在超过前一帧的情况,但可以晚于前一顺序的时间(t >= 0)播放,也可以并行执行,但仍然保持顺序方法调用。

  • 每个执行点位具备固定的时间: duration ,每一个点位执行的动画的时间应该是一样的

  • 允许设置延迟间隔:可设置每一点位相对前一个位置的延迟开始时间

  • 每个执行点位必须在duration之后才能发起下次动画

难点:首尾平滑过渡

实际上,原理很简单,但是难点确实首尾平滑过渡问题,对于不需要平滑过渡的动画,在点位动画duration执行后,再次发起即可,但是对于需要首尾平滑过渡的动画,这个难点在于,必须保证 每个动画较前一个动画duration/n的时间间隔,同时满足下次执行动画必须在(n -1)* (duration/n) 时间之后才能执行,总结如下。

  • 开始时时间间隔:duration/n

  • 下次执行时间:(n -1)* (duration/n)

  • 下次无需考虑时间间隔,因为在(n -1)* (duration/n)时间后,就已经到到时间需要了开始动画了。

三、核心代码

3.1 首先来看非平滑过渡型

这种动画的优点是可随意修改duration和startDelayDuration,同时可以通过修改这两种参数调制出各种队列动画效果。

ini 复制代码
        if (items == null || items.isEmpty()) {
            if (items == null) {
                items = new ArrayList<>();
            } else {
                items.clear();
            }
            long millis = SystemClock.uptimeMillis();
            for (int i = 0; i < mLineCount; i++) {
                float centerX = -contentWidth / 2F + i * blockWidth + spanWidth / 2F;
                float centerY = minSpanHeight / 2F;
                Item item = new Item();
                item.positionX = centerX; 
                item.positionY = centerY;
                item.startTime = millis; //开始时间
                item.startDelayTime = i * keyFrameStartDelayInterval;  //延迟开始时间
                item.duration = keyFrameDuration;  //每一个点位的执行时间
                item.spanHeightOffset = spanHeightOffset; //缩放范围
                item.interpolator = interpolator; //插值器
                item.color = argb(random.nextFloat(),random.nextFloat(),random.nextFloat());
                items.add(item);
            }
        }

绘制逻辑

ini 复制代码
    public void draw(Canvas canvas, Paint paint, long clockTime, boolean isAllFinished) {

            if (isAllFinished) {
                //重制时间,开始下一次播放
                isFinished = false;
                startTime = clockTime;
            }
            long t = clockTime - startTime - startDelayTime;
            float offsetHeight = 0;
            if (t > 0 && !isFinished) {
                float fraction = t * 1F / duration;
                if (fraction > 1) {
                    //防止抖动
                    fraction = 1F;
                }

                float vfraction = interpolator != null ? interpolator.getInterpolation(fraction) : fraction;  //使用插值
//                float degree = vfraction * 180;
//                offsetHeight = (float) (spanHeightOffset * Math.sin(Math.toRadians(degree)));
                //  用sin曲线,当然sin曲线带有速度,和差值有些冲突,正确的做法是分时间段
                if (fraction <= 0.5f) {
                    float upFraction = vfraction * 2F;  //拉伸时间
                    if (upFraction > 1) {
                        upFraction = 1F;
                        //因为使用过插值,因此必须保证不能超过1,避免引起抖动
                    }
                    offsetHeight = spanHeightOffset * upFraction;
                } else {
                    float downFraction = (vfraction - 0.5f) * 2F; //拉伸时间
                    if (downFraction < 0F) {
                        downFraction = 0;
                        //因为使用过插值,因此必须保证不小于0,避免引起抖动
                    }
                    offsetHeight = spanHeightOffset * (1F - downFraction);
                }
            }
            float centerX = positionX;
            float centerY = positionY + offsetHeight / 2;
            paint.setColor(color);
            canvas.drawLine(centerX, centerY, centerX, -centerY, paint);

            if (t >= duration) {
                isFinished = true;
                //本次绘制完成
            }
        }

代码里写了注释,注意,我们的缩放没有使用Sin函数,原因是Sin函数本身也是插值的一种,因此我们这里使用了拉伸比例时间的方式,设计的拉伸和缩放算法。

全部代码

ini 复制代码
public class MatrixLearningView extends View {
    private static final String TAG = "MatrixLearningView";
    private final DisplayMetrics mDM;
    private TextPaint mCommonPaint;
    Matrix matrix = new Matrix();
    private float[] vertex = new float[9];
    private int mLineCount = 5;

    private int spanWidth = 10;
    private int padding = 10;
    private int minSpanHeight = 50;
    private int spanHeightOffset = 50;

    private List<Item> items = new ArrayList<>();

    private int contentWidth;
    private int blockWidth;

    private TimeInterpolator interpolator;

    private long keyFrameStartDelayInterval = 100;  //每个帧之间开始的时间间隔
    private long keyFrameDuration = 350;
    private Random random = new Random();


    public MatrixLearningView(Context context) {
        this(context, null);
    }

    public MatrixLearningView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MatrixLearningView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mDM = getResources().getDisplayMetrics();
        initPaint();

    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        if (widthMode != MeasureSpec.EXACTLY) {
            widthSize = mDM.widthPixels / 2;
        }

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (heightMode != MeasureSpec.EXACTLY) {
            heightSize = widthSize / 2;
        }
        setMeasuredDimension(widthSize, heightSize);
    }


    public float dp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
    }

    public float sp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDM);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int width = getWidth();
        int height = getHeight();
        if (width <= 1 || height <= (minSpanHeight + spanHeightOffset) || mLineCount <= 0) {
            return;
        }
        contentWidth = (spanWidth + padding) * mLineCount - padding;

        if (contentWidth > width) {
            return;
        }

        mCommonPaint.setStrokeWidth(spanWidth);
        int count = canvas.save();

        vertex[0] = 1;
        vertex[1] = 0;
        vertex[2] = width / 2F;

        vertex[3] = 0;
        vertex[4] = 1;
        vertex[5] = height / 2F;

        vertex[6] = 0;
        vertex[7] = 0;
        vertex[8] = 1;
        matrix.reset();
        matrix.setValues(vertex);

        canvas.concat(matrix);
        blockWidth = spanWidth + padding;

        if (items == null || items.isEmpty()) {
            if (items == null) {
                items = new ArrayList<>();
            } else {
                items.clear();
            }
            long millis = SystemClock.uptimeMillis();
            for (int i = 0; i < mLineCount; i++) {
                float centerX = -contentWidth / 2F + i * blockWidth + spanWidth / 2F;
                float centerY = minSpanHeight / 2F;
                Item item = new Item();
                item.positionX = centerX;
                item.positionY = centerY;
                item.startTime = millis; //开始时间
                item.startDelayTime = i * keyFrameStartDelayInterval;  //延迟开始时间
                item.duration = keyFrameDuration;  //每一个点位的执行时间
                item.spanHeightOffset = spanHeightOffset; //缩放范围
                item.interpolator = interpolator; //插值器
                item.color = argb(random.nextFloat(),random.nextFloat(),random.nextFloat());
                items.add(item);
            }
        }

        int size = items.size();
        if (size > 0) {
            boolean isAllFinished = items.get(size - 1).isFinished;
            long millis = SystemClock.uptimeMillis();
            for (int j = 0; j < size; j++) {
                Item item = items.get(j);
                item.draw(canvas, mCommonPaint, millis, isAllFinished);
            }
        }
        canvas.restoreToCount(count);
        postInvalidateDelayed(32);
    }

    private void initPaint() {
        //否则提供给外部纹理绘制
        mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mCommonPaint.setAntiAlias(true);
        mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
        interpolator = new AccelerateDecelerateInterpolator();
    }

    public void start() {
        postInvalidateDelayed(16);
    }


    static class Item {
        private long duration;
        private long startDelayTime;
        private long startTime;
        private float positionX;
        private float positionY;
        private boolean isFinished = false;
        private float spanHeightOffset;

        private int color;

        private TimeInterpolator interpolator;

        public void draw(Canvas canvas, Paint paint, long clockTime, boolean isAllFinished) {

            if (isAllFinished) {
                //重制时间,开始下一次播放
                isFinished = false;
                startTime = clockTime;
            }
            long t = clockTime - startTime - startDelayTime;
            float offsetHeight = 0;
            if (t > 0 && !isFinished) {
                float fraction = t * 1F / duration;
                if (fraction > 1) {
                    //防止抖动
                    fraction = 1F;
                }

                float vfraction = interpolator != null ? interpolator.getInterpolation(fraction) : fraction;  //使用插值
//                float degree = vfraction * 180;
//                offsetHeight = (float) (spanHeightOffset * Math.sin(Math.toRadians(degree)));
                //  用sin曲线,当然sin曲线带有速度,和差值有些冲突,正确的做法是分时间段
                if (fraction <= 0.5f) {
                    float upFraction = vfraction * 2F;  //拉伸时间
                    if (upFraction > 1) {
                        upFraction = 1F;
                        //因为使用过插值,因此必须保证不能超过1,避免引起抖动
                    }
                    offsetHeight = spanHeightOffset * upFraction;
                } else {
                    float downFraction = (vfraction - 0.5f) * 2F; //拉伸时间
                    if (downFraction < 0F) {
                        downFraction = 0;
                        //因为使用过插值,因此必须保证不小于0,避免引起抖动
                    }
                    offsetHeight = spanHeightOffset * (1F - downFraction);
                }
            }
            float centerX = positionX;
            float centerY = positionY + offsetHeight / 2;
            paint.setColor(color);
            canvas.drawLine(centerX, centerY, centerX, -centerY, paint);

            if (t >= duration) {
                isFinished = true;
                //重制绘制条件
            }
        }

    }

    public void setKeyFrameDuration(long keyFrameDuration) {
        this.keyFrameDuration = keyFrameDuration;
        items.clear();
    }

    public void setKeyFrameStartDelayInterval(long keyFrameStartDelayInterval) {
        this.keyFrameStartDelayInterval = keyFrameStartDelayInterval;
        items.clear();
    }

    public static int argb(float red, float green, float blue) {
        return ((int) (1 * 255.0f + 0.5f) << 24) |
                ((int) (red * 255.0f + 0.5f) << 16) |
                ((int) (green * 255.0f + 0.5f) << 8) |
                (int) (blue * 255.0f + 0.5f);
    }
}

3.2 接着看平滑过渡型

平滑过渡型的条件相对严格,不会出现多种构型效果,其duration和startDelayTime存在严格的关联关系。

ini 复制代码
   if (items == null || items.isEmpty()) {
            if (items == null) {
                items = new ArrayList<>();
            } else {
                items.clear();
            }
            long keyFrameStartDelayInterval = duration / mLineCount;  //时间间隔
            long time = clockTime; 
            //卡顿和线程休眠容易造成时间堆积问题,导致最终效果趋同,因此我们用自己的时钟
            float degree = 360f / mLineCount;
            for (int i = 0; i < mLineCount; i++) {
                double radians = Math.toRadians(degree * i);
                float centerX = (float) (radius * Math.cos(radians));
                float centerY = (float) (radius * Math.sin(radians));
                Item item = new Item();
                item.positionX = centerX;
                item.positionY = centerY;
                item.startTime = time;  
                item.startDelayTime = i * keyFrameStartDelayInterval;  //设置延迟时间
                item.duration = duration; //设置每个点位的执行时间
                item.spanHeightOffset = spanHeightOffset; //设置拉伸大小
                item.interpolator = interpolator; //设置插值起
                item.radians = radians; //设置角度
                item.color = argb(random.nextFloat(),random.nextFloat(),random.nextFloat()); //设置颜色
                items.add(item);
            }
        }

绘制逻辑

ini 复制代码
     public void draw(Canvas canvas, Paint paint, long clockTime) {

            long t = getRunningTime(clockTime);
            float offsetHeight = 0;
            if (t > 0) {
                float fraction = t * 1F / duration;
                if (fraction > 1) {
                    //防止抖动
                    fraction = 1F;
                }
                float vfraction = interpolator != null ? interpolator.getInterpolation(fraction) : fraction;  //使用插值
                if (fraction <= 0.5f) {
                    float upFraction = vfraction * 2F;
                    if (upFraction > 1) {
                        upFraction = 1F;
                    }
                    offsetHeight = spanHeightOffset * upFraction;
                } else {
                    float downFraction = (vfraction - 0.5f) * 2F;
                    if (downFraction < 0F) {
                        downFraction = 0;
                    }
                    offsetHeight = spanHeightOffset * (1F - downFraction);
                }

            }

            paint.setColor(color);

            if(offsetHeight != 0) {
                double xOffset = Math.cos(radians) * offsetHeight / 2F;
                double yOffset = Math.sin(radians) * offsetHeight / 2F;
                float startX = (float) (positionX + xOffset);
                float startY = (float) (positionY + yOffset);
                float stopX = (float) (positionX - xOffset);
                float stopY = (float) (positionY - yOffset);
                canvas.drawLine(startX, startY, stopX, stopY, paint);
            }else{
               // offsetHeight  为0时,线条时看不见的,这样会出现闪烁问题,这里使用Point代替
                canvas.drawPoint(positionX,positionY,paint);
            }

            if(t >= duration){
                startTime = clockTime;
                startDelayTime = 0; 
             //  这里是最大的区别,任何一个点,首次播放动画后下次,会一直延续首次的时间间隔,这里如果在使用时间间隔,将造成乱序和延迟
            }
        }

注意: t >= duration 时,startDelayTime本身在顺序位已经延迟过了,需要置为0才行。

另外如果要实现下面效果,修改缩放算法即可

ini 复制代码
  public void draw(Canvas canvas, Paint paint, long clockTime) {

            long t = getRunningTime(clockTime);
            float offsetHeight = 0;
            if (t > 0) {
                float fraction = t * 1F / duration;
                if (fraction > 1) {
                    //防止抖动
                    fraction = 1F;
                }
                float vfraction = interpolator != null ? interpolator.getInterpolation(fraction) : fraction;  //使用插值
                offsetHeight = spanHeightOffset * (1F - vfraction);
            }

            paint.setColor(color);

            if(offsetHeight != 0) {
                double xOffset = Math.cos(radians) * offsetHeight / 2F;
                double yOffset = Math.sin(radians) * offsetHeight / 2F;
                float startX = (float) (positionX + xOffset);
                float startY = (float) (positionY + yOffset);
                float stopX = (float) (positionX - xOffset);
                float stopY = (float) (positionY - yOffset);
                canvas.drawLine(startX, startY, stopX, stopY, paint);
            }else{
               // offsetHeight  为0时,线条时看不见的,这样会出现闪烁问题,这里使用Point代替
                canvas.drawPoint(positionX,positionY,paint);
            }

            if(t >= duration){
                startTime = clockTime;
                startDelayTime = 0;
             //  这里是最大的区别,任何一个点,首次播放动画后下次,会一直延续首次的时间间隔,这里如果在使用时间间隔,将造成乱序和延迟
            }
        }

完整代码

ini 复制代码
public class MatrixCircleLearningView extends View {
    private static final String TAG = "MatrixLearningView";
    private final DisplayMetrics mDM;
    private TextPaint mCommonPaint;
    Matrix matrix = new Matrix();

    private float[] vertex = new float[9];
    private int mLineCount = 20;
    private int spanHeightOffset = 50;

    private List<Item> items = new ArrayList<>();
    private TimeInterpolator interpolator;
    private long duration = 1500;  //每个帧之间开始的时间间隔
    private float padding = 20F;
    private int spanWidth = 10;

    private Random random = new Random();
    //自定义时钟,防止因线程卡顿休眠引起的时间堆积问题
    private long clockTime = 0L;

    public MatrixCircleLearningView(Context context) {
        this(context, null);
    }
    public MatrixCircleLearningView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public MatrixCircleLearningView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mDM = getResources().getDisplayMetrics();
        initPaint();

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        if (widthMode != MeasureSpec.EXACTLY) {
            widthSize = mDM.widthPixels / 2;
        }

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (heightMode != MeasureSpec.EXACTLY) {
            heightSize = widthSize / 2;
        }
        setMeasuredDimension(widthSize, heightSize);
    }


    public float dp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
    }

    public float sp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDM);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int width = getWidth();
        int height = getHeight();
        if (width <= padding || height <= padding || mLineCount <= 0) {
            return;
        }

        clockTime += 32;
        float radius = Math.min(width, height) / 2F - padding * 2;
        int count = canvas.save();

        vertex[0] = 1;
        vertex[1] = 0;
        vertex[2] = width / 2F;

        vertex[3] = 0;
        vertex[4] = 1;
        vertex[5] = height / 2F;

        vertex[6] = 0;
        vertex[7] = 0;
        vertex[8] = 1;
        matrix.reset();
        matrix.setValues(vertex);

        canvas.concat(matrix);

        if (items == null || items.isEmpty()) {
            if (items == null) {
                items = new ArrayList<>();
            } else {
                items.clear();
            }
            long keyFrameStartDelayInterval = duration / mLineCount;  //时间间隔
            long time = clockTime;
            //这类动画对时间敏感,卡顿和线程休眠容易造成时间堆积问题,导致最终效果趋同,因此我们用自己的时钟
            float degree = 360f / mLineCount;
            for (int i = 0; i < mLineCount; i++) {
                double radians = Math.toRadians(degree * i);
                float centerX = (float) (radius * Math.cos(radians));
                float centerY = (float) (radius * Math.sin(radians));
                Item item = new Item();
                item.positionX = centerX;
                item.positionY = centerY;
                item.startTime = time;
                item.startDelayTime = i * keyFrameStartDelayInterval;  //设置延迟时间
                item.duration = duration; //设置每个点位的执行时间
                item.spanHeightOffset = spanHeightOffset; //设置拉伸大小
                item.interpolator = interpolator; //设置插值起
                item.radians = radians; //设置角度
                item.color = argb(random.nextFloat(),random.nextFloat(),random.nextFloat()); //设置颜色
                items.add(item);
            }
        }

        mCommonPaint.setStrokeWidth(spanWidth);
        int size = items.size();
        if (size > 0) {
            long clockTime = this.clockTime;
            for (int j = 0; j < size; j++) {
                Item item = items.get(j);
                item.draw(canvas, mCommonPaint, clockTime);
            }

        }
        canvas.restoreToCount(count);
        postInvalidateDelayed(32);
    }

    private void initPaint() {
        //否则提供给外部纹理绘制
        mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mCommonPaint.setAntiAlias(true);
        mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
        interpolator = new AccelerateDecelerateInterpolator();
    }

    public void start() {
        postInvalidateDelayed(16);
    }


    static class Item {
        private long duration;
        private long startDelayTime;
        private long startTime;
        private float positionX;
        private float positionY;
        private float spanHeightOffset;
        private double radians;
        private TimeInterpolator interpolator;
        private int color;

        public void draw(Canvas canvas, Paint paint, long clockTime) {

            long t = getRunningTime(clockTime);
            float offsetHeight = 0;
            if (t > 0) {
                float fraction = t * 1F / duration;
                if (fraction > 1) {
                    //防止抖动
                    fraction = 1F;
                }
                float vfraction = interpolator != null ? interpolator.getInterpolation(fraction) : fraction;  //使用插值
                if (fraction <= 0.5f) {
                    float upFraction = vfraction * 2F;
                    if (upFraction > 1) {
                        upFraction = 1F;
                    }
                    offsetHeight = spanHeightOffset * upFraction;
                } else {
                    float downFraction = (vfraction - 0.5f) * 2F;
                    if (downFraction < 0F) {
                        downFraction = 0;
                    }
                    offsetHeight = spanHeightOffset * (1F - downFraction);
                }

            }

            paint.setColor(color);

            if(offsetHeight != 0) {
                double xOffset = Math.cos(radians) * offsetHeight / 2F;
                double yOffset = Math.sin(radians) * offsetHeight / 2F;
                float startX = (float) (positionX + xOffset);
                float startY = (float) (positionY + yOffset);
                float stopX = (float) (positionX - xOffset);
                float stopY = (float) (positionY - yOffset);
                canvas.drawLine(startX, startY, stopX, stopY, paint);
            }else{
               // offsetHeight  为0时,线条时看不见的,这样会出现闪烁问题,这里使用Point代替
                canvas.drawPoint(positionX,positionY,paint);
            }

            if(t >= duration){
                startTime = clockTime;
                startDelayTime = 0;
             //  这里是最大的区别,任何一个点,首次播放动画后下次,会一直延续首次的时间间隔,这里如果在使用时间间隔,将造成乱序和延迟
            }
        }
        private long getRunningTime(long clockTime) {
            return clockTime - startTime - startDelayTime;
        }

    }

    public static int argb(float red, float green, float blue) {
        return ((int) (1 * 255.0f + 0.5f) << 24) |
                ((int) (red * 255.0f + 0.5f) << 16) |
                ((int) (green * 255.0f + 0.5f) << 8) |
                (int) (blue * 255.0f + 0.5f);
    }
}

四、总结

其实,作为队列动画,其具备粒子动画的特点,我们任何时候定义粒子动画,不要忘记使用面向对象的编程思想,因此管理每一个粒子远比管理一群粒子简单的多。当然,上述动画也可以使用属性动画去实现,但是每一个点都有个Animator感觉似乎很奢侈。

下面有同感Animator实现的

github.com/ybq/Android...

相关推荐
理想不理想v2 分钟前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
知孤云出岫3 分钟前
web 渗透学习指南——初学者防入狱篇
前端·网络安全·渗透·web
贩卖纯净水.8 分钟前
Chrome调试工具(查看CSS属性)
前端·chrome
懒惰才能让科技进步33 分钟前
从零学习大模型(十二)-----基于梯度的重要性剪枝(Gradient-based Pruning)
人工智能·深度学习·学习·算法·chatgpt·transformer·剪枝
Ni-Guvara1 小时前
函数对象笔记
c++·算法
栈老师不回家1 小时前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙1 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠1 小时前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
泉崎1 小时前
11.7比赛总结
数据结构·算法
你好helloworld1 小时前
滑动窗口最大值
数据结构·算法·leetcode