android自定义带动画的柱状图控件

本文介绍了一位开发者原创的Android自定义柱状图控件,该控件具备动画效果和点击事件。作者通过继承View,重写ondraw方法,使用Canvas进行绘制。文章分享了实现思路,包括数据接收、刻度绘制、柱子布局、颜色随机分配以及动画效果的同步绘制等,并提供了部分代码。同时,作者还提到了回调代码的相关内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.也是突然的想法,写了个柱状图,然后觉得静态的太死板又加上去了动画和点击事件.

2.下面开始上菜!各位道友觉得不好呢,就多给点批评建议,然后都给点个赞,哈哈~(原创作品,转载请标明)

(上传图片竟然不能超过2M,,,,,,,,,,,,本来做了个gif,一看9M多.....重新做了一个,,,就录了上方的一半将就看吧)

3.思路:一般自定义控件都要继承View或者一个控件,然后重写它的ondraw方法,在这个方法里面使用canvas各种浪.

4.柱状图控件代码,直接上了(细节思路,中途遇到的问题,存在的缺陷,心得等都在下面哦):

package com.jeska.testrecyclerview.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;
/**
 * Created by jeska on 2016/8/12.
 */
public class ZhuZhuangTu extends View {
    private Thread mThread;
    /**标题*/
    private String title = "柱状图";
    /***/
    private Paint mPaint;
    private String emptyWarning = "没有数据";
    private Context mContext;
    private ArrayList<HashMap<String,String>> mDataList;
    private ArrayList<HashMap<String,Float>> mLocationList;
    private int[] colors;
    /**整个空间的宽度*/
    private int width;
    /**整个空间的高度*/
    private int height;
    /**箭头宽度*/
    private int arrowWidth = 10;
    /**坐标原点距离边界*/
    private int distance = 30;
    /**底部辅助横线条数*/
    private int horizontalLineCount = 10;
    /**柱形图的宽度*/
    private float ZhuZhuangTuWidth;
    /**水平线间隔*/
    private float horizontalLineInterval;
    /**柱状图间隔*/
    private float ZhuZhuangTuInterval;
    private Random mRandom;
    /**用来做动画的可变值*/
//    private float delta;
    /**用来做动画的线程while的flag*/
    private boolean run = true;
    /**动画时长*/
    private int millions = 6000;
    /**回调*/
    private OnZhuZhuangTuClickListener mListener;
    /**设备密度*/
    private float density;
    /**存放所有的delta*/
    private float[] deltas;
    /**最大值出现的位置*/
    private int maxValuePosition;
    public ZhuZhuangTu(Context context) {
        super(context);
        init(context);
    }

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

