自定义view实例:录音及播放进度控件

前言:今天和大家分享一个项目中用到的录音及播放进度控件。

一 代码实现:

1.view:

public class RecordView extends View {
    /**
     * 进度条颜色
     */
    private int progressColor;
    /**
     * 描边颜色
     */
    private int strokeColor;
    private int textColor;
    /**
     * 椭圆半径
     */
    private float radius;
    /**
     * 图片距离左边的大小
     */
    private int ivMarginLeft;
    /**
     * 图片距离右边的大小
     */
    private int ivMarginRight;
    /**
     * 文字大小
     */
    private int textSize;
    private Context mContext;
    /**
     * 控件总宽度,同时也是进度条的宽度
     */
    private int recordWith;
    private int recordHeight;

    public void setRecordText(String recordText) {
        this.recordText = recordText;
        invalidate();
    }

    /**
     * 文字
     */
    private String recordText;
    /**
     * 画笔
     */
    private Paint mPaint;
    private Paint mTextPaint;
    private Paint mIvPaint;
    private Paint mProgressPaint;
    /**
     * 默认录音时长
     */
    private int mRecordTimeLength = 60;
    /**
     * 当前时间
     */
    private int mTimeLength = 0;

    /**
     * 文件播放总时长
     */
    private int mPlayTime = 0;
    /**
     * 当前进度位置
     */
    private int mCurrentProgressPosition;

    /**
     * 当前进度
     */
    private int mCurrentProgress;
    /**
     * 控件类型
     */
    private String mRecordViewType;
    /**
     * 播放控件
     */
    public static final String PLAY_TYPE = "play";
    /**
     * 录音控件
     */
    public static final String RECORD_TYPE = "record";
    /**
     * 控件左边播放图片
     */
    private Bitmap mVoiceBitmap;
    /**
     * 控件右边播放/录音图片
     */
    private Bitmap mRecordBitmap;
    private Bitmap mRecordBeginBm;
    private Bitmap mRecordOnBm;
    private Bitmap mVoicePlayBm;
    private Bitmap mVoicePlusBm;
    private Bitmap mVoicePlayOneBm;
    private Bitmap mVoicePlayTwoBm;
    private Bitmap mVoicePlayThreeBm;
    private List<Bitmap> mPlayerIcons;
    /**
     * 播放及录音监听
     */
    private RecordViewListener mRecordViewListener;
    private Handler mHandler = new Handler();

    /**
     * 是否正在播放
     */
    boolean isPlaying;
    /**
     * 是否正在录音
     */
    boolean isRecording;
    /**
     * 是否正在准备播放
     */
    boolean isLoadingFinish = true;
    boolean isStartAnimation;

    public RecordView(Context context) {
        super(context);
    }

