Android群英传读书笔记——Android控件架构与自定义控件详解

3.1  Android控件架构

        Android中,控件分为两类,ViewGroup和View。ViewGroup作为父控件可包含多个View。控件树如下:

        

        上层控件负责下层控件的测量和绘制,并传递交互事件。

 

        每个Activity都包含一个Window对象(通常由PhoneWindow实现)。PhoneWindow将一个DecorView(decor

布置,装饰; 布景;)设置为根View。DecorView封装了窗口操作的通用方法。DecorView把所有要现实的内容呈现在PhoneWindow,这里面所有View的监听事件,都听过WindowManagerService来进行接收,并通过Activity来回调相应的onClickListener。DecorView将显示器分为TitleView和ContentView(id为content的FrameLayout,activity.xml就是设置在这样一个FrameLayout里)。视图树如下:

        

        视图树的第二层装载了一个LinearLayout,常见的上面显示TitleBar下面显示Content。标准视图树如下:

        

        在代码中,setContentView()方法后,ActivityManagerService会回调onResume()方法,此时系统才会把DecorView添加到PhoneWindow中,并让其显示出来,完成整个界面的绘制。

 

3.2  View的测量

        要画一个图形,就要知道它的大小和位置。系统在绘制View前,通过onMeasure()方法测量View

       MeasureSpec类可帮助测量View。它是一个32位的int值,高两位代表测量模式,低三十位为测量的大小,使用位运算为了提高效率。

        测量模式如下:

  • EXACTLY:当View的layout_width和layout_height为固定数值或者match_parent时
  • AT_MOST:当View的layout_width和layout_height为wrap-content时
  • UNSPECIFIED(未指定的):它不指定其大小测量模式,View想多大就多大,通常情况下在绘制自定义View是才会使用

      View类默认的onMeasure()方法只支持EXACTLY模式,如果要自定义View支持wrap_content属性,就必须重写onMeasure()方法来制定wrap_content时的大小。

        测量步骤:

        在自定义View中重写onMeasure()方法,初步写法如下:

@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
    super.omMeasure(widthMeasureSpec,heightMeasureSpec);
}

        进入super.onMeasure(),发现系统最终调用的setMeasuredDimension(int measuredwidth,int measuredHeight)。所以重写onMeasure()的目的就是把宽高值赋给终极父类的setMeasureDimension()方法。正式写法如下:

@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
    setMeasureDimension(measureWidth(widthMeasureSpec),measureHeight(heightMeasureSpec));
}

        其中,measureWidth和measureHeight为我们自定义方法。

        以MeasureWidth为例:

private int measureWidth(int widthMeasureSpec){
    int result=0;
    int specMode=MeasureSpec.getMode(widthMeasureSpec);
    int specSize=MeasureSpec.getSize(widthmeasureSpec);

    if(specMode==MeasureSpec.EXACTLY){
        result=specSize;
    }else{
        result=200;//自定义默认值
        if(specMode==MeasureSpec.AT_MOST){
            result=Math.min(result,specSize);
        }
    }
    return result;
}

        

        当指定wrap_content时,View就获得一个默认大小200。

 

3.3  View的绘制

        通常需要继承View并重写其onDraw(canvas)方法,在onDraw(canvas)的参数canvas(画布)上用paint绘制。

        首先创建一个canvas

        Canvas canvas=new Canvas(bitmap);

        通过传入bitmap的方式创建canvas,传入的bitmap与canvas是紧密相连的,此过程称为装载画布。这个bitmap用来存储所有绘制在canvas上的像素信息。后面调用的所有的Canvas.drawXXX方法都发生在这个bitmap上。

        可以再View类的onDraw()方法中绘制两个bitmap

canvas.drawBitmap(bitmap1,0,0,null);
canvas.drawBitmap(bitmap2,0,0,null);

        对于bitmap2,将它装载在另一个对象中

Canvas canvas=new Canvas(bitmap2);

        在其他地方使用Canvas类绘图方法在装载bitmap2的Canvas对象上进行绘图

