sudoku

目录

  • 项目名称

  • 项目概述

  • 项目要求

  • 设计开发

    • 引导页面
    • 主页面
    • 关于页面
    • 关卡选择页面
    • 游戏页面
    • 排行榜页面
  • 项目展示

  • 项目总结

  • 源码


项目名称

Sudoku(数独游戏)

项目概述

数独是源自18世纪瑞士的一种数学游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫(3*3)内的数字均含1-9,不重复。设计开发一个简单的安卓版数独游戏。

项目要求

  1. 设计完整的APP结构,包括以下页面

    • 引导页面
    • 主页面
    • 关卡选择页面
    • 游戏页面
    • 排行榜页面
    • “关于”页面
  2. 游戏共24关,每六个一组,分为四个难度

  3. 在游戏页面,若玩家填入的数字不合法,用红色字体表示,合法则用蓝色字体表示,游戏初始化的数字用黑色字体。

  4. 玩家完成关卡后,显示祝贺信息及用时,并将完成日期,完成关卡,及用时写入数据库。

  5. 排行榜页面中,显示玩家所有完成关卡的游戏记录,并且关卡,用时均按升序进行二级排序。

设计开发

引导页面

该页面只起引导作用,即用户打开程序,显示载入图片,约 3s 后自动跳转至主页面。具体实现如下

  1. 新建一个空活动,GuideActivity,并设置为 launch_activity ,作为引导页面。
  2. 在活动的布局文件中不需要其他控件,仅设置背景为载入图片
  3. 设置该活动全屏显示,实现方法如下 (使用该方法需要 1 中的 GuideActivity 直接继承自 Activity 类)
 		//全屏
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN);
        View decorView = getWindow().getDecorView();
        int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_FULLSCREEN;
        decorView.setSystemUiVisibility(uiOptions);

注:后文中涉及全屏显示均使用以上代码

  1. 设置计时器以实现自动跳转
		//定义一个用来打开MainActivity的Intent
        Intent intent = new Intent(GuideActivity.this, MainActivity.class);
        //设置计时器,等待3s后启动活动
        Timer timer = new Timer();
        TimerTask tast = new TimerTask() {
            @Override
            public void run() {
                startActivity(intent);
            }
        };
            timer.schedule(tast, 3000);
  1. 解决一些其他的问题
    因为该页面只起引导作用,所以只需在打开应用时显示,退出应用时应不再显示。若不做处理,在主页面点击返回按钮,或点击退出时,将再次跳转至该活动,解决方法:
    1. 在 GuideActivity 类中设置一个静态变量 state 并初始化为0;
    2. 重写该活动的 OnPause(),OnStop()方法,当这些方法被调用时将 state 的值改为1;
    3. 重写该活动的 OnCreat() 方法时,先判断,若 state == 1 则直接退出活动;
主页面

该页面包括游戏标题及四个按钮

  • 开始游戏
  • 排行榜
  • 关于
  • 退出游戏

实现如下

  1. 新建活动 MainActivity ,在其布局文件中添加一个 ImageView 来显示游戏的标题图片。
  2. 在布局文件中添加四个 Button 分别对应上述四个按钮。
  3. 为按钮添加监听事件。
    例:
		Button btn1= findViewById(R.id.btn1);
        btn1.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, LevelChooseActivity.class);
                startActivity(intent);
            }
        });
  1. 重写 OnKeyDown() 方法为返回按钮添加“再按一次退出程序”功能
	@Override
    	public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK
                && event.getAction() == KeyEvent.ACTION_DOWN) {
            if ((System.currentTimeMillis() - exitTime) > 2000) {
                //若连续点击时间间隔大于2s则弹出提示
                Toast.makeText(getApplicationContext(), "再按一次退出程序", Toast.LENGTH_SHORT).show();
                //记录当前点击时间
                exitTime = System.currentTimeMillis();
            //若连续点击时间间隔小于2s则直接退出
            } else {
                finish();
            }
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }
“关于”页面

