MPAndroidChart--LineChart使用

记录工作中使用到的MPAndroidChart的一些点

github地址:https://github.com/PhilJay/MPAndroidChart
wiki:https://github.com/PhilJay/MPAndroidChart/wiki

效果图:
折线图(gradient fill)
折线图

需求如下:

  1. 选中显示自定义marker
  2. 纵坐标从0开始
  3. 横坐标自定义标签
  4. 当数据很多时,选取控制圆圈显示的个数
 compile 'com.github.PhilJay:MPAndroidChart:v3.0.3'
<com.github.mikephil.charting.charts.LineChart
              android:id="@+id/line_chart"
               android:layout_width="match_parent"
               android:layout_height="@dimen/dp_300"/>

常用属性:

LineChart:

  • setDescription(String desc):设置图表说明的文字(默认右下角)。
  • setDescriptionPosition(float x, float y):设置图表说明的位置,以像素为单位
  • setDescriptionColor(int color):设置图表说明文字的颜色。
  • setDescriptionTypeface(Typeface t):设置Typeface用于绘制图表说明文字。
  • setDescriptionTextSize(float size):以像素为单位设置描述文本的大小,最小6f,最大16f。
  • setNoDataText(String text):设置图表为空时应显示的文字
  • setTouchEnabled(boolean enabled):允许启用/禁用所有可能的与图表的触摸交互。
  • setDragEnabled(boolean enabled):启用/禁用拖动(平移)图表。
  • setScaleEnabled(boolean enabled):启用/禁用两个轴上图表的缩放比例。
  • setDoubleTapToZoomEnabled(boolean enabled):将其设置为false以禁止通过双击缩放图表来缩放图表。
  • setMarker(IMarker marker) :设置标记(点击对应点显示对应描述,如图1黑色框)。
  • setRenderer(DataRenderer renderer):设置渲染器,可对绘制过程进行操作。
  • setData(LineData data):设置数据集合。
  • animateX(3000); //动画水平3000毫秒。
  • animateY(3000); //动画垂直3000毫秒 。
  • animateXY(3000,3000); //动画水平和垂直3000毫秒。
  • invalidate():刷新数据(使用了动画则无需使用,重复)

XAxis:

  • setAvoidFirstLastClipping(boolean enabled):是否避免图表或屏幕的边缘的第一个和最后一个轴中的标签条目被裁剪。
  • setLabelRotationAngle(float angle):设置绘制x轴标签的角度(以度为单位)。
  • setPosition(XAxisPosition pos):设置XAxis应该出现的位置。在TOP,BOTTOM,BOTH_SIDED,TOP_INSIDE或BOTTOM_INSIDE之间选择。
  • setDrawGridLines(boolean enabled):隐藏点对应与X轴相垂直的线(即图表内纵向的线)。
  • setTextColor(int color):设置横坐标标签文字颜色
  • setLabelCount(int count):设置标签个数(可能不准,需设置强制)。
  • setLabelCount(int count, boolean force):设置标签个数和是否强制。
  • setValueFormatter(IAxisValueFormatter f):格式横坐标标签数据,返回自定义文本。

YAxis:

Y轴相较于X轴分为AxisRight和AxisLeft,但属性都是一样的。

  • setEnabled(boolean enabled):设置是否显示,图1图2都是隐藏右侧Y轴。
  • setAxisMinimum(float min):设置纵坐标最小值,为0即从0开始。

LineDataSet:

  • setColor(int color):设置绘制线条颜色。
  • setLineWidth(float width):设置线条宽度。
  • setDrawValues(boolean enabled) : 设置是否显示点上的数值。
  • setHighLightColor(int color) :设置高亮颜色(如图1黑色相垂直的线条)。
  • setDrawFilled(boolean enabled):是否填充线条与X之间区域。
  • setFillDrawable(Drawable):设置填充图片(可自定义渐变,如图1显示)。
  • setDrawCircles(boolean enabled):是否显示点相应的圆(圆为外圈圆+内圈圆)。
  • setCircleRadius(float radius):设置外圈圆半径。
  • setCircleColor(int color):设置外圈圆颜色。
  • setCircleHoleRadius(float holeRadius):设置内圈圆半径。
  • setCircleColorHole(int color):设置内圈圆颜色。