mCanvas.drawXXX;

        通过mCanvas将挥之效果作用在了bitmap2上,刷新View的时候,通过onDraw()方法就改变了bitmap2,。并不是直接绘制在onDraw()指定的canvas上,而是通过改变bitmap,让View重绘,显示改变之后的bitmap。

        (PS:有点不明白,感觉作者的意思是,canvas可装载bitmap,通过drawXXX改变自己的bitmap,也可通过drawBitmap绘制别人的bitmap。drawXXX可在其他地方使用,drawBitmap必须在onDraw()使用来绘制。先挖个坑,以后彻底明白了,回来填坑)

 

3.4  ViewGroup的测量

        当ViewGroup的measureMode为EXACTLY或AT_MOST时,按指定值绘制。

        当ViewGroup的measureMode为UNSPECIFIED时,需要对子View进行遍历,一遍获得所有子View的大小,从而决定自己的大小。

        ViewGroup遍历子View,调用子View的measure方法。子View的measure方法如下:

public final void measure(int widthmeasureSpec,int heightmeasureSpec){
    ...
    onMeasure(widthmeasureSpec,heightmeasureSpec);//子View重写的测量方法
    ...
}

        大小确定后,需要知道位置。这就是View的Layout过程。ViewGroup在执行Layout时,同样适用遍历调用子View的Layout方法,并指定其具体的显示位置。

        自定义ViewGroup时,通常重写onLayout来控制字View的显示位置。如果需要支持wrap_content,同样要重写onMeasure。

 

3.5  ViewGroup的绘制

        ViewGroup除非指定了背景色,会调用onDraw(),否则不需要绘制。但是,ViewGroup会使用dispatchDraw()方法来绘制其子View,其过程同样是遍历所有子View,并调用子View的o绘制方法完成绘制。

 

3.6  自定义View

      在自定义View时,通常重写onDraw()方法来绘制View的显示内容。如果该View需要wrap_content属性,还要重写onMeasure()。通过自定义attrs属性,可以设置新的属性配置值。

        在View中通常由以下比较重要的回调方法:

  • onFinishInflated():从XML加载组件后回调
  • onSizeChanged():组件大小改变时回调
  • onMeasure():回调该方法来进行测量
  • onLayout():回调该方法来确定显示的位置
  • onTouchView():监听到触摸事件时的回调

        通常由以下三种方式来实现在定义控件:

  • 对现有控件进行拓展
  • 通过组合实现新的控件
  • 重写View来实现全新的控件

3.6.1  对现有控件进行拓展

        例:给TextView多绘制几层背景

        自定义View继承TextView,重写onDraw()

@Override 
protected void onDraw(Canvas canvas){
    //在TextView绘制文本前,可加入自己的逻辑
    super.onDraw(canvas);//TextView的onDraw(),实现了绘制文字
    //在TextView绘制文本后,可加入自己的逻辑
}

        在自定义View的构造方法中完成画笔等的初始化操作

mPaint1=new Paint();
mPaint1.setColor(getResources().getColor(android.R.color.holo_blue_light));
mPaint1.setStyle(Paint.Style.FILL);
mPaint2=new Paint();
mPaint2.setColor(Color.YELLOW);
mPaint2.setStyle(Paint.Style.FILL);

        重写onDraw(canvas)