该页面显示游戏介绍,版本信息,作者联系方式等信息,只需要一个 TextView 即可。

关卡选择页面

该页面包含24个按钮,点击按钮便跳转至对应关卡。

  1. 新建活动 LevelChooseActivity ,并在布局文件中添加24个 Button 并设置页面背景色及按钮背景色。

  2. 在类中创建一个 Button 数组来存储24个按钮。并以此为其设置监听事件,使得点击按钮时打开游戏页面。

    		for (int i = 0; i < levels.length; i++) {
            final int t = i + 1;//表示关卡
            levels[i].setOnClickListener(new View.OnClickListener() {
                public void onClick(View v) {
                    Intent intent=new Intent(LevelChooseActivity.this,GameActivity.class);
                    //将关卡信息传入GameActivity
                    intent.putExtra("level",t);
                    startActivity(intent);
                }
            });
    
  3. 关于按钮的尺寸及位置
    考虑到不同手机屏幕的大小不同,应动态的设置按钮的大小位置,实现如下

    1. 得到屏幕尺寸
    	//得到屏幕尺寸
        WindowManager wm = (WindowManager) this
                .getSystemService(Context.WINDOW_SERVICE);
        width = wm.getDefaultDisplay().getWidth();
        height = wm.getDefaultDisplay().getHeight();
        //每行四个按钮
        size = width / 4.0F;
    
    1. 计算按钮大小并动态设置
     	float white = size * 0.2F;//2*margin
        float color = size * 0.8F;//按钮边长
        //获得导航栏高度
        Resources resources = this.getResources();
        int resourceId = resources.getIdentifier("navigation_bar_height","dimen", "android");
        int h = resources.getDimensionPixelSize(resourceId);
        float white_y = (height-6*color-1.5F*h)/6.0F;
        //设置Button尺寸
        TableRow.LayoutParams layoutParams = new TableRow.LayoutParams();
        layoutParams.width=(int)color;
        layoutParams.height = (int)color;
        layoutParams.leftMargin=(int)(white*0.5F);
        layoutParams.rightMargin=(int)(white*0.5F);
        layoutParams.topMargin=(int)(white_y*0.5F);
        layoutParams.bottomMargin=(int)(white_y*0.5F);
        for(int i=0;i<24;i++){
            levels[i].setLayoutParams(layoutParams);
        }
    
游戏页面

游戏页面为游戏的主体部分,自定义一个控件 SudoView 来绘制游戏界面,新建一个 Game 类实现游戏逻辑。

Game类

该类控制游戏逻辑,实现初始化游戏,设置计时器,判断玩家填数是否合法,判断游戏是否结束等方法。

package com.mahaoyuan.sudoku;

	public class Game {
    
    //储存数独的初始情况用0表示空位
    private final String[] str = new String[24];
    //表示当前关卡
    private int level;
    //对应数独的81个格子,根据玩家操作更新。
    private int sudoku[] = new int[81];
    //初始数组,不随玩家操作更新,即记录玩家不可更改的格子
    private int initial[] = new int[81];
    //表示某格子不能填哪些数
    private int used[][][] = new int[9][9][];
    //记录游戏开始时间
    private long time;

    public Game(int Level){
        level = Level;
        getString();
        sudoku = StringtoArray(str[Level-1]);
        initial=StringtoArray(str[Level-1]);
        //得到当前系统时间
        time=System.currentTimeMillis();
        calculateAllUsedTiles();
    }
    //得到游戏开始时间
    public long getTime(){return time;}
    //得到当前关卡
    public int getLevel(){return level;}
    //得到x行y列的格子中的值
    private int getTile(int x,int y){
        return sudoku[9*y+x];
    }
    //初始化24个关卡
    private void getString(){
        str[0]="360000000004230800000004200070460003820000014500013020001900000007048300000000045";
        str[1]="005020109018094003060010007690850030002100800030400051700080090500270310106040700";
        str[2]="600003007509082010070400082130007800080009030005320071950001020060870104800200003";
        str[3]="030901600050600830790004002040020050807010306010098020900500083073009060002103070";
        str[4]="480300102070460300006200700210090800007804900004003071008002400001035090603008017";
        str[5]="950400068600107004003060700040080071020905030570001090008010600100604007760002013";

        str[6]="207600001100400070003008090008000609004900500905006200070300100050001004300002708";
        str[7]="006004010020060040040080200600100302300706005204009006007050030090010050010900600";
        str[8]="007000600006832700020000080060504090100000007090703010050000070004156200008000400";
        str[9]="700108005020040010001000700600504002030070040400803006004000200070090080900307004";
        str[10]="007314900000600001000059408080000049000805000190000060201960000500001000003572100";
        str[11]="007406000030109002008000150020800060304000907060004030059000400100605080000703600";

        str[12]="700802004080600200090040600000006002050008030400000000007020080009007060100503007";
        str[13]="050800010000001700080004900500002001200130008800900007008700020005200000010009040";
        str[14]="009600000600050070005100008030200400080090050006530010100007800050080006000005300";
        str[15]="400102003008600050010005400050400800000000000001900020007500090030006100100304005";
        str[16]="200007060006504009700002040900050000004008700000000006050800004100206500080700001";
        str[17]="060200070100005020005046100070000000600300009000010040004120900030500004090007010";

        str[18]="020000900060200007700400000005700009800500006600000300000006002100005080003000040";
        str[19]="400000080060100300090800000002060900040000030008002700000006010001007050020000006";
        str[20]="020000000500007001008006002009100040200000006030070900800500200600300005000000090";
        str[21]="179000000465000000328000000000000000000000000000000000000000653000000794000000812";
        str[22]="005800700003700090009000000001004030020900070080000100000000900040002600006001800";
        str[23]="950800000104009200000340000005000090308070506020000700000058000007200603000007012";
    }
    
    //将x行y列的格子中的值转换为字符串
    public String getTileString(int x,int y){
        int v=getTile(x,y);
        if(v==0)
            return "";
        else
            return String.valueOf(v);
    }
    //将字符串数组转换为数字
    protected int[] StringtoArray(String str){
        int [] sudo=new int[81];
        for(int i=0;i<81;i++)
            sudo[i]=str.charAt(i)-'0';
        return sudo;
    }
    //计算x行y列的格子中不能在填的数字
    public int[] calculateUsedTile(int x,int y){
        int c[]=new int[9];
        //找出所在行已经填过的数字
        for (int i=0;i<9;i++) {
            if (i == y)
                continue;
            int t = getTile(x, i);
            if (t != 0)
                c[t - 1] = t;
        }
        //找出所在列已经填过的数字
        for (int i=0;i<9;i++) {
            if (i == x)
                continue;
                int t=getTile(i,y);
                if(t!=0)
                    c[t-1]=t;
        }
        //找出所在矩形已经填过的数字
        int startx=(x/3)*3;
        int starty=(y/3)*3;
        for(int i=startx;i<startx+3;i++)
            for (int j=starty;j<starty+3;j++){
                if(i==x&&j==y)
                    continue;
                int t=getTile(i,j);
                if(t!=0)
                    c[t-1]=t;
            }

            int nused=0;
            for(int t:c)
                if(t!=0)
                    nused++;
            int cc[]=new int[nused];
            nused=0;
        for(int t:c)
            if(t!=0)
                cc[nused++]=t;

        return cc;
    }
    //计算used数组
    public void calculateAllUsedTiles(){
        for(int i=0;i<9;i++)
            for (int j=0;j<9;j++)
                used[i][j]=calculateUsedTile(i,j);
    }
    //将x行y列的格子中的值改为value
    protected void setTile(int x,int y,int value){
        if(value==10)
            value=0;
        sudoku[y*9+x]=value;
    }
    //判断玩家是否可更改x行y列的格子的值
    public boolean isEditable(int x,int y){
        if(initial[x+9*y]==0)
            return true;
        else
            return false;
    }
    //判断玩家所填数字是否合法
    public boolean isValid(int x,int y){
        calculateUsedTile(x,y);
        for(int t:used[x][y])
            //若所填数字在used数组中出现过则不合法
            if(sudoku[9*y+x]==t)
                return false;
        return true;
    }
    //判断游戏是否结束
    public boolean isFinished(){
        calculateAllUsedTiles();
        for(int i=0;i<9;i++)
            for(int j=0;j<9;j++)
                if(getTile(i,j)==0||!isValid(i,j))
                    //若有格子为空,或所填数字不合法,则游戏未结束
                    return false;
        return true;
    }

}
SudoView类

该类继承自 View 类,完成游戏界面绘制,触碰事件响应等。

package com.mahaoyuan.sudoku;

import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.MotionEvent;
import android.view.View;
import java.text.SimpleDateFormat;
import java.util.Date;


	public class SudoView extends View {
    public SudoView(Context context) {
        super(context);
    }

    //记录每个格子的尺寸
    private float width;
    private float height;
    //记录触摸位置
    private int selectx;
    private int selecty;

    private Game game;

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        this.width=w/9f;
        this.height=h/9f;
        super.onSizeChanged(w, h, oldw, oldh);
    }
    //重写OnDraw()方法,绘制游戏界面
    @Override
    protected void onDraw(Canvas canvas) {

        //绘制背景颜色
        Paint background=new Paint();
        background.setARGB(175,196,224,225);
        canvas.drawRect(0,0,getWidth(),getHeight(),background);

        //格子线条画笔
        Paint light=new Paint();
        light.setARGB(100,7,152,199);
        //加深线条画笔
        Paint hilite=new Paint();
        hilite.setARGB(100,109,173,226);
        //大矩形的边界线条,更粗。
        Paint dark=new Paint();
        dark.setARGB(255,25,25,112);
        //绘制线条
        for(int i=0;i<=9;i++)
        {
            //细线
            canvas.drawLine(0,i*height,getWidth(),i*height,light);
            canvas.drawLine(0,i*height+1,getWidth(),i*height+1,light);
            canvas.drawLine(0,i*height+2,getWidth(),i*height+2,hilite);
            canvas.drawLine(i*width,0,i*width,getHeight(),light);
            canvas.drawLine(i*width+1,0,i*width+1,getHeight(),light);
            canvas.drawLine(i*width+2,0,i*width+2,getHeight(),hilite);
            //粗线
            if(i%3==0)
            {
                canvas.drawLine(i*width,0,i*width,getHeight(),dark);
                canvas.drawLine(i*width+1,0,i*width+1,getHeight(),dark);
                canvas.drawLine(i*width+2,0,i*width+2,getHeight(),hilite);
                canvas.drawLine(0,i*height,getWidth(),i*height,dark);
                canvas.drawLine(0,i*height+1,getWidth(),i*height+1,dark);
                canvas.drawLine(0,i*height+2,getWidth(),i*height+2,hilite);
            }
        }
        //单独绘制最下面的粗线
        canvas.drawLine(0,10*height,getWidth(),10*height,dark);
        canvas.drawLine(0,10*height+1,getWidth(),10*height+1,dark);
        canvas.drawLine(0,10*height+2,getWidth(),10*height+2,hilite);
        //数字画笔
        Paint number=new Paint();
        number.setStyle(Paint.Style.FILL);
        number.setTextSize(height*0.75f);
        number.setTextAlign(Paint.Align.CENTER);
        //控制数字大小
        Paint.FontMetrics fm=number.getFontMetrics();
        float x=width/2;
        float y=height/2-(fm.ascent+fm.descent)/2;
        //绘制数字
        for(int i=0;i<9;i++)
            for(int j=0;j<9;j++)
            {
                //初始化的数字,不可编辑
                if(!game.isEditable(i,j))
                    number.setColor(Color.BLACK);
                //不合法的数字
                else if(!game.isValid(i,j))
                    number.setColor(Color.RED);
                //合法数字
                else
                    number.setColor(Color.BLUE);
                canvas.drawText(game.getTileString(i,j),i*width+x,j*height+y,number);
            }
        super.onDraw(canvas);
    }
    //重写 OnTouchEvent()
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(event.getAction()!=MotionEvent.ACTION_DOWN)
            return super.onTouchEvent(event);
        //得到点击位置
        selectx=(int)(event.getX()/width);
        selecty=(int)(event.getY()/height);
        //若点击位置小于零或处于不可编辑格子,不进行任何操作
        if(selecty<0||!game.isEditable(selectx,selecty))
            return false;
        //显示数字显示页面
        KeyDialog keyDialog=new KeyDialog(getContext());
        keyDialog.show();
        setListeners(keyDialog);
        return true;
    }
    //设置监听事件
    public void setListeners(final KeyDialog keyDialog){
        for(int i=0;i<keyDialog.keys.length;i++) {
            final int t = i + 1;
            keyDialog.keys[i].setOnClickListener(new View.OnClickListener() {
                public void onClick(View v) {
                    setSelectedTile(t);
                    keyDialog.dismiss();
                    //如果游戏结束,调用Finish()方法
                    if(game.isFinished())
                        Finish();
                }
            });
        }
    }
    //填入数字,更新game
    public void setSelectedTile(int tile){
        game.setTile(selectx,selecty,tile);
        game.calculateAllUsedTiles();
        this.invalidate();
        }
    //设置游戏关卡
    public void setGame(int level) {
        game=new Game(level);
    }
    //结束游戏
    private void Finish(){
        //计算游戏时间
        long t = (System.currentTimeMillis()-game.getTime())/1000;
        //弹出结束对话框
        FinishDialog finishDialog=new FinishDialog(getContext());
        finishDialog.setTime(t);
        finishDialog.show();
        //将本局游戏信息写入数据库
        MySQLiteOpenHelper mySQLiteOpenHelper = new MySQLiteOpenHelper(getContext());
        SQLiteDatabase mydatebase = mySQLiteOpenHelper.getWritableDatabase();
        ContentValues record = new ContentValues();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss");
        Date date = new Date(System.currentTimeMillis());
        record.put("date",simpleDateFormat.format(date));
        record.put("level",game.getLevel());
        record.put("time",t);
        mydatebase.insert("Rank",null,record);
    }
}

