自定义View之——验证码输入框(逐行注释

以下为源码

package com.zmk.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.InputType;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import com.example.common.MyLog;

import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

public class EditTextInputView extends View {

    //对外提供提交方法
    private OnEnterClickListener onEnterClickListener;

    //字符数量,>>>对外开放
    private int textLengh = 6;
    //框框/下划线宽度,根据手机宽度计算出来,受限于rectMaxWidth
    private int rectWidth;
    //框框/下划线间距,根据框框宽度计算出来
    private int rectMargin;
    //框框间宽比
    private float rectMarginWidthRatio = 0.2f;
    //全框框宽与(屏幕-横杠长度宽)比
    private float rectWidthWidthRatio = 0.85f;
    //框框宽高比
    private float rectAspectRatio = 0.72f;
    //框框高度,根据宽高比和宽度计算出来
    private int rectHeight;
    //文字大小,根据框框宽度和文字大小与框框宽度比计算
    private int textSize;
    //文字大小与框框宽度比
    private float textSizeRectWidthRatio = 0.75f;
    //横杠宽度,根据宽度和横杠宽度与宽度比计算
    private int lineWidth;
    //横杠宽度与宽度比
    private float lineWidthWidthRatio = 0.4f;
    //横杠高度,根据横杠宽度计算
    private int lineHeight;
    //横杠高度与横杠宽度比
    private float lineHeightLineWidthRatio = 0.15f;

    //圆角大小,根据宽度和圆角半径与宽比计算
    private int radius;
    //宽与圆角半径比
    private float widthRadiusRatio = 0.2f;
    //框框/下划线粗细
    private int rectStroke = 3;

    //文字/游标颜色,>>>对外开放
    private int textColor = Color.BLACK;
    //文字显示,>>>对外开放
    private boolean textVisable = true;
    //横杠显示,>>>对外开放
    private boolean lineVisable = false;

    //框框风格
    private final int STYLE_RECT = 0;
    //下划线风格
    private final int STYLE_UNDERLINE = 1;
    //背景风格,>>>对外开放
    private int style = STYLE_RECT;

    //游标消失时间,毫秒
    private int cursorDisappearanceTime = 600;
    //游标长度,根据框框高度和游标长度与框框高度比计算
    private int cursorHeight;
    //游标长度与框框高度比
    private float cursorHeightRectHeighRatio = 0.55f;
    //游标直径,根据游标长度和游标长度与游标直径比计算
    private int cursorWidth;
    //游标直径和游标长度比
    private float cursorWidthcursorHeightRatio = 0.1f;
    //游标显示
    private boolean cursorVisible = true;

    //最大框框宽度
    private int rectMaxWidth = 130;

    //父控件宽度
    private int width;
    //父控件高度
    private int height;

    //框框/下划线画笔
    private Paint rectPaint;
    //横杠画笔
    private Paint linePaint;
    //框框/下划线颜色,>>>对外开放
    private int rectColor = Color.BLACK;
    //框框/下划线选中时颜色,>>>对外开放
    private int rectSelectColor = Color.BLUE;
    //游标画笔
    private Paint cursorPaint;
    //文字画笔
    private Paint textPaint;

    //矩形列表
    private List<Rect> rects;
    //焦点下标
    private int rectIndex = -1;

    //计时器
    private Timer timer;
    //计时任务
    private TimerTask timerTask;

    //字符数组
    private List<String> texts;

    private InputMethodManager inputManager;
    private boolean isFoucus;

    public EditTextInputView(Context context) {
        super(context);
        init();
    }

    public EditTextInputView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

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

    //初始化
    private void init() {
        //重写onCreateInputConnection方法必须,使具备输入焦点
        setFocusableInTouchMode(true);
        //获取Mannager,能用来打开软键盘
        inputManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);

        setOnKeyListener(new OnKeyListener() {
            @Override
            public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
                //获取事件类型
                int action = keyEvent.getAction();
                if (action == KeyEvent.ACTION_DOWN) {
                    //按下事件
                    if (keyCode == KeyEvent.KEYCODE_DEL) {
                        //删除键
                        //删除最后一个字符
                        removeLastText();
                        return true;
                    } else if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
                        //数字键
                        //在第一个空位添加字符
                        addText(String.valueOf(keyCode - 7));
                        //当输入后充满输入框
                        if (texts.size() >= textLengh) {
                            //隐藏软键盘
                            inputManager.hideSoftInputFromWindow(EditTextInputView.this.getWindowToken(), 0);
                        }
                        return true;
                    } else if (keyCode == KeyEvent.KEYCODE_ENTER) {
                        //Enter键
                        return true;
                    }
                } else if (action == KeyEvent.ACTION_UP) {
                    //抬起事件
                    if (keyCode == KeyEvent.KEYCODE_ENTER) {
                        //Enter键
                        //提交方法,含对外接口
                        commit();
                        return true;
                    }
                }
                return false;
            }
        });

        //框框画笔
        rectPaint = new Paint();
        rectPaint.setColor(rectColor);
        rectPaint.setStyle(Paint.Style.STROKE);//空心
        rectPaint.setStrokeWidth(rectStroke);
        //横杠画笔
        linePaint = new Paint();
        linePaint.setColor(rectColor);
        //游标画笔
        cursorPaint = new Paint();
        cursorPaint.setColor(textColor);
        //文字画笔
        textPaint = new Paint();
        textPaint.setColor(textColor);

        //矩形列表
        rects = new ArrayList<>();

        //创建计时器任务
        timerTask = new TimerTask() {
            @Override
            public void run() {
                //改变游标的显示状态
                cursorVisible = !cursorVisible;
                postInvalidate();
            }
        };
        //创建计时器
        timer = new Timer();

        //初始化字符集合
        texts = new ArrayList<>();

        //默认 Enter键点击监听事件
        onEnterClickListener = new OnEnterClickListener() {
            @Override
            public void onClick() {
                //收起软键盘
                inputManager.hideSoftInputFromWindow(EditTextInputView.this.getWindowToken(), 0);
            }
        };

        //当字符长度为奇数时,或使用下划线样式时,不显示不计算横杠
        if (textLengh % 2 != 0 || style == STYLE_UNDERLINE) {
            lineVisable = false;
        }
        //在极端情况下的健壮性
        if (textLengh > 6) {
            textLengh = 6;
        }
    }

    //带参初始化
    private void init(Context context, AttributeSet attrs) {
        //获取xml文件中的自定义属性列表
        TypedArray typedArray = context.getResources().obtainAttributes(attrs, R.styleable.EditTextInputView);
        //加载文字最大长度
        textLengh = typedArray.getInteger(R.styleable.EditTextInputView_text_lengh, textLengh);
        //加载文字颜色
        textColor = typedArray.getColor(R.styleable.EditTextInputView_text_color, textColor);
        //加载文字显示模式
        textVisable = typedArray.getBoolean(R.styleable.EditTextInputView_text_visable, textVisable);
        //加载横杠显示模式
        lineVisable = typedArray.getBoolean(R.styleable.EditTextInputView_line_visable, lineVisable);
        //加载样式
        style = typedArray.getInt(R.styleable.EditTextInputView_style, style);
        //加载背景颜色
        rectColor = typedArray.getColor(R.styleable.EditTextInputView_background_color, rectColor);
        //加载选中背景颜色
        rectSelectColor = typedArray.getColor(R.styleable.EditTextInputView_backgroud_select_color, rectSelectColor);
        //复用无参初始化
        init();
    }

    //测量
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //获取默认测量宽度
        width = MeasureSpec.getSize(widthMeasureSpec);
        //获取默认测量高度
        height = MeasureSpec.getSize(heightMeasureSpec);
        //获取宽度模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        //获取高度模式
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //声明包裹内容宽度
        int widthMeasure;
        //声明包裹内容高度
        int heightMeasure;
        //根据是否需要绘制横杠,计算包裹内容宽度
        if (lineVisable) {
            widthMeasure = (int) (rectMaxWidth * textLengh + rectMaxWidth * rectMarginWidthRatio * (textLengh - 1));
        } else {
            widthMeasure = (int) (rectMaxWidth * textLengh + rectMaxWidth * rectMarginWidthRatio * (textLengh - 1)) + lineWidth;
        }
        //在极端情况下的健壮性
        if (textLengh <= 0) {
            widthMeasure = rectMaxWidth;
        }
        //设置高度,末尾的数字为修正属性,没有特殊意义
        heightMeasure = (int) (rectMaxWidth / rectAspectRatio) + 10;
        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            //当纵横都是包裹自身
            setMeasuredDimension(widthMeasure, heightMeasure);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            //当宽度为包裹自身
            setMeasuredDimension(widthMeasure, height);
        } else if (heightMode == MeasureSpec.AT_MOST) {
            //当高度为包裹自身
            setMeasuredDimension(width, heightMeasure);
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }

        //重新测量父容器的宽与高
        width = getMeasuredWidth();
        height = getMeasuredHeight();

        //根据横杠显示状态测量框框宽度
        if (lineVisable) {
            //获得框框宽和间隔宽之和
            rectWidth = (int) ((width - lineWidth) / (float) textLengh);
            //最终框框宽度值
            rectWidth = (int) (rectWidth / (float) (1 + rectMarginWidthRatio));
        } else {
            rectWidth = (int) (width / (float) textLengh);
            rectWidth = (int) (rectWidth / (1 + rectMarginWidthRatio));
        }

        //如果宽度不合适,即计算出的框框宽度大于最大宽度,则使用最大宽度
        if (rectWidth > rectMaxWidth) {
            rectWidth = rectMaxWidth;
        }

        //根据框框宽度与宽高比计算出【框框高度】
        rectHeight = (int) (rectWidth / rectAspectRatio);

        //如果高度不合适,即计算出的框框高度大于父控件高度,再重新根据高度测量【框框宽度】
        if (rectHeight > height) {
            rectHeight = height;
            rectWidth = (int) (rectHeight * rectAspectRatio);
            rectMargin = (int) (rectWidth * rectMarginWidthRatio);
        }

        //根据框框宽度计算出【框框间隔】
        rectMargin = (int) (rectWidth * rectMarginWidthRatio);
        //根据框框宽度计算出【圆角半径】
        radius = (int) (rectWidth * widthRadiusRatio);
        //根据框框宽度计算出【横杠宽度】
        lineWidth = (int) (rectWidth * lineWidthWidthRatio);
        //根据横杠宽度计算出【横杠高度】
        lineHeight = (int) (lineWidth * lineHeightLineWidthRatio);
        //根据框框高度计算出【游标长度】
        cursorHeight = (int) (rectHeight * cursorHeightRectHeighRatio);
        //根据游标长度计算出【游标宽度】
        cursorWidth = (int) (cursorHeight * cursorWidthcursorHeightRatio);
        //为游标画笔设置宽度
        cursorPaint.setStrokeWidth(cursorWidth);
        //根据框框宽度计算出【文字大小】
        textSize = (int) (rectWidth * textSizeRectWidthRatio);
        //为文字画笔设置文字大小
        textPaint.setTextSize(textSize);
    }

    //绘制
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int middleX;
        int middleY;
        //每一个子背景的绘制属性
        int left;
        int top;
        int right;
        int bottom;

        //中心点的坐标
        middleX = width / 2;
        middleY = height / 2;

        top = middleY - rectHeight / 2;
        bottom = middleY + rectHeight / 2;

        //绘制文字,将会影响 rectIndex
        for (int i = 0; i < texts.size(); i++) {
            String s = texts.get(i);
            //解决文字居中问题
            Paint.FontMetrics fm = textPaint.getFontMetrics();
            int textX = rects.get(i).left + rectWidth / 2 - textSize / 4;
            int textY = (int) (middleY - (fm.descent - (-fm.ascent + fm.descent) / 2));
            //根据文字是否显示,来显示具体文字或显示 *
            if (textVisable) {
                canvas.drawText(s, textX, textY, textPaint);
            } else {
                canvas.drawText("*", textX, textY, textPaint);
            }
            //游标永远指向最后一个文字的下一个位置
            rectIndex = i + 1;
        }
        //处理最后一个字符被删除,上述循环无法执行的情况
        if (texts.size() == 0 && rectIndex != -1) {
            rectIndex = 0;
        }

        //如果View失焦,将游标下标改为-1,以隐藏游标
        if (!isFoucus){
            rectIndex = -1;
        }

        //绘制框框/下划线,会受到 rectIndex影响
        //循环次数为字符数量
        for (int i = 0; i < textLengh; i++) {
            if (textLengh % 2 == 0) {
                //字符长度为偶数
                if (lineVisable) {
                    //有横杠
                    left = i < textLengh / 2 ?
                            middleX - rectWidth * (textLengh / 2 - i) - rectMargin * (textLengh / 2 - i) + rectMargin / 2 - lineWidth / 2 :
                            middleX + rectWidth * (i - textLengh / 2) + rectMargin * (i - (textLengh / 2 - 1)) - rectMargin / 2 + lineWidth / 2;
                    right = i < textLengh / 2 ?
                            middleX - rectWidth * ((textLengh / 2 - 1) - i) - rectMargin * (textLengh / 2 - i) + rectMargin / 2 - lineWidth / 2 :
                            middleX + rectWidth * (i - (textLengh / 2 - 1)) + rectMargin * (i - (textLengh / 2 - 1)) - rectMargin / 2 + lineWidth / 2;
                } else {
                    //没有横杠
                    left = i < textLengh / 2 ?
                            middleX - rectWidth * (textLengh / 2 - i) - rectMargin * (textLengh / 2 - i) + rectMargin / 2 :
                            middleX + rectWidth * (i - textLengh / 2) + rectMargin * (i - (textLengh / 2 - 1)) - rectMargin / 2;
                    right = i < textLengh / 2 ?
                            middleX - rectWidth * ((textLengh / 2 - 1) - i) - rectMargin * (textLengh / 2 - i) + rectMargin / 2 :
                            middleX + rectWidth * (i - (textLengh / 2 - 1)) + rectMargin * (i - (textLengh / 2 - 1)) - rectMargin / 2;
                }
            } else {
                //字符长度为奇数
                left = i < textLengh / 2 + 1 ?
                        middleX - rectWidth * (textLengh / 2 - i) - rectMargin * (textLengh / 2 - i) - rectWidth / 2 :
                        middleX + rectWidth * (i - (textLengh / 2 + 1)) + rectMargin * (i - textLengh / 2) + rectWidth / 2;
                right = i < textLengh / 2 ?
                        middleX - rectWidth * ((textLengh / 2 - 1) - i) - rectMargin * (textLengh / 2 - i) - rectWidth / 2 :
                        middleX + rectWidth * (i - textLengh / 2) + rectMargin * (i - textLengh / 2) + rectWidth / 2;
            }
            //将游标所在位置的背景,绘制成选中颜色
            if (rectIndex == i) {
                //改变画笔颜色
                rectPaint.setColor(rectSelectColor);
            }
            switch (style) {
                case STYLE_RECT:
                    canvas.drawRoundRect(left, top, right, bottom, radius, radius, rectPaint);
                    break;
                case STYLE_UNDERLINE:
                    canvas.drawLine(left, bottom - rectStroke / 2, right, bottom - rectStroke / 2, rectPaint);
                    break;
                default:
                    canvas.drawRoundRect(left, top, right, bottom, radius, radius, rectPaint);
                    break;
            }
            if (rectIndex == i) {
                //复原画笔颜色
                rectPaint.setColor(rectColor);
            }

            //第一次循环的时候,存储每一个背景的坐标
            if (i == rects.size()) {
                rects.add(new Rect(left, top, right, bottom));
            }
        }

        //显示横杠时
        if (lineVisable) {
            //绘制横杠
            canvas.drawRect(middleX - lineWidth / 2, middleY - lineHeight / 2, middleX + lineWidth / 2, middleY + lineHeight / 2, linePaint);
        }

        //绘制游标,并处理下标为-1和下标大于长度的问题,将会受到 rectIndex影响
        if (cursorVisible && rectIndex < textLengh && rectIndex >= 0) {
            int startX = rects.get(rectIndex).left + rectWidth / 2;
            int startY = middleY - cursorHeight / 2;
            int stopY = middleY + cursorHeight / 2;
            //绘制游标
            canvas.drawLine(startX, startY, startX, stopY, cursorPaint);
        }
    }

    //触摸事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float eventX = event.getX();
        float eventY = event.getY();
        //按键抬起时事件
        if (event.getAction() == KeyEvent.ACTION_UP) {
            for (int i = 0; i < rects.size(); i++) {
                Rect rect = rects.get(i);
                if (eventX > rect.left && eventX < rect.right && eventY > rect.top && eventY < rect.bottom) {
                    Log.i("zmklog", "onTouchEvent: " + "请求弹出软键盘");
                    requestFocus();
                    inputManager.showSoftInput(this, 0);
                    //游标下标归零,将会在绘制文字时自动修正
                    rectIndex = 0;
                    invalidate();
                    break;
                }
            }
        }
        return true;
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        //启动游标闪烁Timer
        timer.schedule(timerTask, 0, cursorDisappearanceTime);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        //关闭Timer
        timer.cancel();
    }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        //当页面失焦,隐藏软键盘
        if (!hasWindowFocus) {
            inputManager.hideSoftInputFromWindow(this.getWindowToken(), 0);
        }
    }

    @Override
    protected void onFocusChanged(boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) {
        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
        if (gainFocus) {
            isFoucus = true;
        }else {
            //当该View失焦
            isFoucus = false;
            invalidate();
        }
    }

    @Override
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        //限制输入内容仅为数字?
        outAttrs.inputType = InputType.TYPE_CLASS_NUMBER;
        return super.onCreateInputConnection(outAttrs);
    }

    @Deprecated
    private void showRect(int positon) {
        rectIndex = positon;
        cursorVisible = true;
        invalidate();
    }

    private void addText(String str) {
        //限制字符长度添加
        if (texts.size() < textLengh) {
            texts.add(rectIndex, str);
            invalidate();
        }
    }

    private void removeLastText() {
        //删除最后一个字符
        if (texts.size() > 0) {
            texts.remove(texts.size() - 1);
            invalidate();
        }
    }

    //删除所有字符
    public void delectText(){
        texts.clear();
    }

    //获取所有字符
    public String getText() {
        StringBuffer stringBuffer = new StringBuffer();
        for (int i = 0; i < texts.size(); i++) {
            stringBuffer.append(texts.get(i));
        }
        return stringBuffer.toString();
    }

    //导入字符
    public void setText(String str) {
        for (int i = 0; i < str.length() && i < textLengh; i++) {
            texts.add(String.valueOf(str.charAt(i)));
        }
        invalidate();
    }

    public void setOnEnterClickListener(OnEnterClickListener onEnterClickListener) {
        this.onEnterClickListener = onEnterClickListener;
    }

    //提交
    private void commit() {
        onEnterClickListener.onClick();
    }

    //保存状态,其实并不需要
    @Nullable
    @Override
    protected Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        bundle.putParcelable("superState", super.onSaveInstanceState());
        bundle.putString("texts", getText());
        return bundle;
    }

    //复用状态,其实并不需要
    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) {
            Bundle bundle = (Bundle) state;
            if (texts.size() == 0) {
                setText(bundle.getString("texts", ""));
            }
            state = bundle.getParcelable("superState");
        }
        super.onRestoreInstanceState(state);
    }

    //帮助文档
    public void helpLog() {
        String msg = "helpLog:" +
                "\n1.最多支持6位字符输入" +
                "\n2.仅支持数字输入" +
                "\n3.对外开放的属性有:text_lengh字符长度、text_color字符颜色、text_visable是否显示具体数字、line_visable是否显示中间横杠、style圆角框/下划线两种样式、background_color背景颜色、backgroud_select_color选中时背景颜色" +
                "\n4.其中style属性的值有:STYLE_RECT 0 方框、STYLE_UNDERLINE 1 下划线" +
                "\n5.对外开放的接口有:setOnEnterClickListener() Enter键点击事件监听器" +
                "\n6.获取和填充字符:setText()、getText(),清空字符:delectText()" +
                "\n7.该View可以随宽高任意适配大小";
        MyLog.i("zmklog", msg);
    }
}