@Override
protected void onDraw(canvas){
    //在绘制文字前县绘制两个矩形
    //绘制外层矩形
    canvas.drawRect(0,0,getMeasureWidth,getMeasureHeiget,mPaint1);
    //绘制内层矩形
    canvas.drawRect(10,10,getMeasuredWidth-10,getMeasureHeight-10.mPaint2);
    //用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作。
    canvas.save();
    //绘制文字前平移10像素
    canvas.tranlate(10,0);
    //父类绘制文本
    super.onDraw();
    //用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。
    canvas.restore();
    //当我们对画布进行旋转,缩放,平移等操作的时候其实我们是想对特定的元素进行操作,比如图片,一个        
    //矩形等,但是当你用canvas的方法来进行这些操作的时候,其实是对整个画布进行了操作,那么之后在画
    //布上的元素都会受到影响,所以我们在操作之前调用canvas.save()来保存画布当前的状态,当操作之后
    //取出之前保存过的状态,这样就不会对其他的元素进行影响
}

        例:利用LinearGradient Shader和Matrix实现由动态文字闪动效果的TextView

        

        可充分利用Paint对象的Shader渲染器。通过设置一个不断变化的LinearGradient,并使用带有该属性的Paint对象来绘制现实的文字。首先,在onSizeChanged方法中进行一些对象的初始化工作,并根据View的宽度设置一个LinearLayoutGradient渐变渲染器。

int mViewWidth=0;
Paint mPaint;
LinearGradient mLinearGradient;
Matrix mGradientMatrix;

@Override
protected void onSizeChanged(int w,int h,int oldw,int oldh){
    super.onSizeChanged(w,h,oldw,oldh);
    if(mViewWidth==0){
        mViewWidth=getMeasureWidth();
        if(mViewWidth>0){
            mPaint=getPaint();
            mLinnearGradient=new LinearGradient(0,
                                                0,
                                                mViewWidth,
                                                0,
                                            new int[]{Color.BLUE,0xffffffff,Color.BLUE},
                                                null,
                                                Shader.TileMode.CLAMP);
            mPaint.setShader(mLinearGradient);
            mGradientMatrix=new Matrix();
        }
    }
}

        最关键的是getPaint获取Paint对象并设置原生TextView没有的LinearGradient属性。在onDraw中,通过矩阵方式不断平移渐变效果,从而在绘制文字时,产生动态的渐变效果。

@Override
protected void onDraw(Canvas canvas){
    super.onDraw(canvas);
    if(mGradientMatrix!=null){
        mTranslate+=mViewWidth/5;
        if(mTranslate>2*mViewWidth){
            mTranslate-=mViewWidth;
        }
        mGradientMatrix.setTranslate(mTranslate,0);
        mLinearGradient.setLocalMatrix(mGradientMatrix);
        postInvalidateDelayed(100);
    }
}

3.6.2  创建复合控件

        这种方式需要集成一个合适的ViewGroup,再给它添加指定的控件。一般会给它指定一些可配置的属性。

        例:TopBar

        这种UI模板应该具有通用性和可定制性,需要给调用者以丰富的接口,让他们可以更改模板中的文字、颜色、行为等信息。

3.6.2.1  定义属性

        在values下的attrs.xml添加以控件名为节点名的节点,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="TopBar">
        <attr name="title" format="string"/>
        <attr name="titleTextSize" format="dimension"/>
        <attr name="titleTextColor" format="color"/>
        <attr name="leftTextColor" format="color"/>
        <attr name="leftBackground" format="reference|color"/>
        <attr name="leftText" format="string"/>
        <attr name="rightTextColor" format="color"/>
        <attr name="rightBackgroundColor" format="reference|color"/>
        <attr name="rightText" format="string"/>
    </declare-styleable>
</resources>

        通过declare-styleable声明使用自定义属性,通过name确定引用的名称。通过attr声明具体的自定义属性。format的reference属性代表引用属性,比如leftBackground可以设置为一张图片。

        TopBar继承RelativeLayout,在构造方法中,使用如下代码获取自定义属性

TypedArray ta=context.obtainStyledAttributes(attr,R.styleable.TopBar);

        TypedArray为系统提供的获取自定义属性集的自定义结构,获取自定义属性

//通过这个方法,将attrs.xml中定义的declare-styleable的所有属性的值存储到TypedArray中
TypedArray ta=context.obtaibstyledAttributes(attrs,R.styleable.TopBar);
//从TypedArray中取出对应的值来用于后续绘制
mLeftTextColor=ta.getColor(R.styleable.TopBar_leftTextColor,0);
mLeftBackground=ta.getDrawable(R.styleable.TopBar_lefBackground);
mLeftText=ta.getString(R.styleable.leftText);
mRightTextColor=ta.getColor(R.styleable.TopBar_rightTextColor,0);
mRightBackground=ta.getDrawable(R.styleable.TopBar_rightBackGround);
mRightText=ta.getText(R.styleable.TopBar_rightText);