    public RecordView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initialize(context, attrs);
    }

    public RecordView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initialize(context, attrs);
    }

    public RecordView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initialize(context, attrs);
    }

    private void initialize(Context context, AttributeSet attributeSet) {
        mContext = context;
        TypedArray typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.RecordView);
        progressColor = typedArray.getColor(R.styleable.RecordView_progressColor, Color.parseColor("#F0F3FF"));
        strokeColor = typedArray.getColor(R.styleable.RecordView_strokeColor, Color.parseColor("#BAC6F5"));
        textColor = typedArray.getColor(R.styleable.RecordView_textColor, Color.parseColor("#607DF0"));
        radius = (int) typedArray.getDimension(R.styleable.RecordView_radius, dp2px(18));
        ivMarginLeft = (int) typedArray.getDimension(R.styleable.RecordView_imgvMarginLeft, dp2px(14));
        ivMarginRight = (int) typedArray.getDimension(R.styleable.RecordView_imgvMarginRight, dp2px(14));
        textSize = (int) typedArray.getDimension(R.styleable.RecordView_textSize, dp2px(13));
        recordWith = (int) typedArray.getDimension(R.styleable.RecordView_recordWith, dp2px(220));
        recordHeight = (int) typedArray.getDimension(R.styleable.RecordView_recordHeight, dp2px(36));
        mRecordViewType = typedArray.getString(R.styleable.RecordView_recordViewType);

        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                if (mRecordViewType.equals(PLAY_TYPE)) {
                    if (isLoadingFinish) {
                        startPlayerAnimation();
                    } else {
                        ToastUtils.showShort("加载中,请稍后");
                        startLoadingAnimation();
                    }

                }
                if (mRecordViewType.equals(RECORD_TYPE)) {
                    startRecordAnimation();
                }
            }
        });

        initPaint();
        initBitmap(context);
    }

    /**
     * 初始化画笔
     */
    private void initPaint() {
        mPaint = new Paint();
        mTextPaint = new Paint();
        mIvPaint = new Paint();
        mProgressPaint = new Paint();
        mPaint.setAntiAlias(true);
        mTextPaint.setAntiAlias(true);
        mIvPaint.setAntiAlias(true);
        mProgressPaint.setAntiAlias(true);

    }

    private void initBitmap(Context context) {
        mRecordBeginBm = BitmapFactory.decodeResource(context.getResources(), R.drawable.public_voice_begin_icon);
        mRecordOnBm = BitmapFactory.decodeResource(context.getResources(), R.drawable.public_voice_on_icon);
        mVoicePlayBm = BitmapFactory.decodeResource(context.getResources(), R.drawable.public_voice_play_icon);
        mVoicePlusBm = BitmapFactory.decodeResource(context.getResources(), R.drawable.public_voice_plluse_icon);
        mVoicePlayOneBm = BitmapFactory.decodeResource(context.getResources(), R.drawable.public_voice_on_one);
        mVoicePlayTwoBm = BitmapFactory.decodeResource(context.getResources(), R.drawable.public_voice_on_two);
        mVoicePlayThreeBm = BitmapFactory.decodeResource(context.getResources(), R.drawable.public_voice_on_three);
        //重新压缩图片尺寸
        mRecordBeginBm = Bitmap.createScaledBitmap(mRecordBeginBm, dp2px(12), dp2px(12), false);
        mRecordOnBm = Bitmap.createScaledBitmap(mRecordOnBm, dp2px(12), dp2px(12), false);
        mVoicePlayBm = Bitmap.createScaledBitmap(mVoicePlayBm, dp2px(14), dp2px(14), false);
        mVoicePlusBm = Bitmap.createScaledBitmap(mVoicePlusBm, dp2px(14), dp2px(14), false);
        mVoicePlayOneBm = Bitmap.createScaledBitmap(mVoicePlayOneBm, dp2px(18), dp2px(18), false);
        mVoicePlayTwoBm = Bitmap.createScaledBitmap(mVoicePlayTwoBm, dp2px(18), dp2px(18), false);
        mVoicePlayThreeBm = Bitmap.createScaledBitmap(mVoicePlayThreeBm, dp2px(18), dp2px(18), false);

        mPlayerIcons = new ArrayList<>();
        mPlayerIcons.add(mVoicePlayOneBm);
        mPlayerIcons.add(mVoicePlayTwoBm);
        mPlayerIcons.add(mVoicePlayThreeBm);
        if (mRecordViewType.equals(RECORD_TYPE)) {
            setRecordBitmap(mRecordBeginBm);
            setRecordText("点击录音");
        }
        if (mRecordViewType.equals(PLAY_TYPE)) {
            setRecordBitmap(mVoicePlayBm);
            setRecordText("播放" + "\t" + mPlayTime + "''");
        }
        setVoiceBitmap(mVoicePlayThreeBm);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int minimumWidth = getSuggestedMinimumWidth();
        final int minimumHeight = getSuggestedMinimumHeight();
        int width = measureWidth(minimumWidth, widthMeasureSpec);
        int height = measureHeight(minimumHeight, heightMeasureSpec);
        setMeasuredDimension(width, height);
    }

    private int measureHeight(int defaultHeight, int heightMeasureSpec) {
        int specMode = MeasureSpec.getMode(heightMeasureSpec);
        int specSize = MeasureSpec.getSize(heightMeasureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            defaultHeight = specSize;
        } else {
            defaultHeight = recordHeight;
        }
        return defaultHeight;
    }

    private int measureWidth(int defaultWith, int widthMeasureSpec) {
        int specMode = MeasureSpec.getMode(widthMeasureSpec);
        int specSize = MeasureSpec.getSize(widthMeasureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            defaultWith = specSize;
        } else {

            defaultWith = recordWith;
        }
        return defaultWith;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //绘制最外圈到椭圆
        mPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
        mPaint.setAntiAlias(true);
        mPaint.setColor(strokeColor);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(4.0f);
        float left = 2.0f;
        float top = 2.0f;
        float right = recordWith - 2.0f;
        float bottom = recordHeight - 2.0f;
        RectF rectF = new RectF(left, top, right, bottom);
        canvas.drawRoundRect(rectF, radius, radius, mPaint);

        //绘制进度
        canvas.translate(0, recordHeight / 2.0f);
        mProgressPaint.setAntiAlias(true);
        mProgressPaint.setStyle(Paint.Style.FILL);
        mProgressPaint.setColor(progressColor);
        if (mRecordViewType.equals(RECORD_TYPE)) {
            mCurrentProgressPosition = recordWith * mCurrentProgress / mRecordTimeLength;
        } else {
            if (mPlayTime == 0) {
                mCurrentProgressPosition = 0;
            } else {
                mCurrentProgressPosition = recordWith * mCurrentProgress / mPlayTime;
            }
        }
        if (mRecordViewType.equals(PLAY_TYPE) && mCurrentProgressPosition >= recordWith) {
            mCurrentProgressPosition = recordWith;
            setRecordText("播放" + "\t" + mPlayTime + "''");
        }
        if (mCurrentProgressPosition < radius) {
            //单边角度
            int angle = (int) Math.toDegrees(Math.acos((radius - mCurrentProgressPosition) / (float) radius));
            //开始的位置
            int startAngle = 180 - angle;
            //扫过的角度
            int sweetAngle = 2 * angle;

            float startTop = (float) (Math.sin((Math.PI * angle / 180)) * radius);
            RectF oval = new RectF(4, -startTop + 2, 2 * radius - 2, startTop - 2);
            canvas.drawArc(oval, startAngle, sweetAngle, false, mProgressPaint);
        } else if (mCurrentProgressPosition >= radius && mCurrentProgressPosition < recordWith - radius) {
            //绘制整个半圆弧
            RectF oval = new RectF(4, -(recordHeight / 2) + 6, radius * 2 - 2, recordHeight / 2 - 6);
            canvas.drawArc(oval, 90, 180, false, mProgressPaint);
            canvas.drawRect(radius, -(recordHeight / 2) + 6, mCurrentProgressPosition, recordHeight / 2 - 6, mProgressPaint);
        } else if (mCurrentProgressPosition >= recordWith - radius && mCurrentProgressPosition <= recordWith) {
            //绘制整个半圆弧
            RectF oval = new RectF(4, -(recordHeight / 2) + 6, radius * 2 - 2, recordHeight / 2 - 6);
            canvas.drawArc(oval, 90, 180, false, mProgressPaint);
            //绘制矩形部分进度
            canvas.drawRect(radius, -(recordHeight / 2) + 8, recordWith - radius + 4, recordHeight / 2 - 8, mProgressPaint);

            Path leftPath = new Path();
            RectF rectFLeft = new RectF(recordWith - ivMarginRight - radius, -(recordHeight / 2) + 6, recordWith - 6, recordHeight / 2 - 6);
            leftPath.addArc(rectFLeft, -90, 180);
            canvas.drawPath(leftPath, mProgressPaint);
            int angle = (int) Math.toDegrees(Math.acos(((radius - (recordWith - mCurrentProgressPosition)) / (float) radius)));
            float top1 = (float) ((Math.sin((Math.PI * angle / 180)) * radius));
            float left1 = mCurrentProgressPosition - (radius - (recordWith - mCurrentProgressPosition)) - radius;
            RectF rectFRight = new RectF(left1 - 2, -top1 + 2,
                    recordWith - 4, top1 - 2);
            mProgressPaint.setColor(Color.WHITE);
            canvas.drawArc(rectFRight, -angle, 2 * angle, false, mProgressPaint);
        }
        //绘制左边图片
        canvas.translate(ivMarginLeft, 0);
        if (mRecordViewType.equals(PLAY_TYPE)) {
            int voiceIconWith = mVoiceBitmap.getWidth();
            int voiceIconHeight = mVoiceBitmap.getHeight();
            canvas.drawBitmap(mVoiceBitmap, 0, -voiceIconHeight / 2, mIvPaint);
        }
        //绘制中间文字
        canvas.translate(-ivMarginLeft + recordWith / 2.0f, 0);
        mTextPaint.setColor(textColor);
        mTextPaint.setTextSize(textSize);
        if (recordText != null) {
            float measureText = mTextPaint.measureText(recordText);
            float textX = -measureText / 2.0f;
            float textY = textSize / 2.0f;
            canvas.drawText(recordText, textX, textY, mTextPaint);
        }
        //绘制右边图片
        canvas.translate(-ivMarginRight + recordWith / 2.0f, 0);
        //录音
        if (mRecordViewType.equals(RECORD_TYPE)) {
            int radius = mRecordBitmap.getWidth() / 2;
            canvas.drawBitmap(mRecordBitmap, -radius * 2, -radius, mIvPaint);
        }
        //播放
        if (mRecordViewType.equals(PLAY_TYPE)) {
            if (isStartAnimation) {
                isStartAnimation = false;
                drawLoadingAnimation(canvas);
            } else {
                int with = mRecordBitmap.getWidth();
                int height = mRecordBitmap.getHeight();
                canvas.drawBitmap(mRecordBitmap, -with / 2 - 4, -height / 2, mIvPaint);
            }
        }


    }

    private void drawLoadingAnimation(Canvas canvas) {
        canvas.translate(-12, 0);
        Path path = new Path();
        int width = mVoicePlusBm.getWidth();
        RectF rectF = new RectF();
        rectF.left = -width / 2;
        rectF.top = -width / 2;
        rectF.right = width / 2;
        rectF.bottom = width / 2;
        path.addArc(rectF, 20 * loadingRote, 100);
        mIvPaint.setStyle(Paint.Style.STROKE);
        mIvPaint.setStrokeWidth(4);
        mIvPaint.setColor(Color.parseColor("#607DF0"));
        canvas.drawPath(path, mIvPaint);
    }

    int loadingRote = 0;

    private void startLoadingAnimation() {
        mRecordViewListener.loadingFinish(isLoadingFinish);
        mHandler.postDelayed(loadingRunnable, 10);
    }

    private void startRecordAnimation() {
        if (isRecording) {
            if (mRecordViewListener != null) {
                mRecordViewListener.record(isRecording);
            }
            isRecording = false;
//            recordText = "播放";
            setRecordViewType(PLAY_TYPE);
            setLoadingFinish(true);
            setVoiceBitmap(mVoicePlayThreeBm);
            setRecordBitmap(mVoicePlayBm);
            //进度为0
            setCurrentProgress(0);
            //停止计时
            mHandler.removeCallbacks(timeRunnable);
            mHandler.removeCallbacks(runnable);
            mTimeLength = 0;
        } else {
            if (mRecordViewListener != null) {
                mRecordViewListener.record(isRecording);
            }
            isRecording = true;
            setRecordBitmap(mRecordOnBm);
            mHandler.postDelayed(runnable, 100);
            //计时器
            mHandler.postDelayed(timeRunnable, 0);
        }

    }

    private void startPlayerAnimation() {
        if (isPlaying) {
            if (mRecordViewListener != null) {
                mRecordViewListener.play(isPlaying);
            }
            isPlaying = false;
            setRecordBitmap(mVoicePlayBm);
            setRecordText("播放" + "\t" + mPlayTime + "''");
            //停止进度计时
            mHandler.removeCallbacks(runnable);
            //停止计时
            mHandler.removeCallbacks(timeRunnable);
            //恢复到播放的初始状态
            setVoiceBitmap(mVoicePlayThreeBm);
            //进度为0
            setCurrentProgress(0);
            mTimeLength = 0;
        } else {
            if (mRecordViewListener != null) {
                mRecordViewListener.play(isPlaying);
            }
            isPlaying = true;
            setRecordText("播放中" + "\t" + mTimeLength + "''");
            setRecordBitmap(mVoicePlusBm);
            mHandler.postDelayed(runnable, 0);
            //计时器
            setCurrentProgress(0);
            mHandler.postDelayed(timeRunnable, 1000);
        }
    }

    int iconIndex = 0;
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            setVoiceBitmap(mPlayerIcons.get(iconIndex));
            iconIndex++;
            if (iconIndex > mPlayerIcons.size() - 1) {
                iconIndex = 0;
            }
            mHandler.postDelayed(this, 500);
        }
    };
    Runnable timeRunnable = new Runnable() {
        @Override
        public void run() {
            mTimeLength++;
            setCurrentProgress(mTimeLength);
            if (isPlaying) {
                setRecordText("播放中" + "\t" + mTimeLength + "''");
            }
            if (isRecording) {
                setRecordText("录音中" + "\t" + mTimeLength + "''");
            }
            mHandler.postDelayed(this, 1000);
            if (mTimeLength > mRecordTimeLength) {
                finishRecordOrPlay();
            }
        }
    };
    Runnable loadingRunnable = new Runnable() {
        int sweetRote = 18;

        @Override
        public void run() {
            isStartAnimation = true;
            loadingRote++;
            if (loadingRote == sweetRote) {
                loadingRote = 0;
            }
            mHandler.postDelayed(this, 100);
            invalidate();

        }
    };

    /**
     * 完成播放/录音
     */
    private void finishRecordOrPlay() {
        setCurrentProgress(mRecordTimeLength);
        //停止更新进度的计时
        mHandler.removeCallbacks(runnable);
        //停止计时
        mHandler.removeCallbacks(timeRunnable);
        if (mRecordViewType.equals(PLAY_TYPE)) {
            isPlaying = false;
            setRecordBitmap(mVoicePlayBm);
            setRecordText("播放" + "\t" + mPlayTime + "''");
            //恢复到播放的初始状态
            setVoiceBitmap(mVoicePlayThreeBm);
            //恢复到播放状态之后,表示已经加载完成
            setLoadingFinish(true);
        }
        if (mRecordViewType.equals(RECORD_TYPE)) {
            isRecording = false;
            setRecordViewType(PLAY_TYPE);
            setRecordBitmap(mVoicePlayBm);
            setVoiceBitmap(mVoicePlayThreeBm);
            setRecordText("播放" + "\t" + mPlayTime + "''");
        }
        if (mRecordViewListener != null) {
            mRecordViewListener.finishEvent();
        }
        mTimeLength = 0;
        setCurrentProgress(0);
    }

    private int dp2px(final float dpValue) {
        final float scale = mContext.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

    //************************************以下为对外提供的直接调用方法***************************************************************
    public void setLoadingFinish(boolean loadingFinish) {
        isLoadingFinish = loadingFinish;
        if (isLoadingFinish) {
            setRecordBitmap(mVoicePlayBm);
            if (mHandler != null && loadingRunnable != null) {
                mHandler.removeCallbacks(loadingRunnable);
            }
        }
    }


    /**
     * 开始播放
     */
    public void startPlay() {
        if (mRecordViewType.equals(PLAY_TYPE) && isLoadingFinish) {
            startPlayerAnimation();
        } else {
            return;
        }
    }

    /**
     * 停止播放
     */
    public void stopPlay() {
        if (mRecordViewType.equals(PLAY_TYPE) && isPlaying) {
            finishRecordOrPlay();
        }
    }

    public void setRecordViewType(String recordViewType) {
        this.mRecordViewType = recordViewType;
        invalidate();
    }

    public void setProgressColor(int progressColor) {
        this.progressColor = progressColor;
        invalidate();
    }

    public void setCurrentProgress(int mCurrentProgress) {
        this.mCurrentProgress = mCurrentProgress;
        invalidate();
    }

    public void setRecordTimeLength(int recordTimeLength) {
        this.mRecordTimeLength = recordTimeLength;
    }

    public void setRecordBitmap(Bitmap recordBitmap) {
        this.mRecordBitmap = recordBitmap;
        invalidate();
    }

    public void setVoiceBitmap(Bitmap voiceBitmap) {
        this.mVoiceBitmap = voiceBitmap;
        invalidate();
    }

    public void setPlayTime(int playTime) {
        this.mPlayTime = playTime;
    }

    /**
     * 录音完成并添加或者删除当前录音之后,恢复录音控件的属性
     */
    public void convertRecordToPlay() {
        setRecordTimeLength(60);
        setRecordText("点击录音");
        setRecordBitmap(mRecordBeginBm);
        setRecordViewType(RECORD_TYPE);
    }

    /**
     * 录制完成之后,需要调用该方法更新文件录制长度,以便进行录音的预播放
     */
    public void updatePlayTime(int playTime) {
        setPlayTime(playTime);
        //修改播放总时长
        setRecordTimeLength(playTime);
        setRecordText("播放" + "\t" + playTime + "''");
    }

    /**
     * 设置控件的监听
     *
     * @param recordViewListener 监听接口
     */
    public void setRecordViewListener(RecordViewListener recordViewListener) {
        this.mRecordViewListener = recordViewListener;
    }

    /**
     * 销毁计时器对象,防止内存溢出
     */
    public void clear() {
        finishRecordOrPlay();
        if (mHandler != null) {
            mHandler.removeCallbacks(runnable);
            mHandler.removeCallbacks(loadingRunnable);
            mHandler.removeCallbacks(timeRunnable);
        }
    }
}