    public ZhuZhuangTu(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        this.mContext = context;
        mPaint = new Paint();
        mLocationList = new ArrayList<>();
        run = true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        setWillNotDraw(false);
        if (isDataMapEmpty()){
            canvas.drawColor(Color.GRAY);
            mPaint.setColor(Color.MAGENTA);
            mPaint.setTextSize(40f);
            canvas.drawText(emptyWarning, width / 2 - mPaint.getTextSize()*emptyWarning.length()/2, height / 2-mPaint.getTextSize()/2, mPaint);
        }else{
            mPaint.setColor(Color.BLACK);
            mPaint.setStrokeWidth(4);
            //绘制x轴
            canvas.drawLine(distance * density, height - distance * density, width - distance * density, height - distance * density, mPaint);
            //绘制x轴箭头
            canvas.drawLine(width - distance * density-arrowWidth * density, height - distance * density-arrowWidth * density, width - distance * density, height - distance * density, mPaint);
            canvas.drawLine(width - distance * density - arrowWidth * density, height - distance * density + arrowWidth * density, width - distance * density, height - distance * density, mPaint);
            //绘制y轴
            canvas.drawLine(distance * density, distance * density, distance * density, height - distance * density, mPaint);
            //绘制y轴箭头
            canvas.drawLine(distance * density - arrowWidth * density, distance * density + arrowWidth * density, distance * density, distance * density, mPaint);
            canvas.drawLine(distance * density + arrowWidth * density, distance * density + arrowWidth * density, distance * density, distance * density, mPaint);
            //绘制底部阶梯标准线
            horizontalLineInterval = (height - 2 * distance * density) / horizontalLineCount;
            mPaint.setColor(Color.GRAY);
            mPaint.setStrokeWidth(1);
            float[] arr = getMaxValue(mDataList);
            if (arr[1] == 0){
                return;
            }
            float average = arr[0] / (horizontalLineCount);
            mPaint.setTextSize(18);
            for (int i = 1 ;i<=horizontalLineCount;i++){
                canvas.drawLine(distance * density, height - distance * density - horizontalLineInterval * i, width - distance * density, height - distance * density - horizontalLineInterval * i,mPaint);
                canvas.drawText(average * i+"",5f,height - distance * density - horizontalLineInterval * i + mPaint.getTextSize()/2,mPaint);
            }
            canvas.drawText("0", 5f, height - distance * density + mPaint.getTextSize() / 2, mPaint);
            mPaint.setStrokeWidth(3f);
            ZhuZhuangTuInterval = (width - 2 * distance * density)/ mDataList.size();
            ZhuZhuangTuWidth = ZhuZhuangTuInterval/3;
            mLocationList.clear();
            for (int i = 0;i< mDataList.size();i++){
                mPaint.setColor(colors[i]);
                float value = Float.parseFloat(mDataList.get(i).get("value").toString());
                //从上到下的动画
//                canvas.drawRect(distance * density + ZhuZhuangTuInterval * i + ZhuZhuangTuInterval / 2 - ZhuZhuangTuWidth / 2,
//                        height - distance * density - (height - 2 * distance * density) * (value / arr[0]),
//                        distance * density + ZhuZhuangTuInterval * i + ZhuZhuangTuInterval / 2 + ZhuZhuangTuWidth / 2
//                        , height - distance * density - delta, mPaint);
                //从下到上的动画,所有的图形的绘制时间都是一样的(delta一样),也就是说所有柱子同时绘制完成,长的绘制早,短的绘制晚.
                canvas.drawRect(distance * density + ZhuZhuangTuInterval * i + ZhuZhuangTuInterval / 2 - ZhuZhuangTuWidth / 2,
                        height - distance * density - (height - 2 * distance * density) * (value / arr[0]) + deltas[i],
                        distance * density + ZhuZhuangTuInterval * i + ZhuZhuangTuInterval / 2 + ZhuZhuangTuWidth / 2
                        , height - distance * density, mPaint);
                //若要所有的柱子同时开始绘制,需要给每个柱子添加一个delta.并且要对动画时间分别控制.
//                System.out.println(".........."+deltas[i]);
                HashMap<String,Float> map = new HashMap<>();
                map.put("left",distance * density + ZhuZhuangTuInterval * i + ZhuZhuangTuInterval / 2 - ZhuZhuangTuWidth / 2);
                map.put("top",height - distance * density - (height - 2 * distance * density) * (value / arr[0]));
                map.put("right",distance * density + ZhuZhuangTuInterval * i + ZhuZhuangTuInterval / 2 + ZhuZhuangTuWidth / 2);
                map.put("bottom",height - distance * density + deltas[i]);
                mLocationList.add(map);
                mPaint.setColor(Color.BLACK);
                mPaint.setTextSize(20);
                String name = mDataList.get(i).get("name").toString();
                canvas.drawText(name,distance* density+ZhuZhuangTuInterval*i+ZhuZhuangTuInterval/2-(name.length()/2)*mPaint.getTextSize(),
                        height-distance* density+mPaint.getTextSize(),mPaint);
                String unit = mDataList.get(i).get("unit").toString();
                String describe = value+unit;
                canvas.drawText(describe,
                        distance* density+ZhuZhuangTuInterval*i+ZhuZhuangTuInterval/2-ZhuZhuangTuWidth/2,
                        height - distance * density - (height - 2 * distance * density) * (value / arr[0]),mPaint);
            }
            mPaint.setColor(Color.BLACK);
            canvas.drawText("unit:"+mDataList.get(0).get("unit").toString(),0,distance* density/2,mPaint);
            mPaint.setTextSize(30);
            canvas.drawText(title,width/2-title.length()/2*mPaint.getTextSize(),distance* density,mPaint);
        }
        if (stop(deltas) && !mThread.isAlive()){
            if (null != mListener){
                mListener.onDrawFinished();
            }
        }
    }
    /**是否为空,同来判断绘制是否完成*/
    private boolean stop(float[] arr){
        for(float f : arr){
            if (f != 0){
                return false;
            }
        }
        return true;
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                float x = event.getX();
                float y = event.getY();
                for (int i = 0;i<mLocationList.size();i++){
                    if (x>=mLocationList.get(i).get("left") && x<=mLocationList.get(i).get("right")
                            && y >=mLocationList.get(i).get("top") && y<=mLocationList.get(i).get("bottom")){
                        if (null != mListener){
                            mListener.onZhuZhuangTuClick(i,mLocationList.get(i));
                        }
                    }
                }


                break;
        }
        return super.onTouchEvent(event);
    }

    /**获取集合中的最大值*/
    private float[] getMaxValue(ArrayList<HashMap<String,String>> list) {
        try {
            float max = Float.parseFloat(list.get(0).get("value").toString());
            for (int i = 0;i<list.size();i++){
                float temp = Float.parseFloat(list.get(i).get("value").toString());
                if (temp > max){
                    max = temp;
                }
            }
            return new float[]{max,1};
        }catch (Exception e){
            return new float[]{-1,0};
        }
    }
    /**获取最大值出现的位置*/
    private int getMaxValuePosition(ArrayList<HashMap<String,String>> list){
        try {
            int j = 0;
            float max = Float.parseFloat(list.get(0).get("value").toString());
            for (int i = 0;i<list.size();i++){
                float temp = Float.parseFloat(list.get(i).get("value").toString());
                if (temp > max){
                    j = i;
                }
            }
            return j;
        }catch (Exception e){
            return 0;
        }
    }
    /**设置数据*/
    public void setDataMap(ArrayList<HashMap<String,String>> list){
        if (null == list || list.size() ==0){
            return;
        }
        this.mDataList = list;
        colors = new int[mDataList.size()];
        mRandom = new Random();
        for (int i = 0;i<mDataList.size();i++){
            int r = mRandom.nextInt(256);
            int g= mRandom.nextInt(256);
            int b = mRandom.nextInt(256);
            int color = Color.rgb(r, g, b);
            colors[i] = color;
        }
    }
    /**设置动画时间*/
    public void setAnimationDurtion(int millions){
        this.millions = millions;
    }
    /**监察数据集合是否为空*/
    private boolean isDataMapEmpty(){
        if (null == mDataList || mDataList.size() ==0){
            return true;
        }else{
            return false;
        }
    }
    /**设置回调*/
    public void setOnZhuZhuangTuClickListener(OnZhuZhuangTuClickListener mListener){
        this.mListener = mListener;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        width = this.getMeasuredWidth();
        height = this.getMeasuredHeight();
        density = getDensity();
        maxValuePosition = getMaxValuePosition(mDataList);
//        delta = height - 2*distance*density;
        //获取所有的delta
        if (!isDataMapEmpty()){
            deltas = new float[mDataList.size()];
            float m = getMaxValue(mDataList)[0];
            for (int i =0;i<mDataList.size();i++){
                float v = Float.parseFloat(mDataList.get(i).get("value").toString());
                float delta = (height - 2*distance*getDensity())*(v/m);
                deltas[i] = delta;
            }
        }

    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    public void layout(int l, int t, int r, int b) {
        super.layout(l, t, r, b);
        mThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (run){
                    try {
                        Thread.sleep(15);
                        for (int i =0;i<deltas.length;i++){
                            //                        delta = delta - (height - 2*distance*density)/(millions/20);//做向下的动画时使用
                            float delta = deltas[i];
                            float m = getMaxValue(mDataList)[0];
                            float v = Float.parseFloat(mDataList.get(i).get("value").toString());
                            float time = millions*(v/m);
                            if (time <= 800){
                                time = 800;
                            }
                            delta = delta - (height - 2*distance*density)*(v
                                    /m)/(time/15);
                            if (delta <=0){
                                delta = 0;
                                if (i==maxValuePosition){
                                    run = false;
                                }
                            }
                            deltas[i] = delta;
                        }
                        postInvalidate();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        mThread.start();
    }

    /**获取设备密度*/
    private float getDensity(){
        DisplayMetrics metric = new DisplayMetrics();
        WindowManager manager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        manager.getDefaultDisplay().getMetrics(metric);
        return metric.density;
    }
    /**为柱状图设置标题*/
    public void setTitle(String title) {
        this.title = title;
    }
}

5.note:

1>接收数据的集合是我写死的ArrayList<HashMap<String,String>>,hashmap的键必须有name(每个柱子的名称),value(每个柱子的值),unit(单位);这些都是可以改的,根据自己的需要改就行了.

2>获取控件的宽高,并且将高度分成10份,从0到集合中数据的最大值,分为10个刻度绘制在y轴的左侧,并给每个刻度加上水平线.(这一块我没优化,根据数据来的,有时候可能刻度都不是整数,根据需要自己调整吧.)

3>x轴是按照数据集合中柱子的数量n,平分为n段,每一段再分成3份,取中间的一份画柱子,左右的两份空白.

4>在ondraw中,刻度,柱子名称,柱子高度,图表名称,水平线等是一次绘制,而柱状图是循环绘制.

5>ArrayList<HashMap<String,Float>>mLocationList用来存放所有的柱子的"左上右下"值,用以点击事件中.

6>每个柱子的颜色都是随机的

7>deltas数组存放着所有柱子的变化高度,在onlayout中开启的线程,控制着数组的值,每次数值变化后使用postInvalidate()重新绘制,所以每个柱子都是同时开始绘制的,并且完成时间是按照自身的高度.(这个可能要具体体会一下了,,,,,,)

8>还有些细节的东西慢慢看吧......

6.回调代码:

package com.jeska.testrecyclerview.view;
import java.util.HashMap;
/**
 * Created by jeska on 2016/8/12.
 */
public interface OnZhuZhuangTuClickListener {
    void onZhuZhuangTuClick(int position,HashMap<String,Float> locationMap);
    void onDrawFinished();
}
7.使用,先上布局文件:等分成了4块

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:orientation="horizontal"
        >
        <com.jeska.testrecyclerview.view.ZhuZhuangTu
            android:id="@+id/zzt1"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"/>
        <com.jeska.testrecyclerview.view.ZhuZhuangTu
            android:id="@+id/zzt2"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"/>
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        >
        <com.jeska.testrecyclerview.view.ZhuZhuangTu
            android:id="@+id/zzt3"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"/>
        <com.jeska.testrecyclerview.view.ZhuZhuangTu
            android:id="@+id/zzt4"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="match_parent"/>
    </LinearLayout>
</LinearLayout>
8.activity代码:

package com.jeska.testrecyclerview;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;
import com.jeska.testrecyclerview.view.OnZhuZhuangTuClickListener;
import com.jeska.testrecyclerview.view.ZhuZhuangTu;
import java.util.ArrayList;
import java.util.HashMap;
/**
 * Created by jeska on 2016/8/12.
 */
public class ZhuZhuangTuActivity extends AppCompatActivity {
    private ZhuZhuangTu zzt1;
    private ZhuZhuangTu zzt2;
    private ZhuZhuangTu zzt3;
    private ZhuZhuangTu zzt4;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_zhuzhuangtu);
        zzt1 = (ZhuZhuangTu) findViewById(R.id.zzt1);
        zzt2 = (ZhuZhuangTu) findViewById(R.id.zzt2);
        zzt3 = (ZhuZhuangTu) findViewById(R.id.zzt3);
        zzt4 = (ZhuZhuangTu) findViewById(R.id.zzt4);
        final ArrayList<HashMap<String,String>> list = new ArrayList<>();
        HashMap<String,String> map1 = new HashMap<>();
        map1.put("name","初中");
        map1.put("value", "100");
        map1.put("unit", "元");
        list.add(map1);
        HashMap<String,String> map2 = new HashMap<>();
        map2.put("name","高中");
        map2.put("value", "300");
        map2.put("unit", "元");
        list.add(map2);
        HashMap<String,String> map3 = new HashMap<>();
        map3.put("name","大学");
        map3.put("value", "500");
        map3.put("unit", "元");
        list.add(map3);
        HashMap<String,String> map4 = new HashMap<>();
        map4.put("name","研究生");
        map4.put("value", "1000");
        map4.put("unit", "元");
        list.add(map4);
        HashMap<String,String> map5 = new HashMap<>();
        map5.put("name","博士");
        map5.put("value", "2000");
        map5.put("unit", "元");
        list.add(map5);
        HashMap<String,String> map6 = new HashMap<>();
        map6.put("name","硕导");
        map6.put("value", "3000");
        map6.put("unit", "元");
        list.add(map6);
        HashMap<String,String> map7 = new HashMap<>();
        map7.put("name","博导");
        map7.put("value", "4000");
        map7.put("unit", "元");
        list.add(map7);
        HashMap<String,String> map8 = new HashMap<>();
        map8.put("name","boss");
        map8.put("value", "6000");
        map8.put("unit", "元");
        list.add(map8);
        zzt1.setDataMap(list);
        zzt1.setAnimationDurtion(3000);
        zzt1.setTitle("学历与日薪");
        zzt1.setOnZhuZhuangTuClickListener(new OnZhuZhuangTuClickListener() {
            @Override
            public void onZhuZhuangTuClick(int position, HashMap<String, Float> locationMap) {
                Toast.makeText(ZhuZhuangTuActivity.this, "第" + position + "个:" + list.get(position).get("name") + ":" + list.get(position).get("value") + list.get(position).get("unit"),
                        Toast.LENGTH_SHORT).show();
            }
            @Override
            public void onDrawFinished() {
                Toast.makeText(ZhuZhuangTuActivity.this,"zzt1...绘制完成",
                        Toast.LENGTH_SHORT).show();
            }
        });
        zzt4.setDataMap(list);
        zzt4.setAnimationDurtion(18000);
        zzt4.setTitle("学历与日薪");
        zzt4.setOnZhuZhuangTuClickListener(new OnZhuZhuangTuClickListener() {
            @Override
            public void onZhuZhuangTuClick(int position, HashMap<String, Float> locationMap) {
                Toast.makeText(ZhuZhuangTuActivity.this, "第" + position + "个:" + list.get(position).get("name") + ":" + list.get(position).get("value") + list.get(position).get("unit"),
                        Toast.LENGTH_SHORT).show();
            }
            @Override
            public void onDrawFinished() {
                Toast.makeText(ZhuZhuangTuActivity.this,"zzt4...绘制完成",
                        Toast.LENGTH_SHORT).show();
            }
        });
        final ArrayList<HashMap<String,String>> list2 = new ArrayList<>();
        HashMap<String,String> map11 = new HashMap<>();
        map11.put("name", "动漫");
        map11.put("value", "1000");
        map11.put("unit", "部");
        list2.add(map11);
        HashMap<String,String> map12 = new HashMap<>();
        map12.put("name", "电视剧");
        map12.put("value", "200");
        map12.put("unit", "部");
        list2.add(map12);
        HashMap<String,String> map13 = new HashMap<>();
        map13.put("name", "电影");
        map13.put("value", "500");
        map13.put("unit", "部");
        list2.add(map13);
        HashMap<String,String> map14 = new HashMap<>();
        map14.put("name", "综艺");
        map14.put("value", "1500");
        map14.put("unit", "部");
        list2.add(map14);
        zzt2.setDataMap(list2);
        zzt2.setAnimationDurtion(8000);
        zzt2.setTitle("视频的崛起");
        zzt2.setOnZhuZhuangTuClickListener(new OnZhuZhuangTuClickListener() {
            @Override
            public void onZhuZhuangTuClick(int position, HashMap<String, Float> locationMap) {
                Toast.makeText(ZhuZhuangTuActivity.this, "第" + position + "个:" + list2.get(position).get("name") + ":" + list2.get(position).get("value") + list2.get(position).get("unit"),
                        Toast.LENGTH_SHORT).show();
            }
            @Override
            public void onDrawFinished() {
                Toast.makeText(ZhuZhuangTuActivity.this, "zzt2...绘制完成",
                        Toast.LENGTH_SHORT).show();
            }
        });
        zzt3.setDataMap(list2);
        zzt3.setAnimationDurtion(13000);
        zzt3.setTitle("视频的崛起");
        zzt3.setOnZhuZhuangTuClickListener(new OnZhuZhuangTuClickListener() {
            @Override
            public void onZhuZhuangTuClick(int position, HashMap<String, Float> locationMap) {
                Toast.makeText(ZhuZhuangTuActivity.this, "第" + position + "个:" + list2.get(position).get("name") + ":" + list2.get(position).get("value") + list2.get(position).get("unit"),
                        Toast.LENGTH_SHORT).show();
                Snackbar snackbar = Snackbar.make(zzt3,"VideoView方式查看视频",Snackbar.LENGTH_INDEFINITE);
                snackbar.setAction("确定", new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        Intent intent = new Intent(ZhuZhuangTuActivity.this, VideoActivity.class);
                        startActivity(intent);
                    }
                });
                snackbar.setActionTextColor(Color.MAGENTA);
                snackbar.show();
            }
            @Override
            public void onDrawFinished() {
                Toast.makeText(ZhuZhuangTuActivity.this, "zzt3...绘制完成",
                        Toast.LENGTH_SHORT).show();
            }
        });
    }
}
9.好了,全都奉献了,写的急,可能有不妥的地方,还望诸位道友批评指正啦,





评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值