进入正题:

1. 选中显示自定义marker

ChemicalsTrendMarker marker = new ChemicalsTrendMarker(MyApplication.getApplication(),  getString(R.string.chemicals_trend_interval_marker_title));
        marker.setChartView(lineChart);
        lineChart.setMarker(marker);
public class ChemicalsTrendMarker extends MarkerView {

    private String title;
    /**
     * Constructor. Sets up the MarkerView with a custom layout resource.
     *
     * @param context
     */
    public ChemicalsTrendMarker(Context context, String title) {
        super(context, R.layout.marker_fuel_trend);
        this.title = title;
    }

    @Override
    public void refreshContent(Entry e, Highlight highlight) {

        if (e.getData() instanceof ChemicalsTrendModel) {

            ChemicalsTrendModel model = (ChemicalsTrendModel) e.getData();
            ((TextView) findViewById(R.id.tvTitle)).setText(title);
            ((TextView) findViewById(R.id.tvContext)).setText(model.releaseDate + " : " + model.price);
        }

        super.refreshContent(e, highlight);
    }

    @Override
    public MPPointF getOffset() {
        return new MPPointF(-(getWidth() / 2), getHeight()/2);
    }

}

2、纵坐标从0开始

    YAxis leftAxis = lineChart.getAxisLeft();
    leftAxis.setAxisMinimum(0);//纵坐标从0开始

3、横坐标自定义标签

        int size = xVals.size();

        XAxis xAxis = lineChart.getXAxis();
        //控制横坐标标签个数
        if(size <= 4 ){
            xAxis.setLabelCount(size, false);
        }else {
            xAxis.setLabelCount(4, true);//强制
        }
        xAxis.setValueFormatter(new IAxisValueFormatter() {
            @Override
            public String getFormattedValue(float value, AxisBase axis) {
                int index = (int) value;
                if(index >= 0 && index < size ){
                    return xVals.get(index);
                }else {
                    return "";
                }
            }
        });

注意:当size>4时,设置强制,size<=4时设置为不强制,这样可以避免当数据小于4时,标签数据显示出现异常,如下比较两图横坐标标签:
这里写图片描述这里写图片描述

4、当数据很多时,选取控制圆圈显示的个数
先上一张未处理前的图表:
这里写图片描述

在这里仅仅对XAxis进行格式化是不够的。MPAndroidChart将X轴Y轴及数据绘制线条分开进行设置,但是我们在对LineDataSet的CircleRadius等属性设置时,并找不到可以对圆圈集合进行控制的方法,那么我们可以通过查看源码的方式,查看LineChart是如何绘制圆的。


package com.github.mikephil.charting.charts;

import android.content.Context;
import android.util.AttributeSet;

import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.interfaces.dataprovider.LineDataProvider;
import com.github.mikephil.charting.renderer.LineChartRenderer;

/**
 * Chart that draws lines, surfaces, circles, ...
 *
 * @author Philipp Jahoda
 */
public class LineChart extends BarLineChartBase<LineData> implements LineDataProvider {

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