mTitleTextSize=ta.getDimension(R.styleable.TopBar_titleSize,10);
mTitleTextColor=ta.getColor(R.styleable.TopBar_titleTextColor,0);
mTitle=ta.getString(R.styleable.TopBar_title);

//获取完TypeArray的值后,一般需要调用recycle方法完成资源的回收
ta.recycle();

3.6.2.2  组合控件

        通过动态添加addView()的方式,并设置前面获取到的属性值

mLeftButton=new Button(context);
mRightButton=new Button(context);
mTitleView=new TextView(context);

//为创建的组件元素赋值
//值就来源于引用xml文件中对应该出的值
mLeftButton.setTextColor(mLeftTextColor);
mLeftButton.setBackGround(mLeftBackground);
mLeftButton.setText(mLeftText);

mRightButton.setTextColor(mLeftTextColor);
mRightButton.setBackGround(mRightBackground);
mRightButton.setText(mRightText);

mTitleView.setText(mTitle);
mTitleView.setTextColor(mTitleTextColor);
mTitleView.setTextSize(mTitleTextSize);
mTitleView.setGravity(Gravity.CENTER);

//为组件设置相应的布局元素
mLeftParams=new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.MATCH_PARENT);
mLwftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT,TRUE);
//添加到ViewGroup
addView(mLeftButton,mLeftParams);

mRightParams=new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.MATCH_PARENT);
mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT,TRUE);
addView(mRightButton,mRightParams);

mTitleParams=new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT);
mTitleParams.addRule(RelativeLayout.CENTER_IN_PARENT,TRUE);
addView(mTitleView,mTitleParams);

        定义接口

//报漏一个方法给调用者注册接口回调
//通过接口来获得回调者对接口方法的实现

public void setOnTopBarClickListener(topBarClickListener mListener){
    this.mListener=mListener;
}

//接口对象,实现回调机制,在回调方法中
//通过映射的接口对象调用接口中的方法
//而不用去考虑如何实现,具体的实现由调用者去创建

public interface topBarClickListener{
    //左侧按钮点击事件
    void leftClick();
    //右侧按钮点击事件
    void rightClick();
}

//按钮的点击事件,只需要调用接口的方法,会掉的时候,会有具体的实现

mLeftButton.setOnClickListener(new onClickListener(){
    @Override
    public void onClick(View v){
       mListener.liftClick();
    }
});

mRightButton.setOnClickListener(new onClickListener(){
    @Override
    public void onClick(View v){
       mListener.rightClick();
    }
});

        除了通过接口会掉的方式动态的控制模板,还可以使用公共方法动态的修改UI

//id区分按钮,flag区分显示
public void setButtonVisible(int id,boolean flag){
    if(flag){
        if(id==0){
            mLeftButton.setVisible(View.VISIBLE);
        }else{
            mRightButton.setVisible(View.VISIBLE);
        }
    }else{
        if(id==0){
            mLeftButton.setVisible(View.INVISIBLE);
        }else{
            mRightButton.setVisible(View.INVISIBLE);
        } 
    }
}

3.6.2.3  引用UI模板

        在引用前,需要制定第三方控件的命名空间。

xmlns:android="http://schemas.android.com/apk/res/android"

        这行代码指定引用的命名空间为xmlns,即xml namespace。这里制定了命名空间为"android",接下来使用系统属性的时候,才可以使用“android”。第三方控件使用如下代码来引入名字空间,这里我们把第三方控件的命名空间指定为custom。

xmlns:custom="http://schemas.android.com/apk/res-auto"

        实例代码如下:

