图表加载MPAndroidChart

MPAndroidChart图表加载框架,适合各种直观的展示。

参考:https://blog.csdn.net/dapangzao/article/details/74949541

参考:https://www.jianshu.com/p/c6e8ea5e9ba0

如果想要了解其原理,请参考上面两个博客,本文将展示几个使用例子。

导入依赖

在根目录下的build文件中添加

allprojects {
repositories {
maven { url “https://jitpack.io” }
}
}

然后,在app下的build中添加依赖

compile 'com.github.PhilJay:MPAndroidChart:v3.0.2'


然后,就可以使用了,先从饼状图开始,图片可以旋转。


下面是代码:

布局:

<com.github.mikephil.charting.charts.PieChart
    android:id="@+id/piechart"
    android:layout_width="200dp"
    android:layout_height="100dp"
    android:layout_gravity="center_horizontal"

    ></com.github.mikephil.charting.charts.PieChart>

因为考虑到这种图都是由后台给数据,然后展示出来,而且还有个对比,所以可以使用bean类进行封装,这是测试用的bean类,只有四个float类型

PieChart   piechart = findViewById(R.id.piechart);

//创建集合,测试用的bean类,里面只有四个float类型
 List<Bean.DistributionBean> energyDistribution = new ArrayList<>();
//增加假数据
Bean.DistributionBean distributionBean=new Bean.DistributionBean();
distributionBean.setChild((float) 2466.2);
distributionBean.setChildPum((float) 330.25);
distributionBean.setCoolingPum((float)291.0);
distributionBean.setCoolingTow((float) 74.6);
energyDistribution.add(distributionBean);
Bean.DistributionBean distributionBean2=new Bean.DistributionBean();
distributionBean2.setChild((float) 27095.0);
distributionBean2.setChildPum((float) 3602.1);
distributionBean2.setCoolingPum((float) 3378.0);
distributionBean2.setCoolingTow((float) 801.2);
energyDistribution.add(distributionBean2);
Bean.DistributionBean distributionBean3=new Bean.DistributionBean();
distributionBean3.setChild((float) 233049.8);
distributionBean3.setChildPum((float)103586.84);
distributionBean3.setCoolingPum((float) 38910.6);
distributionBean3.setCoolingTow((float)10368.0);
energyDistribution.add(distributionBean3);
//获取环形图数据 解释在下面的方法中
 PieData loopPieData = getLoopPieData(this, energyDistribution.size(), 100, energyDistribution, 0);
 showLoopChart(piechart,loopPieData);


//------------------------------------------------------------------------------
  /**
     * 获取环形图数据
     * @param context  上下文
     * @param count 分成几部分
     * @param range 总数多少
     * @param energyDistribution 数据集合
     * @param i 显示的第几个数据
     */
    public PieData getLoopPieData(Activity context, int count, float range, List<Bean.DistributionBean> energyDistribution, int i) {
        // 饼图数据                           this, energyDistribution.size(), 100, energyDistribution, 1)
        /**
         * 将一个饼形图分成四部分, 四部分的数值比例为14:14:34:38
         * 所以 14代表的百分比就是14%
         */
        float quarterly1 = energyDistribution.get(i).getChild();
        float quarterly2 = energyDistribution.get(i).getCoolingTow();
        float quarterly3 = energyDistribution.get(i).getCoolingPum();
        float quarterly4 = energyDistribution.get(i).getChildPum();

        ArrayList<PieEntry> entries = new ArrayList<PieEntry>();
        entries.add(new PieEntry(quarterly1, ""));
        entries.add(new PieEntry(quarterly2, ""));
        entries.add(new PieEntry(quarterly3, ""));
        entries.add(new PieEntry(quarterly4, ""));

        ArrayList<Integer> colors = new ArrayList<Integer>();
        // 饼图颜色
        colors.add(context.getResources().getColor(R.color.blue));
        colors.add(context.getResources().getColor(R.color.light_pink));
        colors.add(context.getResources().getColor(R.color.light_blue));
        colors.add(context.getResources().getColor(R.color.blue_grey));
        //y轴的集合
        PieDataSet pieDataSet = new PieDataSet(entries, ""/*显示在比例图上*/);
        pieDataSet.setSliceSpace(1f); //设置个饼状图之间的距离
        pieDataSet.setColors(colors);
        pieDataSet.setValueTextSize(10f);//百分比文字大小
        pieDataSet.setValueLinePart1OffsetPercentage(0f);
        pieDataSet.setValueLinePart1Length(0.5f);
//        pieDataSet.setValueLinePart2Length(0.7f);
//        pieDataSet.setYValuePosition(PieDataSet.ValuePosition.INSIDE_SLICE);
        pieDataSet.setYValuePosition(PieDataSet.ValuePosition.OUTSIDE_SLICE);
        pieDataSet.setXValuePosition(PieDataSet.ValuePosition.OUTSIDE_SLICE);
//        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
//        float px = 5 * (metrics.densityDpi / 160f);
        pieDataSet.setSelectionShift(0); // 选中态多出的长度
//        PieData pieData = new PieData(xValues, pieDataSet);
        pieDataSet.setValueFormatter(new PercentFormatter());//添加百分号
        PieData pieData = new PieData(pieDataSet);
        return pieData;
    }
    /**
     * 环形图
     * @param pieChart
     * @param pieData
     */
    public void showLoopChart(PieChart pieChart, PieData pieData) {
        pieChart.setHoleRadius(50f);  //半径
        pieChart.setTransparentCircleRadius(0f); // 半透明圈
//        pieChart.setTransparentCircleColor(Color.rgb(200,200,200));
//        pieChart.setTransparentCircleAlpha(255);
        desc = new Description();
        desc.setText("");
        pieChart.setDescription(desc);
        pieChart.setExtraOffsets(5f,5.f,5.f,5.f);//饼状图内填充
       pieChart.setDrawCenterText(false);  //饼状图中间可以添加文字
        pieChart.setEntryLabelTextSize(10f);//饼状图字体大小
        pieChart.setDrawHoleEnabled(true);//设置内环是否显示
        pieChart.setRotationAngle(270); // 初始旋转角度
        pieChart.setRotationEnabled(true); // 可以手动旋转
        pieChart.setUsePercentValues(true);  //显示成百分比
        pieChart.setCenterText("");  //饼状图中间的文字
        pieChart.setHighlightPerTapEnabled(false);
        pieChart.setDrawEntryLabels(true);
        //设置数据
        pieChart.setData(pieData);
        Legend mLegend = pieChart.getLegend();  //设置比例图
        mLegend.setEnabled(false);
        pieChart.animateXY(1500, 1500);  //设置动画
        pieChart.invalidate();
    }
上面分区的颜色在colors文件中设置就可以了,res/values/colors

这样,一个可以旋转的环形图就做好了,接下来是线形图



为了展示上面点击的圆点,还有数据的展示,需要配备另外的类跟布局

布局:
<com.github.mikephil.charting.charts.LineChart
    android:layout_width="match_parent"
    android:layout_height="300px"
    android:id="@+id/line_chart"
    ></com.github.mikephil.charting.charts.LineChart>
id:
LineChart  linechart = findViewById(R.id.line_chart);
创建假的数据!!!!!注意的是,线形图跟柱形图,x坐标的起始点一定要是0.0,不然柱形图会报错下标溢出
//创建假x、y轴坐标
List<Float> x=new ArrayList<>();
List<Float> y=new ArrayList<>();
x.add((float) 0.0);
x.add((float) 1.0);
x.add((float) 2.0);
x.add((float) 3.0);
x.add((float) 4.0);
x.add((float) 5.0);
y.add((float) 1.3);
y.add((float) 4.2);
y.add((float) 3.1);
y.add((float) 8.3);
y.add((float) 4.3);
y.add((float) 5.0);
//将坐标呈现键值对的方式加入到一个集合中
ArrayList<ArrayList<Entry>> list = new ArrayList<ArrayList<Entry>>();
ArrayList<Entry> entryList = new ArrayList<Entry>();
if (x.size() > y.size()) {
    for (int i = 0; i < x.size(); i++) {
        entryList.add(new Entry(x.get(i), y.get(i)));
    }
} else {
    for (int i = 0; i < y.size(); i++) {
        entryList.add(new Entry(x.get(i), y.get(i)));
    }
}
list.add(entryList);
//实例化工具类
SingleChartUtils singleChartUtils = new SingleChartUtils();
//调用线形图
singleChartUtils.showSingleLineChart(this, linechart, list, 0, false);
工具类:
public class SingleChartUtils implements OnChartValueSelectedListener {

    private Context context;
    private LineChart lineChart;
    private ArrayList<ArrayList<Entry>> lists;
    private float aFloat;
    private GreenMarkerViewRight markerViewRight;
    private int dataSetIndex;
    private boolean isShow;
    private Entry e1;
    private GreenMarkerViewLeft markerViewLeft;

    /**
     * 单根平滑曲线图
     * @param context  上下文
     * @param mChart  控件
     * @param list 坐标集合
     * @param max 最大值,可以传0
     * @param showLegend 是否启用设置,可以传入false
     */
    public void showSingleLineChart(Context context, LineChart mChart, ArrayList<ArrayList<Entry>> list, float max, boolean showLegend) {
        this.context = context;
        lineChart = mChart;
        lists = list;
        // no description text
        Description description = mChart.getDescription();
        description.setEnabled(false);
        description.setText("");

        mChart.setTouchEnabled(true);
        mChart.setOnChartValueSelectedListener(this);

        mChart.setDragDecelerationFrictionCoef(0.9f);

        mChart.setDragEnabled(true);
        mChart.setScaleEnabled(true);
        mChart.setDrawGridBackground(false);
        mChart.setHighlightPerDragEnabled(true);

        // if disabled, scaling can be done on x- and y-axis separately
        mChart.setPinchZoom(true);
        mChart.setScaleEnabled(true);
        mChart.setScaleXEnabled(true);
        mChart.setScaleYEnabled(true);

        // set an alternative background color
        mChart.setBackgroundColor(Color.TRANSPARENT);

        // add data 单根曲线的数值
        setSingleData(mChart,list);

        mChart.animateX(2000);

        // get the legend (only possible after setting data)
        Legend l = mChart.getLegend();
        //是否启用设置
        l.setEnabled(showLegend);

        // modify the legend ...
        l.setForm(Legend.LegendForm.LINE);
        l.setFormLineWidth(1f);
        l.setFormSize(20f);
        l.setTextSize(11f);
        l.setFormToTextSpace(10f);//legend和文字之间的距离
        l.setXEntrySpace(30f);//legend之间的距离
        l.setTextColor(R.color.light_grey);
        l.setVerticalAlignment(Legend.LegendVerticalAlignment.TOP);
        l.setHorizontalAlignment(Legend.LegendHorizontalAlignment.CENTER);
        l.setOrientation(Legend.LegendOrientation.HORIZONTAL);
        l.setDrawInside(false);
        l.setYOffset(10f);//Y方向上的位移

        XAxis xAxis = mChart.getXAxis();
        xAxis.setTextSize(11f);
        xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
        xAxis.setTextColor(R.color.light_grey);
        xAxis.setDrawGridLines(true);
        xAxis.setGranularityEnabled(true);
        xAxis.setLabelCount(12);//轴上的标签数量

        YAxis leftAxis = mChart.getAxisLeft();
        leftAxis.setTextColor(R.color.light_grey);
//        leftAxis.setAxisMaximum(max);
        leftAxis.setAxisMinimum(0f);
        leftAxis.setDrawGridLines(false);
        leftAxis.setDrawAxisLine(false);

        YAxis rightAxis = mChart.getAxisRight();
        rightAxis.setEnabled(false);
    }

    /**
     * 单根曲线的数值
     * @param mChart
     * @param list
     */
    private void setSingleData(LineChart mChart,ArrayList<ArrayList<Entry>> list) {

        ArrayList<Entry> yVals1 = list.get(0);
        LineDataSet set1;

        if (mChart.getData() != null &&
                mChart.getData().getDataSetCount() > 0) {
            set1 = (LineDataSet) mChart.getData().getDataSetByIndex(0);
            set1.setValues(yVals1);
            mChart.getData().notifyDataChanged();
            mChart.notifyDataSetChanged();
        } else {
            set1 = new LineDataSet(yVals1, "实际");
            set1.setAxisDependency(YAxis.AxisDependency.LEFT);
            set1.setColor(context.getResources().getColor(R.color.light_green));
            set1.setCircleColor(ColorTemplate.getHoloBlue());
            set1.setDrawCircles(false);
            set1.setMode(LineDataSet.Mode.CUBIC_BEZIER);
            set1.setLineWidth(1f);
            set1.setCircleRadius(3f);
            set1.setFillAlpha(65);
            set1.setFillColor(context.getResources().getColor(R.color.light_green));
            set1.setHighLightColor(context.getResources().getColor(R.color.light_blue));
            set1.setDrawCircleHole(false);
            set1.setDrawValues(false);//曲线上是否显示数据


            LineData data = new LineData(set1);
            data.setValueTextColor(Color.BLACK);
            data.setValueTextSize(9f);

            mChart.setData(data);
        }

    }

    /**
     * 曲线的点击加点事件
     * @param e
     * @param h
     */
    @Override
    public void onValueSelected(Entry e, Highlight h) {

        float x = h.getX();
        float y = h.getY();
        float yPx = h.getYPx();//所点击的点在曲线图中的像素值
        float xPx = h.getXPx();//所点击的点在屏幕中的像素值
        int dataSetIndex = h.getDataSetIndex();
        Log.e("++++++++++++++++","x:"+x+"y:"+y+"xPx:"+xPx+"yPx:"+yPx);
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        int width = wm.getDefaultDisplay().getWidth();
        int bottom = lineChart.getBottom();
        float dimension = context.getResources().getDimension(R.dimen.fifty_two);
        float n = (float) (823.0 / 1080.0);
        float space = width * (1 - n);
        float v = (float)555/(float)600;
        float i = (bottom * v);//x轴在曲线图的位置高度
        aFloat = width - space - xPx;
        Log.e("++++++++++++++++","width:"+width+"+n:"+n+"+space:"+space+"+i:"+i+"+aFloat:"+ aFloat);

        if (aFloat <130){
            markerViewRight = new GreenMarkerViewRight(context, R.layout.green_marker_view_right, lists.get(0).size());
            markerViewRight.setChartView(lineChart);
            lineChart.setMarker(markerViewRight);
        }else {
            markerViewLeft = new GreenMarkerViewLeft(context, R.layout.green_marker_view_left, lists.get(0).size());
            markerViewLeft.setChartView(lineChart);
            lineChart.setMarker(markerViewLeft);
        }

        this.dataSetIndex = h.getDataSetIndex();
        if (isShow){
            e1.setIcon(null);
            isShow = false;
        }
        if (!isShow && this.dataSetIndex !=2){
            Bitmap originalBitmap = BitmapFactory.decodeResource(context.getResources(),
                    R.drawable.sign);
            int originalWidth = originalBitmap.getWidth();
            int originalHeight = originalBitmap.getHeight();
            int newWidth = 45;
            int newHeight = 45; // 自定义 高度 暂时没用

            float scale = ((float) newHeight) / originalHeight;
            Matrix matrix = new Matrix();
            matrix.postScale(scale, scale);
            Bitmap changedBitmap = Bitmap.createBitmap(originalBitmap, 0, 0,
                    originalWidth, originalHeight, matrix, true);
            BitmapDrawable bitmapDrawable = new BitmapDrawable(context.getResources(), changedBitmap);
            e.setIcon(bitmapDrawable);
            isShow = true ;
            e1 = e;
            lineChart.invalidate();
        }
    }

    @Override
    public void onNothingSelected() {
        e1.setIcon(null);
        isShow = false;
    }

}
工具类配套类:
public class GreenMarkerViewRight extends MarkerView {

    private final TextView tvCusuomMarkerViewDate;
    private final TextView tvCusuomMarkerViewEnergy;
    private final int size;
    private int dataSetIndex;

    /**
     * Constructor. Sets up the MarkerView with a custom layout resource.
     * @param context
     * @param layoutResource the layout resource to use for the MarkerView
     * @param index
     */
    public GreenMarkerViewRight(Context context, int layoutResource, int index) {
        super(context, layoutResource);
        this.size = index;
        tvCusuomMarkerViewDate = (TextView) findViewById(R.id.custom_tv_marker_view_date);
        tvCusuomMarkerViewEnergy = (TextView) findViewById(R.id.custom_tv_marker_view_energy);
    }

    @Override
    public void refreshContent(Entry e, Highlight highlight) {
        dataSetIndex = highlight.getDataIndex();
        int stackIndex = highlight.getDataSetIndex();
        int dataIndex = highlight.getDataIndex();
        String s = Utils.formatNumber(e.getX(), 0, true);
        Log.e("+++++++++++++++-",dataIndex+"s"+s);
        if (e instanceof CandleEntry) {
            CandleEntry ce = (CandleEntry) e;
            tvCusuomMarkerViewEnergy.setText("" + Utils.formatNumber(ce.getHigh(), 0, true));

        } else {
            tvCusuomMarkerViewEnergy.setText("" + Utils.formatNumber(e.getY(), 0, true));
            String s1 = Utils.formatNumber(e.getX(), 0, true);
            if (s1.length()==1){
                tvCusuomMarkerViewDate.setText("0"+s1+":00");
            }else {
                tvCusuomMarkerViewDate.setText(s1+":00");
            }

            String[] data = (String[]) e.getData();
        }

        super.refreshContent(e, highlight);
    }

    @Override
    public MPPointF getOffset() {
        Log.e("--------->>>>>",getWidth()+"+"+getHeight());
        return new MPPointF(-getWidth()-10, -(getHeight()/2));
    }

}
配套类1布局:
<?xml version="1.0" encoding="utf-8"?>
<com.zhy.autolayout.AutoLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="149px"
    android:layout_height="176px"
    android:orientation="vertical"
    android:background="#0621f2"
    android:paddingTop="20px"
    android:paddingBottom="14px"
    android:paddingRight="15px"
    android:paddingLeft="20px">

    <TextView
        android:id="@+id/custom_tv_marker_view_date"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:text="12:00"
        android:textSize="18px"
        android:textColor="@color/light_grey"
        android:ellipsize="end"
        android:lines="1"
        android:textAppearance="?android:attr/textAppearanceSmall" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/custom_tv_marker_view_energy"
        android:layout_marginTop="8px"
        android:gravity="center_horizontal"
        android:text="100000"
        android:textSize="30px"
        android:textColor="#222222"
        android:ellipsize="end"
        android:lines="1"
        android:textAppearance="?android:attr/textAppearanceSmall"/>

    <View
    android:layout_width="match_parent"
    android:layout_height="1px"
    android:background="@color/light_grey"
    android:layout_marginTop="8px"
    android:layout_marginLeft="5px"
    android:layout_marginRight="5px"></View>

    <com.zhy.autolayout.AutoLinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:layout_marginTop="10px">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="16px"
            android:src="@drawable/marker_high"
            />

        <TextView
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        android:id="@+id/custom_tv_marker_view1"
        android:layout_marginLeft="2px"
        android:text="HIGH"
        android:textSize="16px"
        android:textColor="@color/light_green"
        android:ellipsize="end"
        android:lines="1"
        android:gravity="center_vertical"
        android:textAppearance="?android:attr/textAppearanceSmall"/>

        <TextView
        android:layout_width="50px"
        android:layout_height="16px"
        android:id="@+id/custom_tv_marker_view_state1"
        android:text="1111"
        android:textSize="16px"
        android:textColor="@color/light_green"
        android:ellipsize="end"
        android:lines="1"
        android:textAppearance="?android:attr/textAppearanceSmall"/>

    </com.zhy.autolayout.AutoLinearLayout>

    <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:layout_marginTop="8px">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="16px"
            android:src="@drawable/marker_low"
            android:layout_gravity="bottom"
            android:paddingBottom="2px"/>

        <TextView
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        android:id="@+id/custom_tv_marker_view2"
        android:layout_marginLeft="2px"
        android:text="LOW"
        android:textSize="16px"
        android:textColor="@color/light_pink"
        android:ellipsize="end"
        android:lines="1"
        android:textAppearance="?android:attr/textAppearanceSmall"/>

    <TextView
    android:layout_width="50px"
    android:layout_height="18px"
    android:id="@+id/custom_tv_marker_view_state2"
    android:text="11111"
    android:textSize="16px"
    android:textColor="@color/light_pink"
    android:ellipsize="end"
    android:lines="1"
    android:textAppearance="?android:attr/textAppearanceSmall"/>

    </LinearLayout>

</com.zhy.autolayout.AutoLinearLayout>
 
配套类2:
public class GreenMarkerViewLeft extends MarkerView {

    private final TextView tvCusuomMarkerViewDate;
    private final TextView tvCusuomMarkerViewEnergy;
    private final int size;
    private int dataSetIndex;

    /**
     * Constructor. Sets up the MarkerView with a custom layout resource.
     *  @param context
     * @param index
     * @param layoutResource the layout resource to use for the MarkerView
     */
    public GreenMarkerViewLeft(Context context, int layoutResource, int index) {
        super(context, layoutResource);
        this.size = index;
        tvCusuomMarkerViewDate = (TextView) findViewById(R.id.custom_tv_marker_view_date);
        tvCusuomMarkerViewEnergy = (TextView) findViewById(R.id.custom_tv_marker_view_energy);
    }

    @Override
    public void refreshContent(Entry e, Highlight highlight) {
        dataSetIndex = highlight.getDataIndex();
        int stackIndex = highlight.getDataSetIndex();
        Log.e("+++++++++++++++",dataSetIndex+"+"+stackIndex);
        if (e instanceof CandleEntry) {
            CandleEntry ce = (CandleEntry) e;

            tvCusuomMarkerViewEnergy.setText("" + Utils.formatNumber(ce.getHigh(), 0, true));
        } else {

            tvCusuomMarkerViewEnergy.setText("" + Utils.formatNumber(e.getY(), 0, true));
            String s1 = Utils.formatNumber(e.getX(), 0, true);
            if (s1.length()==1){
                tvCusuomMarkerViewDate.setText("0"+s1+":00");
            }else {
                tvCusuomMarkerViewDate.setText(s1+":00");
            }
            String[] data = (String[]) e.getData();

        }

        super.refreshContent(e, highlight);
    }

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

}
配套类2布局与1的一样
柱形图可以用曲线图一样的数据,方法也在工具类中有,直接调用就可以了。
//柱形图------
// 柱形图  注意,x轴坐标必须从零开始,不然会报错下标越界

BarData barData = charUtils.getBarData(this, x, y);
charUtils.showBarChart(bar_chart, barData, x);
唯一的问题就是,柱形图的需要重写一下,不能直接使用,这是布局
<qnkj.cn.practice.view.MyBarChart
    android:layout_width="match_parent"
    android:layout_height="300px"
    android:id="@+id/bar_chart"
   ></qnkj.cn.practice.view.MyBarChart>
重写
public class MyBarChart extends BarChart {
    public MyBarChart(Context context) {
        super(context);
    }

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

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












评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值