接口代码

package com.zmk.widget;

public interface OnEnterClickListener {
    void onClick();
}

MyLog代码

package com.example.common;

import android.util.Log;

public class MyLog {

    public static final String name_NORMAL = "normal_tag";

    public static void i(String msg){
        Log.i(name_NORMAL,   msg);
    }

    public static void i(String name,String msg){
        Log.i(name_NORMAL+"-"+name,   msg);
    }

    public static void d(String msg){
        Log.d(name_NORMAL,   msg);
    }

    public static void d(String name,String msg){
        Log.d(name_NORMAL+"-"+name,  msg);
    }

    public static void e(String msg){
        Log.e(name_NORMAL,   msg);
    }
    public static void e(String name,String msg){
        Log.e(name_NORMAL+"-"+name,   msg);
    }

    public static void w(String msg){
        Log.w(name_NORMAL,   msg);
    }
    public static void w(String name,String msg){
        Log.w(name_NORMAL+"-"+name,   msg);
    }
}

values.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="EditTextInputView">
        <attr name="text_lengh" format="integer"/>
        <attr name="text_color" format="color"/>
        <attr name="text_visable" format="boolean"/>
        <attr name="line_visable" format="boolean"/>
        <attr name="style" format="integer">
            <enum name="STYLE_RECT" value="0"/>
            <enum name="STYLE_UNDERLINE" value="1"/>
        </attr>
        <attr name="background_color" format="color"/>
        <attr name="backgroud_select_color" format="color"/>
    </declare-styleable>
</resources>
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值