<com.inooc.systemwidget.TopBar
    android:id="@+id/topBar"
    android:layout_width="match_parent"
    android:layout+height="40dp"
    custom:leftBackGround="@drawable/blue_button"
    custom:leftText="Back"
    custom:leftTextColor="#FFFFFF"
    custom:leftBackGround="@drawable/blue_button"
    custom:leftText="More"
    custom:leftTextColor="#FFFFFF"
    custom:title="自定义标题"
    custom:titleTextColor="#123456"
    custom:titleTextSize="10sp"/>

        使用自定义控件时需要指定完整的包名。

        如果将其写成一个独立的布局文件以供调用

<com.inooc.systemwidget.TopBar         
    xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:id="@+id/topBar"
    android:layout_width="match_parent"
    android:layout+height="40dp"
    custom:leftBackGround="@drawable/blue_button"
    custom:leftText="Back"
    custom:leftTextColor="#FFFFFF"
    custom:leftBackGround="@drawable/blue_button"
    custom:leftText="More"
    custom:leftTextColor="#FFFFFF"
    custom:title="自定义标题"
    custom:titleTextColor="#123456"
    custom:titleTextSize="10sp">
<com.inooc.systemwidget.TopBar/>

        可在其他布局文件中,通过<include>来调用

<include layout="@layout/topbar"/>

3.6.3  重写View实现全新的控件

3.6.3.1  弧线展示图

        

        这个自定义View分为中间的圆形,显示的文字,已经最外的环形。只要在onDraw中一个个去绘制就可以了。

        初始化一些值

//圆环半径,length为控件宽度
mCircleXY=length/2;
//圆环内外径之差
mRadius=(float)(length*0.5/2);
//圆环外接矩形
mArcRectF=new RectF((float)length*0.1,
                    (float)length*0.1,
                    (float)length*0.9,
                    (float)length*0.9);

      在onDraw方法中调用

//绘制圆,参数圆心XY坐标,半径和画笔
canvas.drawCircle(mCircleXY,mCircleXY,nRadius,mCirclePaint);
//绘制弧线,参数外接矩形,起始角度,扫过角度,是否经过圆心(经过为扇形),画笔
canvas.drawArc(mArcRectF,270,mSweepAngle,false,mArcPaint);
//绘制文字
canvas.drawText(mShowText,0,mShowText.Length(),mCircleXY,mCircleXY+(mShowTextSize/4),mPaintText);

3.6.3.2  音频条形图

        核心是绘制一个个矩形

@Override
protected void onDraw(Canvas canvas){
    super.onDraw(canvas);
    for(int i=0;i<nRectCount;i++){
        canvas.drawRect(
            (float)(mWidth*0.4/2+mRectWidth*i+offset),
            currentHeight,
            (float)(mWidth*0.4/2+mRectWidth*(i+1)),
            mRectHeight,
            mPaint
        );
    }
}

        高度动态随机变化

mRandom=Math.Random();
float currentHeight=(float)(mRectHeight*mRandom);

        在onDraw()中调用invalidate()方法通知View重绘。使用postInvalidateDelayed(300)进行延迟,避免刷新过频。

        给Paint增加一个LinearGradient的渐变效果

@Override
protected void onSizeChanged(int w,int h,int oldw,int oldh){
    super.onSizeChanged(w,h,oldw,oldh);
    mWidth=getWidth();
    mRectHeight=getHeight();
    mRectWidth=(int)(mWidth*0.6/mRectCount);
    mLinearGradient=new LinearGradient(
        0,0,mRectWidth,mRectHeight,
        COLOR.YELLOW,COLOR.BLUE,Shader.TileMode.CLAMP
    );
    mPaint.setShader(mLinearGradient);
}

 

3.7  自定义ViewGroup

        ViewGroup存在的意义就是对子View进行管理。通常需要重写onMeasure,onLayout,onTouchEvent。

        例:一个有惯性的ScrollView,滑动超过一定距离会惯性画出下一个View,小于一定距离会弹回原位置

        重写onMeasure