注:FinishDialog 和 Keypad类及布局需自行定义

排行榜页面

该页面显示玩家所有完成关卡的游戏记录,并对用时排序。,主体用 ListView 实现

  1. 新建 RankListActivity 活动,在布局页面中加入一个 TableLayout 其中包括三个 TextView 作为排行榜题头,下面加入一个线性布局,里面是 ListView
<TableLayout
    android:id="@+id/tableLayout"
    android:background="@color/colorRank"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:stretchColumns="*"
    app:layout_constraintBottom_toTopOf="@+id/linearLayout">
    <TableRow>
        <TextView
            android:id="@+id/date"
            android:background="@color/colorRankRow"
            android:text="完成日期"
            android:textAlignment="center"
            android:textSize="25sp" />

        <TextView
            android:id="@+id/level"
            android:background="@color/colorRankRow"
            android:text="关卡"
            android:textAlignment="center"
            android:textSize="25sp" />

        <TextView
            android:id="@+id/usetime"
            android:background="@color/colorRankRow"
            android:text="用时"
            android:textAlignment="center"
            android:textSize="25sp" />
    </TableRow>

</TableLayout>

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/linearLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="3dp"
        app:layout_constraintTop_toBottomOf="@+id/tableLayout">


        <ListView
            android:id="@+id/ranklist"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

        </ListView>
    </LinearLayout>

  1. 在 RankListActivity 中动态设置题头 TextView 尺寸
   		//得到屏幕尺寸
        WindowManager wm = (WindowManager) this
                .getSystemService(Context.WINDOW_SERVICE);
        width = wm.getDefaultDisplay().getWidth();

        TableRow.LayoutParams layoutParams_date = new TableRow.LayoutParams();
        layoutParams_date.width=(int)(width*0.6F);
        layoutParams_date.rightMargin=3;
        layoutParams_date.leftMargin=3;
        layoutParams_date.topMargin=3;
        layoutParams_date.bottomMargin=3;
        TextView tv_date = findViewById(R.id.date);
        tv_date.setLayoutParams(layoutParams_date);

        TableRow.LayoutParams layoutParams_level = new TableRow.LayoutParams();
        layoutParams_level.width=(int)(width*0.2F);
        layoutParams_level.rightMargin=3;
        layoutParams_level.leftMargin=3;
        TextView tv_level = findViewById(R.id.level);
        tv_level.setLayoutParams(layoutParams_level);

        TableRow.LayoutParams layoutParams_time = new TableRow.LayoutParams();
        layoutParams_time.width=(int)(width*0.2F);
        layoutParams_time.leftMargin=3;
        layoutParams_time.rightMargin=3;
        TextView tv_time = findViewById(R.id.usetime);
        tv_time.setLayoutParams(layoutParams_time);

    }
  1. 新建 myAdapter 类继承自 SimpleCursorAdapter 类,重写 getView() 方法,格式化设置 ListView 的 Item 中TextView 的大小,以适应不同尺寸的屏幕。
	public class myAdapter extends SimpleCursorAdapter {
    private float width;
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        View view = super.getView(position,convertView,parent);

        View db_date = view.findViewById(R.id.db_date);
        View db_level = view.findViewById(R.id.db_level);
        View db_time = view.findViewById(R.id.db_time);

        LinearLayout.LayoutParams linearParams0 = (LinearLayout.LayoutParams)db_date.getLayoutParams();
        linearParams0.width = (int)(width*0.6F);
        db_date.setLayoutParams(linearParams0);

        LinearLayout.LayoutParams linearParams1 = (LinearLayout.LayoutParams)db_level.getLayoutParams();
        linearParams1.width = (int)(width*0.2F);
        db_level.setLayoutParams(linearParams1);

        LinearLayout.LayoutParams linearParams2 = (LinearLayout.LayoutParams)db_time.getLayoutParams();
        linearParams2.width = (int)(width*0.2F);
        db_time.setLayoutParams(linearParams2);

        return  view;

    }
  1. 从数据库中读取数据,并显示在 ListView 中
		MySQLiteOpenHelper mySQLiteOpenHelper = new MySQLiteOpenHelper(this);
        SQLiteDatabase db = mySQLiteOpenHelper.getReadableDatabase();
        String sql = "SELECT date as _id, level, time FROM Rank ORDER BY level, time;";
        Cursor cursor = db.rawQuery(sql,null);
        myAdapter adapter =
                new myAdapter(this,R.layout.item,cursor,
                    new String[]{"_id","level","time"},
                        new int[]{R.id.db_date,R.id.db_level,R.id.db_time});
        adapter.setWidth(width);
        ListView listview = findViewById(R.id.ranklist);
        listview.setAdapter(adapter);

项目展示

载入页面
主页面
关卡选择页面
游戏页面
完成游戏
排行榜页面
关于页面

项目总结

  • 第一次 Android 开发实践,过程中遇到了不少问题,最终都依靠 Google 一一解决。
  • 因为缺少经验,许多代码的实现都是只要实现功能就好,也许不是经典的,或者通用的做法。
  • 部分代码的实现只是学到了为了实现对应功能,该怎么使用它,而没有深入研究。
  • Game类和SudoView类的编写使得更加深刻的理解面向对象开发。
  • 目前程序功能简单,单一,日后考虑加入网络游戏,实现网络排名,题库更新等题目

源码

codes on github

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值