    public LineChart(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public LineChart(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    protected void init() {
        super.init();

        mRenderer = new LineChartRenderer(this, mAnimator, mViewPortHandler);
    }

    @Override
    public LineData getLineData() {
        return mData;
    }

    @Override
    protected void onDetachedFromWindow() {
        // releases the bitmap in the renderer to avoid oom error
        if (mRenderer != null && mRenderer instanceof LineChartRenderer) {
            ((LineChartRenderer) mRenderer).releaseBitmap();
        }
        super.onDetachedFromWindow();
    }
}

可以看到LineChart内部主要是初始化了一个LineChartRenderer,那么LineChart主要的方法就应该在这里面了。果不其然,很容易就能找到LineChart绘制Circle的方法。

protected void drawCircles(Canvas c) {

        mRenderPaint.setStyle(Paint.Style.FILL);

        float phaseY = mAnimator.getPhaseY();

        mCirclesBuffer[0] = 0;
        mCirclesBuffer[1] = 0;

        List<ILineDataSet> dataSets = mChart.getLineData().getDataSets();

        for (int i = 0; i < dataSets.size(); i++) {

            ILineDataSet dataSet = dataSets.get(i);

            if (!dataSet.isVisible() || !dataSet.isDrawCirclesEnabled() ||
                    dataSet.getEntryCount() == 0)
                continue;

            mCirclePaintInner.setColor(dataSet.getCircleHoleColor());

            Transformer trans = mChart.getTransformer(dataSet.getAxisDependency());

            mXBounds.set(mChart, dataSet);

            float circleRadius = dataSet.getCircleRadius();
            float circleHoleRadius = dataSet.getCircleHoleRadius();
            boolean drawCircleHole = dataSet.isDrawCircleHoleEnabled() &&
                    circleHoleRadius < circleRadius &&
                    circleHoleRadius > 0.f;
            boolean drawTransparentCircleHole = drawCircleHole &&
                    dataSet.getCircleHoleColor() == ColorTemplate.COLOR_NONE;

            DataSetImageCache imageCache;

            if (mImageCaches.containsKey(dataSet)) {
                imageCache = mImageCaches.get(dataSet);
            } else {
                imageCache = new DataSetImageCache();
                mImageCaches.put(dataSet, imageCache);
            }

            boolean changeRequired = imageCache.init(dataSet);

            // only fill the cache with new bitmaps if a change is required
            if (changeRequired) {
                imageCache.fill(dataSet, drawCircleHole, drawTransparentCircleHole);
            }

            int boundsRangeCount = mXBounds.range + mXBounds.min;

            for (int j = mXBounds.min; j <= boundsRangeCount; j++) {

                Entry e = dataSet.getEntryForIndex(j);

                if (e == null) break;

                mCirclesBuffer[0] = e.getX();
                mCirclesBuffer[1] = e.getY() * phaseY;

                trans.pointValuesToPixel(mCirclesBuffer);

                if (!mViewPortHandler.isInBoundsRight(mCirclesBuffer[0]))
                    break;

                if (!mViewPortHandler.isInBoundsLeft(mCirclesBuffer[0]) ||
                        !mViewPortHandler.isInBoundsY(mCirclesBuffer[1]))
                    continue;

                Bitmap circleBitmap = imageCache.getBitmap(j);

                if (circleBitmap != null) {
                    c.drawBitmap(circleBitmap, mCirclesBuffer[0] - circleRadius, mCirclesBuffer[1] - circleRadius, null);
                }
            }
        }
    }
List<ILineDataSet> dataSets = mChart.getLineData().getDataSets(); 获取到LineChart中绘制的线条集合。
ILineDataSet dataSet = dataSets.get(i); 获取某一条线条的点集合。
Entry e = dataSet.getEntryForIndex(j); 获取到具体一个点的数据。

到此,我们已经获取到LineChart如何获取到线条集合进行绘制圆圈的了,这样我们就可以在绘制圆之前进行判断,当前点是否为我们需要绘制的点,如果不是,则不进行绘制。当然这只是提供一种方法思路。
因此,我们可以自定义一个Renderer,继承自LineChartRenderer并对drawCircles方法进行重写。
下面的例子是结合需求,只显示4个点(首尾两点+中间部分去平均值得到2个点),供参考

public class BaseLineChartRenderer extends LineChartRenderer{
    private int mMaxCount = 4;//最多绘制点个数

    private float[] mCirclesBuffer = new float[2];

    private HashMap<IDataSet, DataSetImageCache> mImageCaches = new HashMap<>();

    public BaseLineChartRenderer(int maxCount ,LineDataProvider chart, ChartAnimator animator, ViewPortHandler viewPortHandler) {
        super(chart, animator, viewPortHandler);
        mMaxCount = maxCount;
    }

    @Override
    protected void drawCircles(Canvas c) {

        mRenderPaint.setStyle(Paint.Style.FILL);

        float phaseY = mAnimator.getPhaseY();

        mCirclesBuffer[0] = 0;
        mCirclesBuffer[1] = 0;

        List<ILineDataSet> dataSets = mChart.getLineData().getDataSets();

        for (int i = 0; i < dataSets.size(); i++) {

            ILineDataSet dataSet = dataSets.get(i);

----------

            //要画出点的数组
            int size = dataSet.getEntryCount();;
            int avg = size / (mMaxCount - 1);//平均数
            int[] indexArr = new int[mMaxCount];
            //首尾两点
            indexArr[0] = 0;
            indexArr[mMaxCount - 1] = size - 1;
            //中间平分点
            for(int j = 1; j < mMaxCount - 1; j++){
                indexArr[j] = avg + indexArr[j - 1];
            }
            int index = 0;

----------

            if (!dataSet.isVisible() || !dataSet.isDrawCirclesEnabled() ||
                    dataSet.getEntryCount() == 0) {
                continue;
            }

            mCirclePaintInner.setColor(dataSet.getCircleHoleColor());

            Transformer trans = mChart.getTransformer(dataSet.getAxisDependency());

            mXBounds.set(mChart, dataSet);

            float circleRadius = dataSet.getCircleRadius();
            float circleHoleRadius = dataSet.getCircleHoleRadius();
            boolean drawCircleHole = dataSet.isDrawCircleHoleEnabled() &&
                    circleHoleRadius < circleRadius &&
                    circleHoleRadius > 0.f;
            boolean drawTransparentCircleHole = drawCircleHole &&
                    dataSet.getCircleHoleColor() == ColorTemplate.COLOR_NONE;

            DataSetImageCache imageCache;

            if (mImageCaches.containsKey(dataSet)) {
                imageCache = mImageCaches.get(dataSet);
            } else {
                imageCache = new DataSetImageCache();
                mImageCaches.put(dataSet, imageCache);
            }

            boolean changeRequired = imageCache.init(dataSet);

            // only fill the cache with new bitmaps if a change is required
            if (changeRequired) {
                imageCache.fill(dataSet, drawCircleHole, drawTransparentCircleHole);
            }

            int boundsRangeCount = mXBounds.range + mXBounds.min;

            for (int j = mXBounds.min; j <= boundsRangeCount; j++) {


----------

                //在绘制前拦截判断当前点是否为要画的点
                if(index < mMaxCount){
                    if( j != indexArr[index]){
                        continue;//不是要绘制的点
                    }else {
                        index ++;//需要绘制的点
                    }
                }else {
                    break;//点全都绘制完成
                }

----------

                Entry e = dataSet.getEntryForIndex(j);

                if (e == null) {
                    break;
                }

                mCirclesBuffer[0] = e.getX();
                mCirclesBuffer[1] = e.getY() * phaseY;

                trans.pointValuesToPixel(mCirclesBuffer);

                if (!mViewPortHandler.isInBoundsRight(mCirclesBuffer[0])) {
                    break;
                }

                if (!mViewPortHandler.isInBoundsLeft(mCirclesBuffer[0]) ||
                        !mViewPortHandler.isInBoundsY(mCirclesBuffer[1])) {
                    continue;
                }

                Bitmap circleBitmap = imageCache.getBitmap(j);

                if (circleBitmap != null) {
                    c.drawBitmap(circleBitmap, mCirclesBuffer[0] - circleRadius, mCirclesBuffer[1] - circleRadius, null);
                }
            }
        }
    }


    private class DataSetImageCache {...}

最后将自定义的Renderer设置给LineChart。

//控制数据绘制圆圈个数
lineChart.setRenderer(new BaseLineChartRenderer(4, lineChart, lineChart.getAnimator(), lineChart.getViewPortHandler()));

最后,将图1图2设置的属性贴在下面。

    @Override
    public void initChart() {
        //图表描述
        Description description = new Description();
        description.setText("");
        lineChart.setDescription(description);
        lineChart.setDragEnabled(true);
        lineChart.setScaleEnabled(false);
        lineChart.setDoubleTapToZoomEnabled(false);
        ChemicalsTrendMarker marker = new ChemicalsTrendMarker(MyApplication.getApplication(), getString(R.string.chemicals_trend_interval_marker_title));
        marker.setChartView(lineChart);
        lineChart.setMarker(marker);
        //控制数据绘制圆圈个数
        lineChart.setRenderer(new BaseLineChartRenderer(4, lineChart, lineChart.getAnimator(), lineChart.getViewPortHandler()));

        XAxis xAxis = lineChart.getXAxis();
        xAxis.setAvoidFirstLastClipping(true);//坐标最后一个点
        xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
        xAxis.setLabelRotationAngle(0f);
        //隐藏x对应垂直线
        xAxis.setDrawGridLines(false);
        //xAxis.setAxisMaximum(4);
        xAxis.setTextColor(ContextCompat.getColor(MyApplication.getApplication(), R.color.gray_666));

        YAxis rightAxis = lineChart.getAxisRight();
        rightAxis.setEnabled(false);
        YAxis leftAxis = lineChart.getAxisLeft();
        leftAxis.setTextColor(ContextCompat.getColor(MyApplication.getApplication(), R.color.gray_666));
        leftAxis.setAxisMinimum(0);//纵坐标从0开始

        Legend l = lineChart.getLegend();//图例
        l.setEnabled(false);
    }


    @Override
    public void refreshLineChart(ArrayList<Entry> yVals, ArrayList<String> xVals) {
        int size = xVals.size();

        XAxis xAxis = lineChart.getXAxis();
        //控制横坐标标签个数
        if(size <= 4 ){
            xAxis.setLabelCount(size, false);
        }else {
            xAxis.setLabelCount(4, true);//强制
        }
        xAxis.setValueFormatter(new IAxisValueFormatter() {
            @Override
            public String getFormattedValue(float value, AxisBase axis) {
                int index = (int) value;
                if(index >= 0 && index < size ){
                    return xVals.get(index);
                }else {
                    return "";
                }
            }
        });

        LineDataSet dataSet = new LineDataSet(yVals, "化工品价格走势图");
        dataSet.setColors(ContextCompat.getColor(MyApplication.getApplication(), R.color.colorPrimary));
        dataSet.setLineWidth(2);
        dataSet.setDrawValues(false);
        dataSet.setHighLightColor(ContextCompat.getColor(MyApplication.getApplication(), R.color.gray_333));
        dataSet.setDrawFilled(true);
        dataSet.setFillDrawable(ContextCompat.getDrawable(MyApplication.getApplication(), R.drawable.shape_gradual_blue));
        //点上的数据
        dataSet.setDrawValues(false);
        dataSet.setDrawCircles(true);
        dataSet.setCircleRadius(DensityUtil.dip2px(MyApplication.getApplication(), 2));
        dataSet.setCircleColorHole(ContextCompat.getColor(MyApplication.getApplication(), R.color.white));
        dataSet.setCircleHoleRadius(DensityUtil.dip2px(MyApplication.getApplication(), 1.5f));
        dataSet.setCircleColor(ContextCompat.getColor(MyApplication.getApplication(), R.color.colorPrimary));
        lineChart.setData(new LineData(dataSet));
        lineChart.animateX(1000);

    }
@Override
    public void initChart() {

        //图表描述
        Description description = new Description();
        description.setText("");
        mLineChart.setDescription(description);
        mLineChart.setDragEnabled(true);
        mLineChart.setScaleEnabled(false);
        mLineChart.setDoubleTapToZoomEnabled(false);

        XAxis xAxis = mLineChart.getXAxis();
        xAxis.setAvoidFirstLastClipping(false);//坐标最后一个点
        xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
        xAxis.setLabelCount(9);
        xAxis.setLabelRotationAngle(-30f);

        YAxis rightAxis=mLineChart.getAxisRight();
        rightAxis.setEnabled(false);

        Legend l = mLineChart.getLegend();//图例
        l.setEnabled(false);
        l.setPosition(Legend.LegendPosition.RIGHT_OF_CHART_INSIDE);//设置图例的位置
        l.setTextSize(10f);//设置文字大小
        l.setForm(Legend.LegendForm.CIRCLE);//正方形,圆形或线
        l.setFormSize(10f); // 设置Form的大小
        l.setWordWrapEnabled(true);//是否支持自动换行 目前只支持BelowChartLeft, BelowChartRight, BelowChartCenter
        l.setFormLineWidth(10f);//设置Form的宽度
    }

    @Override
    public void refreshLineChart(ArrayList<Entry> yVals, ArrayList<String> xVals) {
        mTvNoData.setVisibility(View.GONE);
        mLlTable.setVisibility(View.VISIBLE);
        mTvLineChartTitle.setText((!TextUtils.isEmpty(mTidePortName) ? mTidePortName + " " : "") + getString(R.string.tide_search_chart_title));
        int size = xVals.size();//防止数组越界
        mLineChart.getXAxis().setValueFormatter(new IAxisValueFormatter() {
            @Override
            public String getFormattedValue(float value, AxisBase axis) {
                return xVals.get((int) value < size ? (int) value : size - 1);
            }
        });
        TideMarker marker = new TideMarker(MyApplication.getApplication(), mTidePortName);
        marker.setChartView(mLineChart);
        mLineChart.setMarker(marker);

        LineDataSet dataSet = new LineDataSet(yVals, "潮汐表曲线图");
        dataSet.setColors(ContextCompat.getColor(MyApplication.getApplication(), R.color.colorPrimary));
        dataSet.setCircleColor(ContextCompat.getColor(MyApplication.getApplication(), R.color.colorPrimary));
        dataSet.setLineWidth(3);
        dataSet.setDrawValues(false);
        dataSet.setHighLightColor(ContextCompat.getColor(MyApplication.getApplication(), R.color.colorPrimary));
        mLineChart.setData(new LineData(dataSet));
        mLineChart.animateX(1000);
    }
要实现这个功能,可以使用RecyclerView作为列表容器,使用LinearLayoutManager设置为横向滚动,然后将每个item的布局设置为一个LineChart。当用户滑动某个LineChart时,我们可以通过RecyclerView中的getChildAt()方法获取到当前显示的所有item,然后遍历每个item中的LineChart,设置其偏移量,以实现同步移动的效果。 为了优化性能,可以使用节流或防抖等技术,限制移动频率,避免因为过于频繁的移动而导致卡顿问题。另外,可以在RecyclerView的Adapter中使用ViewHolder模式,避免重复创建和销毁LineChart对象,提高性能。 具体实现步骤如下: 1. 使用RecyclerView作为列表容器,设置LayoutManager为横向滚动。 ``` RecyclerView recyclerView = findViewById(R.id.recycler_view); LinearLayoutManager layoutManager = new LinearLayoutManager(this); layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); recyclerView.setLayoutManager(layoutManager); ``` 2. 在RecyclerView的Adapter中,重写onBindViewHolder()方法,遍历每个item中的LineChart,设置其偏移量。 ``` @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { // 设置LineChart数据 LineChart lineChart = holder.lineChart; // ... // 设置LineChart偏移量 int scrollX = recyclerView.computeHorizontalScrollOffset(); int chartOffset = position * chartWidth - scrollX; lineChart.offsetLeftAndRight(chartOffset - lineChart.getLeft()); } ``` 3. 在RecyclerView的OnScrollListener中,监听滚动事件,限制移动频率,避免卡顿问题。 ``` recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { private long lastScrollTime = 0; @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { long currentTime = System.currentTimeMillis(); if (currentTime - lastScrollTime < 100) { // 限制移动频率 return; } lastScrollTime = currentTime; // 遍历所有item中的LineChart,设置偏移量 for (int i = 0; i < recyclerView.getChildCount(); i++) { View childView = recyclerView.getChildAt(i); ViewHolder viewHolder = (ViewHolder) recyclerView.getChildViewHolder(childView); LineChart lineChart = viewHolder.lineChart; int chartOffset = viewHolder.getAdapterPosition() * chartWidth - recyclerView.computeHorizontalScrollOffset(); lineChart.offsetLeftAndRight(chartOffset - lineChart.getLeft()); } } }); ```
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值