@Override
protected void onMeasure(int WidthMeasureSpec,int HeightMeasureSpec){
    super.onMeasure(WidthMeasureSpec,HeightMasureSpec);
    int count=getChildCount();
    for(int i=0;i<count;i++){
        View childView=getChildAt(i);
        measureChild(WidthMeasureSpec,HeightMeasureSpec);
    }
}

        设定View大小为完整的一屏以达到明显滑动效果,确定ViewGroup高度

//设置ViewGroup的高度
MarginLayoutParams mpl=(MarginLayoutParams)getLayoutParams();
mpl.geight=mScreenHeight*childCount;
setLayoutParams(mpl);

        重写onLayout

@Override
protected void onLayout(boolean changed,int l,int t,int r,int b){
    int childCount=getChildCount();
    //设置ViewGroup的高度
    MarginLayoutParams mlp=(MarginLayoutParams)getLayoutParams();
    mpl.height=mScreenHeight*childCount;
    setLayoutParams(mlp);
    for(int i=0;i<childCount;i++){
        View childView=getChildAt(i);
        childView.layout(l,i*mScreenLayout,r,(i+1)*mScreenLayout);
    }
}

        计算滑动距离dy

case MotionEvent.ACTION_DOWN:
    mLastY=y;
    break;
case MotionEvent.ACTION_MOVE:
    if(!mScroller.isFinished){
        mScroller.abortAnimation();
    }
    int dy=mLastY-y;
    if(getScrollY<0){
        dy=0;
    }
    if(getScrollY>getHeight()-mScreenHeight){
        dy=0;
    }
    scrollby(0,dy);
    mLastY=y;
    break;
    //getScrollY()的详解参考
    //https://blog.csdn.net/linmiansheng/article/details/17767795
    //讲的很简单透彻

    //scrollto scrollby  参考
    //https://www.jianshu.com/p/2e60448ac44c
    

        判断手指离开后的惯性效果

case MotionEvent.ACTION_DOWN:
    //记录触摸起点
    mStart=getScrollY();
case MotionEvent.ACTION_UP:
    //记录触摸终点
    mEnd=getScrollY();
    int dScrollY=mEnd-mStart;
    if(dScrollY>0){
        if(dScroll<mScreenHeight/3){
            mScroller.startScroll(0,getScrollY,0,-dScrollY);
        }else{
            mScroller.startScroll(0,getScrollY,0,mScreenHeight-dScrollY);
        }
    }else{
        if(-dScroll<mScreenHeight/3){
            mScroller.startScroll(0,getScrollY,0,-dScrollY);
        }else{
             mScroller.startScroll(0,getScrollY,0,-mScreenHeight-dScrollY);
        }
    }
    break;

        整个onTouchEvent的代码如下

        加上computeScroll代码

@Override
public void computeScroll(){
    super.computeScroll();
    if(mScroller.computeScrollOffset()){
        scrollTo(0,mScroller.getCurrentY());
        postInValitate();
    }
}

 

3.8  事件拦截机制分析

        ViewGroup重写三个方法

//事件分发方法
@Override
public boolean dispatchTouchEvent(MotionEvent ev){
    return super.dispatchTouchEvent(ev);
}

//事件拦截方法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev){
    return super.onInterceptYouchEvent(ev);
}

//触摸事件
@Override
public boolean onTouchEvent(MotionEvent ev){
    return super.onTouchEvent(ev);
}

        View重写两个方法

@Override
public boolean dispatchTouchEvent(MotionEvent ev){
    super.dispatchTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent ev){
    super.onMotionEvent(ev);
}

 

        事件流程:以ViewGroupA嵌套ViewGroupB嵌套MyView为例,不做改动下log为

        

        

        分发拦截事件向下传递,触摸处理向上传递。

        一般不需要改dispatchTouchEvent

        onInterceptTouchEvent:事件传递的返回值:True,拦截,False,不拦截,继续流程

        onTouchEvent:事件处理:True,处理了,不用向上传递,False,向上传递

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值