《高仿支付宝运动轨迹七日曲线图》

最近在项目中要仿支付宝做一个运动轨迹的曲线图。话不多说先上图

仿支付宝运动轨迹曲线图
仿支付宝运动轨迹曲线图

需求 过去7天有动画、时间轴、阴影、点击时显示步数、横向轴有步数均值

我是通过自定义View实现这些需求的,绘制之前需要掌握自定义View的基本操作方法。

代码如下

import 你项目的包名

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.Point;
import android.graphics.Shader;
import android.graphics.SweepGradient;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;

import com.zdkj.module_sport.R;
import com.zdkj.module_sport.view.activity.api.e.SpetEvent;

import org.greenrobot.eventbus.EventBus;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * @author guoqiang
 * @date 2019/12/18.
 * description:1580321809@qq.com
 */

public class UshareChatView extends View {

    private Paint xianPaint;
    private Paint pointPaint;
    private Paint datelinePaint;
    private Paint circlePaint;
    private Paint textRulerPaint;
    private Paint textPointPaint;

    private Path linePath;
    private Path tablePath;
    private Path mFillPath = new Path();

    private int mWidth, mHeight;

    private List<DataBean> dataList = new ArrayList<>();
    private String[] xline = new String[7];


    private Point[] linePoints;
    private int stepStart;
    private int stepEnd;
    private int stepSpace;
    private int stepSpaceDefault = 43;
    private int stepSpaceDP = stepSpaceDefault;
    private int topSpace, bottomSpace;
    private int tablePadding;
    private int tablePaddingDP = 20;

    private int maxValue, minValue;
    private int rulerValueDefault = 5000;
    private int rulerValue = rulerValueDefault;
    private int rulerValuePadding;
    private int rulerValuePaddingDP = 8;
    private float heightPercent = 0.618f;

    private float lineWidthDP = 2f;

    private int pointColor = Color.parseColor("#5FB4F8");
    private float pointWidthDefault = 2f;
    private float pointWidthDP = pointWidthDefault;

    private int datelineColor = Color.parseColor("#BBBBBB");
    private float datelineWidthDefault = 0.5f;
    private float datelineWidthDP = 0.5f;//锚点宽度dp

    private int tableColor = Color.parseColor("#BBBBBB");
    private float tableWidthDP = 0.5f;

    private int rulerTextColor = tableColor;
    private float rulerTextSizeSP = 10f;

    private int pointTextColor = Color.parseColor("#009688");
    private float pointTextSizeSP = 10f;

    private boolean isShowTable = true;
    private boolean isBezierLine = true;
    private boolean isInitialized = false;
    private boolean isPlayAnim = true;

    private ValueAnimator valueAnimator;
    private float currentValue = 0f;
    private boolean isAnimating = false;
    private int endX;
    private int startX;
    private int index;


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

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