2.attr.xml中的自定义属性:

<?xml version="1.0" encoding="utf-8"?>
<resources>


    <!--录音控件属性-->
    <declare-styleable name="RecordView">
        <!--进度颜色-->
        <attr name="progressColor" format="color" />
        <!--描边颜色-->
        <attr name="strokeColor" format="color" />
        <!--文字颜色-->
        <attr name="textColor" format="color" />
        <!--半径-->
        <attr name="radius" format="dimension" />
        <!--图片距离左右的距离-->
        <attr name="imgvMarginLeft" format="dimension" />
        <attr name="imgvMarginRight" format="dimension" />
        <attr name="textSize" format="dimension" />
        <attr name="recordWith" format="dimension" />
        <attr name="recordHeight" format="dimension" />
        <attr name="recordViewType" format="string" />
    </declare-styleable>
</resources>

3.控件监听:

public interface RecordViewListener {
    /**
     * 是否正处于播放状态
     *
     * @param isPlaying 是否为播放状态
     */
    void play(boolean isPlaying);

    /**
     * 是否处于录音状态
     *
     * @param isRecording 是否为录音状态
     */
    void record(boolean isRecording);

    /**
     * 完成录音或者播放
     */
    void finishEvent();

    /**
     * 播放准备的监听
     * @param isFinish 是否正在准备
     */
    void loadingFinish(boolean isFinish);

}

二 使用:
需要指出,在使用时,由于录音及播放是两种不同的状态,因此,需要在recordViewType中进行声明其类型。此外,也可以通过提供的setRecordViewType方法改变控件的类型。

  <views.record.RecordView
            android:id="@+id/recorder"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginTop="40dp"
            recordWith:recordHeight="36dp"
            recordWith:recordViewType="record"
            recordWith:recordWith="220dp" />

至此,该控件的自定义基本已经完成。接下来可直接使用。需要指出的是,控件中使用的进度更新方法逻辑有点混乱,事实上可以使用valueanimator来实现,相应的,会在后期进行优化,目前基本功能是可以实现的。不足之处,欢迎大家指出。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

易小四

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值