    public UshareChatView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        setupView();
    }

    private void setupView() {
        xianPaint = new Paint();
        xianPaint.setStyle(Paint.Style.STROKE);
        xianPaint.setAntiAlias(true);
        LinearGradient linearGradient = new LinearGradient(100, 100, 500, 500, Color.parseColor("#67C0F8"), Color.parseColor("#4487F6"), Shader.TileMode.CLAMP);
        int[] colorSweep = {Color.parseColor("#67C0F8"), Color.parseColor("#4487F6"), Color.parseColor("#67C0F8")};
        float[] position = {0f, 0.5f, 1f};
        Shader shader = new SweepGradient(getWidth() >> 1, getHeight() >> 1, colorSweep, position);
        xianPaint.setShader(shader);
        xianPaint.setShader(linearGradient);
        xianPaint.setStrokeWidth(dip2px(lineWidthDP));

        pointPaint = new Paint();
        pointPaint.setAntiAlias(true);
        pointPaint.setStyle(Paint.Style.STROKE);
        pointPaint.setColor(pointColor);
        pointPaint.setStrokeWidth(dip2px(pointWidthDP));

        datelinePaint = new Paint();
        datelinePaint.setAntiAlias(true);
        datelinePaint.setStyle(Paint.Style.STROKE);
        datelinePaint.setColor(datelineColor);
        datelinePaint.setStrokeWidth(dip2px(datelineWidthDP));

        circlePaint = new Paint();
        circlePaint.setAntiAlias(true);
        circlePaint.setStyle(Paint.Style.STROKE);
        circlePaint.setColor(tableColor);
        circlePaint.setStrokeWidth(dip2px(tableWidthDP));

        textRulerPaint = new Paint();
        textRulerPaint.setAntiAlias(true);
        textRulerPaint.setStyle(Paint.Style.FILL);
        textRulerPaint.setTextAlign(Paint.Align.CENTER);
        textRulerPaint.setColor(rulerTextColor);
        textRulerPaint.setTextSize(sp2px(rulerTextSizeSP));

        textPointPaint = new Paint();
        textPointPaint.setAntiAlias(true);
        textPointPaint.setStyle(Paint.Style.FILL);
        textPointPaint.setTextAlign(Paint.Align.CENTER);
        textPointPaint.setColor(pointTextColor);
        textPointPaint.setTextSize(sp2px(pointTextSizeSP));

        linePath = new Path();
        tablePath = new Path();

        resetParam();
    }

    private void initAnim() {
        valueAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(dataList.size() * 150);
        valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentValue = (float) animation.getAnimatedValue();
                postInvalidate();
            }
        });
        valueAnimator.addListener(new AnimatorListenerAdapter() {

            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
                currentValue = 0f;
                isAnimating = true;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                currentValue = 1f;
                isAnimating = false;
                isPlayAnim = false;
            }
        });
        valueAnimator.setStartDelay(500);
    }

    private void resetParam() {
        linePath.reset();
        tablePath.reset();
        stepSpace = dip2px(stepSpaceDP);
        tablePadding = dip2px(tablePaddingDP);
        rulerValuePadding = dip2px(rulerValuePaddingDP);
        stepStart = tablePadding * (isShowTable ? 2 : 1);
        stepEnd = stepStart + stepSpace * (dataList.size() - 1);
        topSpace = bottomSpace = tablePadding;
        linePoints = new Point[7];
        initAnim();
        isInitialized = false;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = tablePadding + getTableEnd() + getPaddingLeft() + getPaddingRight();
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        if (MeasureSpec.EXACTLY == heightMode) {
            height = getPaddingTop() + getPaddingBottom() + height;
        }
        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 onFinishInflate() {
        super.onFinishInflate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.TRANSPARENT);
        canvas.translate(0f, mHeight / 2f + (getViewDrawHeight() + topSpace + bottomSpace) / 2f);
        if (!isInitialized) {
            setupLine();
        }

        drawCircle(canvas);
        drawTable(canvas);
        drawLinePoints(canvas);
    }


    private void drawText(Canvas canvas, Paint textPaint, String text, float x, float y) {
        canvas.drawText(text, x, y, textPaint);
    }

    private void drawRulerYText(Canvas canvas, String text, float x, float y) {
        textRulerPaint.setTextAlign(Paint.Align.RIGHT);
        Paint.FontMetrics fontMetrics = textRulerPaint.getFontMetrics();
        float fontTotalHeight = fontMetrics.bottom - fontMetrics.top;
        float offsetY = fontTotalHeight / 2 - fontMetrics.bottom;
        float newY = y + offsetY;
        float newX = x - rulerValuePadding;
        drawText(canvas, textRulerPaint, text, newX, newY);
    }

    private void drawRulerXText(Canvas canvas, String text, float x, float y) {
        textRulerPaint.setTextAlign(Paint.Align.CENTER);
        Paint.FontMetrics fontMetrics = textRulerPaint.getFontMetrics();
        float fontTotalHeight = fontMetrics.bottom - fontMetrics.top;
        float offsetY = fontTotalHeight / 2 - fontMetrics.bottom;
        float newY = y + offsetY + rulerValuePadding;
        drawText(canvas, textRulerPaint, text, x, newY);
    }

    private void drawLinePointText(Canvas canvas, String text, float x, float y) {
        textPointPaint.setTextAlign(Paint.Align.CENTER);
        float newY = y - rulerValuePadding;
        drawText(canvas, textPointPaint, text, x, newY);
    }

    private int getTableStart() {
        return isShowTable ? stepStart + tablePadding : stepStart;
    }

    private int getTableEnd() {
        return isShowTable ? stepEnd + tablePadding : stepEnd;
    }


    private void drawCircle(Canvas canvas) {

        canvas.translate(0f, -dip2px(10));
        int tableEnd = getTableEnd();

        int rulerCount = maxValue / rulerValue;

        int rulerMaxCount = maxValue % rulerValue > 0 ? rulerCount + 1 : rulerCount;

        int rulerMax = rulerValue * rulerMaxCount + rulerValueDefault;

        tablePath.moveTo(stepStart, 0);


        tablePath.lineTo(stepStart, 0);
        tablePath.lineTo(tableEnd, 0);
        int startValue = minValue - (minValue > 0 ? 0 : minValue % rulerValue);
        int endValue = (maxValue + rulerValue);

        do {
            int startHeight = -getValueHeight(startValue);
            tablePath.moveTo(stepStart, startHeight);
            tablePath.lineTo(tableEnd, startHeight);
            drawRulerYText(canvas, String.valueOf(startValue), stepStart, startHeight);
            startValue += rulerValue;
        } while (startValue < endValue);

        canvas.drawPath(tablePath, circlePaint);
        drawRulerXValue(canvas);
    }

    private void drawRulerXValue(Canvas canvas) {
        if (linePoints == null) return;
        for (int i = 0; i < 7; i++) {
            Point point = linePoints[i];
            if (point == null) break;
            drawRulerXText(canvas, xline[i], linePoints[i].x - dip2px(10), 0);
            Log.i("qwe", "zhanguoqiang: " + linePoints[i]);
        }
        if (linePoints != null && linePoints[0] != null) {
            startX = linePoints[0].x- dip2px(10);
            endX = linePoints[6].x- dip2px(10);
        } else {
            Log.e("asd", "drawRulerXValue: linePoints空指针");
        }
//此处有崩溃
    }

    private void drawTable(Canvas canvas) {
        if (isPlayAnim) {
            Path path = new Path();
            PathMeasure measure = new PathMeasure(linePath, false);
            measure.getSegment(0, currentValue * measure.getLength(), path, true);
            canvas.drawPath(path, xianPaint);
        } else {
            canvas.drawPath(linePath, xianPaint);
        }

        mFillPath.reset();
        mFillPath.addPath(linePath);
        mFillPath.lineTo(endX, getViewDrawHeight() - dip2px(80));
        mFillPath.lineTo(startX, getViewDrawHeight() - dip2px(80));



        mFillPath.close();
        int save = canvas.save();
        canvas.clipPath(mFillPath);
        Drawable drawable = getAllDrawable();
        int left = startX, top = -1000, right = mWidth, bottom = endX;
        drawable.setBounds(left, top, right, bottom);
        drawable.draw(canvas);
        canvas.restoreToCount(save);
    }


    private Drawable getAllDrawable() {
        return getResources().getDrawable(R.drawable.path_fill_blue);
    }

    private void drawLinePoints(Canvas canvas) {
        if (linePoints == null) return;
        int pointCount = 7;
        float pointWidth = dip2px(pointWidthDP) / 2;
        if (isPlayAnim) {
            pointCount = Math.round(currentValue * 7);
        }
        for (int i = 0; i < pointCount; i++) {
            Point point = linePoints[i];
            startX = linePoints[0].x-dip2px(10);
            endX = linePoints[6].x-dip2px(10);
            if (point == null) break;
            if (i == 6)
                canvas.drawCircle(point.x - 20, point.y, pointWidth, pointPaint);
            if (index == 0) {
                canvas.drawLine(linePoints[0].x -  dip2px(10), 0, linePoints[0].x -  dip2px(10), point.y, datelinePaint);//       180, -26
            } else if (index == 1) {
                canvas.drawLine(linePoints[1].x -  dip2px(10), 0, linePoints[1].x -  dip2px(10), point.y, datelinePaint);//      309, -213
            } else if (index == 2) {
                canvas.drawLine(linePoints[2].x -  dip2px(10), 0, linePoints[2].x -  dip2px(10), point.y, datelinePaint);//      438, -138
            } else if (index == 3) {
                canvas.drawLine(linePoints[3].x -  dip2px(10), 0, linePoints[3].x -  dip2px(10), point.y, datelinePaint);//      567, -176
            } else if (index == 4) {
                canvas.drawLine(linePoints[4].x -  dip2px(10), 0, linePoints[4].x -  dip2px(10), point.y, datelinePaint);//      696, 0
            } else if (index == 5) {
                canvas.drawLine(linePoints[5].x -  dip2px(10), 0, linePoints[5].x -  dip2px(10), point.y, datelinePaint);//      825, -176
            } else if (index == 6) {
                canvas.drawLine(linePoints[6].x -  dip2px(10), 0, linePoints[6].x -  dip2px(10), point.y, datelinePaint);//      954, -168
            }
//            drawLinePointText(canvas, String.valueOf(dataList.get(i).getValue()), point.x - 30, point.y);
        }
    }

    private int getValueHeight(int value) {
        float valuePercent = Math.abs(value - minValue) * 100f / (Math.abs(maxValue - minValue) * 100f);
        Log.i("asd", "getValueHeight: " + (int) (getViewDrawHeight() * valuePercent + bottomSpace + 0.5f));
        return (int) (getViewDrawHeight() * valuePercent + 0.5f);
    }

    private float getViewDrawHeight() {
        Log.i("", "getViewDrawHeight: " + getMeasuredHeight() * heightPercent);
        return getMeasuredHeight() * heightPercent;
    }

    private void setupLine() {
        if (dataList.isEmpty()) return;

        int stepTemp = getTableStart();
        Point point = new Point();
        point.set(stepTemp, -getValueHeight(dataList.get(0).getValue()));//
        linePoints[0] = point;
        linePath.moveTo(point.x - dip2px(10), point.y);


        for (int i = 1; i < dataList.size(); i++) {
            DataBean data = dataList.get(i);
            Point next = new Point();
            next.set(stepTemp += stepSpace, -getValueHeight(data.getValue()));

            if (isBezierLine) {
                int cW = point.x + stepSpace / 2;

                Point p1 = new Point();
                p1.set(cW, point.y);

                Point p2 = new Point();
                p2.set(cW, next.y);

                linePath.cubicTo(p1.x - dip2px(10), p1.y, p2.x - dip2px(10), p2.y, next.x - dip2px(10), next.y);
            } else {
                linePath.lineTo(next.x - dip2px(10), next.y);
            }

            point = next;
            linePoints[i] = next;
        }

        isInitialized = true;
    }

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

    private int sp2px(float spValue) {
        final float fontScale = getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }

    private void refreshLayout() {
        resetParam();
        requestLayout();
        postInvalidate();
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                whichCircle(x, y);
                break;
        }
        return true;

    }

    private void whichCircle(float x, float y) {
        Log.i("qwe", "whichCircle: " + x);
        if (dip2px(17) < x && x < dip2px(77)) {
            index = 0;
            Log.i("zxc", "whichCircle: " + String.valueOf(dataList.get(0).getValue()));
            EventBus.getDefault().post(new SpetEvent(202, String.valueOf(dataList.get(0).getValue())));

        } else if ( dip2px(77) < x && x <  dip2px(125)) {
            index = 1;
            Log.i("zxc", "whichCircle: " + String.valueOf(dataList.get(1).getValue()));
            EventBus.getDefault().post(new SpetEvent(202, String.valueOf(dataList.get(1).getValue())));
        } else if ( dip2px(77) < x && x <  dip2px(173)) {
            index = 2;
            Log.i("zxc", "whichCircle: " + String.valueOf(dataList.get(2).getValue()));
            EventBus.getDefault().post(new SpetEvent(202, String.valueOf(dataList.get(2).getValue())));
        } else if (dip2px(173) < x && x < dip2px(221)) {
            index = 3;
            Log.i("zxc", "whichCircle: " + String.valueOf(dataList.get(3).getValue()));
            EventBus.getDefault().post(new SpetEvent(202, String.valueOf(dataList.get(3).getValue())));
        } else if (dip2px(221) < x && x <dip2px(269) ) {
            index = 4;
            Log.i("zxc", "whichCircle: " + String.valueOf(dataList.get(4).getValue()));
            EventBus.getDefault().post(new SpetEvent(202, String.valueOf(dataList.get(4).getValue())));
        } else if (dip2px(269) < x && x < dip2px(283)) {
            index = 5;
            Log.i("zxc", "whichCircle: " + String.valueOf(dataList.get(5).getValue()));
            EventBus.getDefault().post(new SpetEvent(202, String.valueOf(dataList.get(5).getValue())));
        } else if (x > dip2px(283)) {
            index = 6;
            Log.i("zxc", "whichCircle: " + String.valueOf(dataList.get(index).getValue()));
            EventBus.getDefault().post(new SpetEvent(202, String.valueOf(dataList.get(6).getValue())));
        }
        refreshLayout();
     /*   if (x == 180) {

        } else if (x == 324) {

        } else if (x == 468) {

        } else if (x == 612) {

        } else if (x == 756) {

        } else if (x == 900) {

        } else if (x == 1044) {

        }*/

    }


    /**
     * 设置数据
     *
     * @param dataList
     */
    public void setData(List<DataBean> dataList) {
        if (dataList == null) {
            throw new RuntimeException("dataList cannot is null!");
        }
        if (dataList.isEmpty()) return;
        this.dataList.clear();
        this.dataList.addAll(dataList);

        maxValue = Collections.max(this.dataList, new Comparator<DataBean>() {
            @Override
            public int compare(DataBean o1, DataBean o2) {
                return o1.getValue() - o2.getValue();
            }
        }).getValue();

        minValue = Collections.min(this.dataList, new Comparator<DataBean>() {
            @Override
            public int compare(DataBean o1, DataBean o2) {
                return o1.getValue() - o2.getValue();
            }
        }).getValue();

        refreshLayout();
    }


    public void setDate(String[] dateList) {

        if (dateList == null) {
            throw new RuntimeException("dateList cannot is null!");
        }
        if (dateList.length == 0) return;
        this.xline = null;
        this.xline = dateList;
        refreshLayout();
    }

    public void playAnim() {
        this.isPlayAnim = true;
        if (isAnimating) return;
        if (valueAnimator != null) {
            valueAnimator.start();
        }
    }

    public static class DataBean {

        int key;

        public DataBean(int key) {
            this.key = key;
        }

        public int getValue() {
            return key;
        }
    }

    public void setRulerYSpace(int space) {
        if (space <= 0) {
            space = rulerValueDefault;
        }
        this.rulerValue = space;
        refreshLayout();
    }
}

资源文件

drawable文件夹下

path_fill_blue.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <gradient
        android:startColor="@color/blue_transparent_start"
        android:endColor="@color/blue_transparent_end"
        android:angle="90"
        />
</shape>

path_fill_green.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <gradient
        android:startColor="@color/green_transparent_start"
        android:endColor="@color/green_transparent_end"
        android:angle="0"
        />
</shape>

path_fill_pink.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <gradient
        android:angle="0"
        android:endColor="@color/pink_transparent_end"
        android:startColor="@color/pink_transparent_start" />
</shape>

colors.xml文件下


<color name="blue_transparent_start">#0000</color>
<color name="blue_transparent_end">#505FB4F8</color>
<color name="green_transparent_start">#004fd4d0</color>
<color name="green_transparent_end">#804fd4d0</color>
<color name="pink_transparent_start">#00FFB6C1</color>
<color name="pink_transparent_end">#80FFB6C1</color>

结束。

调用

(写在Activity或者Fragment方法中的)我是写在 onCreate()方法中了

↓↓↓

ArrayList<Integer> listValueNum = new ArrayList<>();

listValueNum.add(1000);
listValueNum.add(6000);
listValueNum.add(4000);
listValueNum.add(5000);
listValueNum.add(300);
listValueNum.add(5000);
listValueNum.add(4800);
List<UshareChatView.Data> datas = new ArrayList<>();
for (int value : listValueNum) {
    UshareChatView.Data data = new UshareChatView.Data(value);
    datas.add(data);
ushareChatView.setData(datas);
if (Collections.max(listValueNum) < 1200) {
    ushareChatView.setRulerYSpace(30);
} else if (Collections.max(listValueNum) > 1200 && Collections.max(listValueNum) < 5000) {
    ushareChatView.setRulerYSpace(400);
} else {
    ushareChatView.setRulerYSpace(5000);
}
if (ushareChatView != null) {
    ushareChatView.playAnim();//曲线动画
}
}
//获取当前日期往前推一个星期   
 public String getOtherDate(int value) {
        DateFormat dateFormat = new SimpleDateFormat("dd");
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.HOUR_OF_DAY, value);
        String otherDate = dateFormat.format(calendar.getTime());
//        Log.i("asd", "getOtherDate: " + otherDate);
        return otherDate;
    }

资源文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:orientation="vertical">

    <UshareChatView
        android:id="@+id/ushareChatView"
        android:layout_width="328dp"
        android:layout_height="115dp" />
</LinearLayout>

 

//手敲操作 欢迎指正 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值