AS+kotlin+SurfaceView最佳实践之打造六子棋小游戏

1.源码:

https://gitee.com/zouchengxin/liu_zi_qi

2.效果:

http://39.106.207.193:8080/liu_zi_qi.mp4

3.App下载:

https://gitee.com/zouchengxin/liu_zi_qi/blob/master/app/release/app-release.apk (放心下载)

4.实现功能:

  • 双人对战
  • 悔棋
  • 游戏退出
  • 游戏重新开始

5.未实现功能:

由于时间有限,技术限制等原因未能实现的功能

  • 人机对战:额,这个实现有点复杂,不想做了
  • 蓝牙对战:这个蓝牙模块也没接触过

6.游戏说明:

双方各6个棋子,只能上下左右移动一格,2个相连棋子可以干掉对方一个棋子,3个棋子不行,2个棋子不相连也不行,2个棋子与对方棋子有间隔也不行.不如:白白黑空空可以干掉黑子,白白空黑空,白空白黑空,白白黑空白这些都不行

7.代码:

MainActivity
package com.zcx.liu_zi_qi

import android.content.Intent
import android.os.Bundle
import android.os.PersistableBundle
import android.support.v7.app.AppCompatActivity
import android.view.View
import android.widget.Toast

class MainActivity:AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    /**
     * 人机对战
     */
    fun onRenJi(view:View){
        Toast.makeText(this,"正在进入人机对战", Toast.LENGTH_SHORT).show()
        val intent=Intent()
        intent.setClass(this,GameActivity::class.java)
        intent.putExtra("model",GameModel.RENJI)
        startActivity(intent)
    }

    /**
     * 双人PK
     */
    fun onTwo(view:View){
        Toast.makeText(this,"正在进入双人对战", Toast.LENGTH_SHORT).show()
        val intent=Intent()
        intent.setClass(this,GameActivity::class.java)
        intent.putExtra("model",GameModel.TWO)
        startActivity(intent)
    }

    /**
     * 蓝牙对战
     */
    fun onLanYa(view:View){
        Toast.makeText(this,"正在进入蓝牙对战", Toast.LENGTH_SHORT).show()
        val intent=Intent()
        intent.setClass(this,GameActivity::class.java)
        intent.putExtra("model",GameModel.LANYA)
        startActivity(intent)
    }
}

主活动类:3个按钮的显示和点击事件的编写

GameActivity
package com.zcx.liu_zi_qi
import android.annotation.TargetApi
import android.app.AlertDialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Point
import android.os.Build
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Message
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.collections.HashMap

class GameActivity : AppCompatActivity(),Handler.Callback {
    val handle=Handler(this)
    var windowWidth=0
    var windowHeight=0
    lateinit var bt_regret:Button
    lateinit var bt_restart:Button
    lateinit var bt_exit:Button
    lateinit var tv_black:TextView
    lateinit var tv_white:TextView
    lateinit var gameView:GameUI

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val display=(getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
        val size=Point()
        display.getSize(size)
        windowWidth=size.x
        windowHeight=size.y
        println("($windowWidth,$windowHeight)")
        init()
    }

    /**
     * 初始化
     */
    fun init(){
        gameView= GameUI(this)
        gameView.layoutParams=ViewGroup.LayoutParams(windowWidth*3/5,ViewGroup.LayoutParams.MATCH_PARENT)

        val userView=LayoutInflater.from(this).inflate(R.layout.user_ui,null)
        userView.layoutParams=ViewGroup.LayoutParams(windowWidth*2/5,ViewGroup.LayoutParams.MATCH_PARENT)

        bt_regret=userView.findViewById<Button>(R.id.bt_regret)
        bt_regret.setOnClickListener {
            synchronized(Map){
                if(!Map.history.isEmpty()){
                    Map.list.removeAll(Map.list)
                    Map.list=Map.copyList(Map.history.last().first)
                    Map.map=Map.copyMap(Map.history.last().second)

                    Map.printList(Map.history.last().first)
                    Map.printMap(Map.history.last().second)
                    Map.history.remove(Map.history.last())
                    Map.printList()
                    Map.printMap()
                }else{
                    Toast.makeText(this,"官人,不能再悔了", Toast.LENGTH_LONG).show()
                }

            }
        }
        bt_restart=userView.findViewById<Button>(R.id.bt_restart)
        bt_restart.setOnClickListener {
            val builder=AlertDialog.Builder(this)
            builder.setTitle("提示")
            builder.setMessage("是否重新开始")
            builder.setPositiveButton("确定",
                object :DialogInterface.OnClickListener{
                    override fun onClick(dialog: DialogInterface?, which: Int) {
                        gameView.exit()
                        this@GameActivity.init()
                    }

                })
            builder.setNegativeButton("取消",
                object :DialogInterface.OnClickListener{
                    override fun onClick(dialog: DialogInterface?, which: Int) {

                    }

                })
            builder.create().show()
        }
        bt_exit=userView.findViewById<Button>(R.id.bt_exit)
        bt_exit.setOnClickListener {
            val builder=AlertDialog.Builder(this)
            builder.setTitle("提示")
            builder.setMessage("是否退出游戏")
            builder.setPositiveButton("确定",
                object :DialogInterface.OnClickListener{
                    override fun onClick(dialog: DialogInterface?, which: Int) {
                        gameView.exit()
                        val intent= Intent()
                        intent.setClass(this@GameActivity,MainActivity::class.java)
                        this@GameActivity.finish()
                    }

                })
            builder.setNegativeButton("取消",
                object :DialogInterface.OnClickListener{
                    override fun onClick(dialog: DialogInterface?, which: Int) {

                    }

                })
            builder.create().show()
        }
        tv_black=userView.findViewById<TextView>(R.id.tv_black)
        tv_white=userView.findViewById<TextView>(R.id.tv_white)


        val layout=LinearLayout(this)
        layout.layoutParams=ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT)
        layout.addView(gameView)
        layout.addView(userView)
        setContentView(layout)
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    override fun handleMessage(msg: Message?): Boolean {
        when(msg?.what){
            1 ->{
                val builder=AlertDialog.Builder(this)
                builder.setTitle("提示")
                val str=when(msg.obj){ChessManType.WHITE ->"白子" else -> "黑子"}
                builder.setMessage("${str}获胜,是否开始下一局")
                builder.setPositiveButton("开始下一局",
                    object :DialogInterface.OnClickListener{
                        override fun onClick(dialog: DialogInterface?, which: Int) {
                            gameView.exit()
                            this@GameActivity.init()
                        }

                    })
                builder.setNegativeButton("退出",
                    object :DialogInterface.OnClickListener{
                        override fun onClick(dialog: DialogInterface?, which: Int) {

                        }

                    })
                builder.create().show()
            }
            2 ->{
                val map=msg.obj as HashMap<String, String>
                if(Map.gameModel==GameModel.RENJI){
                    tv_black.setText("黑子(电脑):"+map.get("blackCount"))
                }else{
                    tv_black.setText("黑子:"+map.get("blackCount"))
                }
                tv_white.setText("白子:"+map.get("whiteCount"))

                when(Map.whoChess){
                    ChessManType.WHITE ->{
                        tv_white.setBackgroundResource(R.color.btnSelect)
                        tv_black.setBackgroundResource(R.color.btnNotSelect)
                    }
                    else ->{
                        tv_black.setBackgroundResource(R.color.btnSelect)
                        tv_white.setBackgroundResource(R.color.btnNotSelect)
                    }
                }

            }
        }
        return true
    }

    override fun onBackPressed() {
        if(gameView.isExit) {
            super.onBackPressed()
        }else{
            Toast.makeText(this,"再按一次退出",Toast.LENGTH_LONG).show()
            gameView.isExit=true
        }
    }
}

游戏活动:手机横屏,3/5宽度的SurfaceView(显示棋盘),2/5显示用户界面(黑棋,白棋个数,悔棋,重新开始,退出游戏按钮)

GameUI
package com.zcx.liu_zi_qi

import android.annotation.TargetApi
import android.content.Context
import android.content.Intent
import android.graphics.*
import android.os.Build
import android.os.Message
import android.util.Range
import android.view.MotionEvent
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.widget.Toast

class GameUI constructor(context:Context):SurfaceView(context),SurfaceHolder.Callback{
    private lateinit var thread:Thread//绘图线程
    private lateinit var chessBoard:ChessBoard//棋盘实例
    private var isChessable=true//标记是否可下子
    private var whoWin:ChessManType?=null//哪方获胜,null代表没有胜利者
    var isExit=false//标记是否退出游戏
    private lateinit var background:Bitmap//背景图片

    init {
        this.setZOrderOnTop(true)
        //this.setBackgroundResource(R.drawable.background)
        background=BitmapFactory.decodeResource(context.resources,R.drawable.background)
        this.holder.setFormat(PixelFormat.TRANSLUCENT)
        this.holder.addCallback(this)
        Map.list.removeAll(Map.list)
        Map.history.removeAll(Map.history)
        for (i in 0..3){
            val arr= Array<Int>(4,{it -> 0})
            for (j in 0..3){
                if(i==0||(i==1&&(j==0||j==3))){
                    arr[j]=2
                }
                if(i==3||(i==2&&(j==0||j==3))){
                    arr[j]=1
                }
            }
            Map.map[i]=arr
            Toast.makeText(context,"游戏初始化完成", Toast.LENGTH_SHORT).show()
        }
    }
    override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {

    }

    override fun surfaceDestroyed(holder: SurfaceHolder?) {
        thread.join()
    }

    override fun surfaceCreated(holder: SurfaceHolder?) {
        thread= Thread(GameThread(holder))
        thread.start()
    }

    inner class GameThread(val holder: SurfaceHolder?):Runnable{
        override fun run() {
            while(true){
                if(isExit){
                    return
                }
                var canvas= holder?.lockCanvas()
                //canvas?.drawColor(Color.WHITE)
                canvas?.drawBitmap(background, Rect(0,0,background.width,background.height),
                    Rect(0,0,canvas.width,canvas.height),Paint())
                canvas?.let {
                    chessBoard=ChessBoard(it)
                    chessBoard.update()
                }
                holder?.unlockCanvasAndPost(canvas)
                if(whoWin!=null){
                    val msg=Message.obtain()
                    msg.what=1
                    msg.obj=whoWin
                    (context as GameActivity).handle.sendMessage(msg)
                    return
                }
            }

        }

    }

    /**
     * 监听触摸事件
     */
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        val x= event?.getX()
        val y=event?.getY()
        //println("(x,y)=($x,$y)")
        if (x != null&&y!=null) {
            selectChessMan(x,y)
        }
        return true
    }

    /**
     * 下子
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private fun selectChessMan(x:Float,y:Float){
        val ij=chessBoard.xy2ij(Pair(x,y))
        val i=ij?.first
        val j=ij?.second
        println("(i,j)=($i,$j)")
        synchronized(Map){//同步代码块
            isChessable=when(Map.whoChess){
                ChessManType.WHITE -> if (Map.map[i!!][j!!]!=2) true else false
                else -> if (Map.map[i!!][j!!]!=1) true else false
            }
            if(isChessable){
                Map.list.forEach {
                    if(x in Range(it.cx-100,it.cx+100)
                        && y in Range(it.cy-100,it.cy+100)){
                        Map.list.filter { it.isSelect }.forEach {
                            it.isSelect=false
                        }
                        it.isSelect=true
                    }
                }
                moveChessMan(i!!,j!!)
            }

        }

    }

    /**
     * 移动棋子
     */
    private fun moveChessMan(i:Int,j:Int){
        if(Map.map[i][j]!=0){//目标位置有子
            return
        }
        Map.list.filter { it.isSelect }.forEach {
            if(it.move(i,j)){
                //println(Map.printMap())
                it.isSelect=false
                Map.whoChess=when(Map.whoChess){
                    ChessManType.WHITE -> ChessManType.BLACK
                    else -> ChessManType.WHITE
                }
                it.check()
                val msg=Message.obtain()
                msg.what=2
                val map=kotlin.collections.HashMap<String,String>()
                map.put("blackCount",Map.list.filter { it.type==ChessManType.BLACK }.size.toString())
                map.put("whiteCount",Map.list.filter { it.type==ChessManType.WHITE }.size.toString())
                msg.obj=map
                (context as GameActivity).handle.sendMessage(msg)
                checkWin()
            }


        }

    }

    /**
     * 判断输赢
     */
    fun checkWin(){
        if(Map.list.filter { it.type==ChessManType.WHITE }.size==0){
            whoWin=ChessManType.BLACK
        }
        if(Map.list.filter { it.type==ChessManType.BLACK }.size==0){
            whoWin=ChessManType.WHITE
        }
    }

    /**
     * 退出游戏
     */
    fun exit(){
        isExit=true
        thread.join()

    }



}

继承SurfaceView,循环绘制棋盘,棋子.监听触摸事件并做出响应

ChessBoard:
package com.zcx.liu_zi_qi

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF

/**
 *棋盘类
 */
class ChessBoard(val canvas: Canvas) {
    val width = canvas.height - 200//棋盘宽度
    val height = canvas.height - 200//棋盘高度
    val block = width / 3
    val rect: RectF = RectF(100f, 100f, (width + 100).toFloat(), (height + 100).toFloat())

    /**
     *内部棋子类
     * i:横坐标(0,1,2,3)
     * j:纵坐标(0,1,2,3)
     */
    inner class ChessMan(var i:Int=0,var j:Int=0,var type:ChessManType=ChessManType.WHITE):Cloneable{
        var cx=this@ChessBoard.rect.left+j*block//棋子中心x坐标
        var cy=this@ChessBoard.rect.top+i*block//棋子中心y坐标
        var isSelect=false//标记棋子是否被选中
        var callMoveDirection= arrayListOf<ChessManDirection>()//棋子可移动方向

        override fun toString():String{
            return "{i:${this.i},j:${this.j},cx:${this.cx},cy:${this.cy},type:${this.type},isSelect:${this.isSelect}}"
        }

        fun copy(): ChessMan {
            val temp=ChessMan(i,j)
            temp.cx=cx
            temp.cy=cy
            temp.type=type
            temp.isSelect=isSelect
            return temp
        }
        /**
         * 绘制棋子
         */
        fun draw(r:Float=50f){
            val paint=Paint()
            paint.style=Paint.Style.FILL_AND_STROKE
            paint.strokeWidth=4f
            paint.color=when(type){
                ChessManType.WHITE -> Color.WHITE
                else -> Color.BLACK
            }
            canvas.drawCircle(cx,cy,r,paint)

            paint.color=Color.BLACK
            paint.style=Paint.Style.STROKE
            canvas.drawCircle(cx,cy,r,paint)
            if(isSelect){
                paint.color=Color.YELLOW
                paint.strokeWidth=6f
                paint.strokeCap=Paint.Cap.SQUARE//设置笔刷图形样式
                paint.strokeMiter=10f//设置笔画倾斜度
                canvas.drawRect(
                    RectF((cx-60).toFloat(),(cy-60).toFloat(),(cx+60).toFloat(),(cy+60).toFloat()),
                    paint
                )
            }
        }

        /**
         * 移动棋子
         */
        fun move(i:Int,j:Int):Boolean{
            if((Math.abs(this.i-i)<1&&this.i==i)
                ||(Math.abs(this.j-j)<1&&this.j==j)){
                if(Map.history.size>5){//历史记录大于5,清空
                    Map.history.removeAll(Map.history)
                }
                Map.history.add(Pair(Map.copyList(),Map.copyMap()))
                this.cx=this@ChessBoard.rect.left+j*block
                this.cy=this@ChessBoard.rect.top+i*block
                Map.map[this.i][this.j]=0
                this.i=i
                this.j=j
                Map.map[i][j]=when(type){
                    ChessManType.BLACK ->2
                    else -> 1
                }
                return true
                //Map.printMap()
                //Map.printList()
            }
            return false
        }

        /**
         * 检查
         */
        fun check() {
            val tp=when(type){ChessManType.WHITE ->1 else ->2}
            val _tp=when(type){ChessManType.WHITE ->2 else ->1}
            when(i){
                0 ->{
                    if(Map.map[1][j]==tp&&Map.map[2][j]==_tp&&Map.map[3][j]==0){
                        Map.map[2][j]=0
                        Map.list.filter { it.i==2&&it.j==this.j }[0].destroy()
                    }
                }
                1 ->{
                    if(Map.map[0][j]==tp&&Map.map[2][j]==_tp&&Map.map[3][j]==0){
                        Map.map[2][j]=0
                        Map.list.filter { it.i==2&&it.j==this.j }[0].destroy()
                    }
                    if(Map.map[0][j]==0&&Map.map[2][j]==tp&&Map.map[3][j]==_tp){
                        Map.map[3][j]=0
                        Map.list.filter { it.i==3&&it.j==this.j }[0].destroy()
                    }
                }
                2 ->{
                    if(Map.map[0][j]==0&&Map.map[1][j]==_tp&&Map.map[3][j]==tp){
                        Map.map[1][j]=0
                        Map.list.filter { it.i==1&&it.j==this.j }[0].destroy()
                    }
                    if(Map.map[0][j]==_tp&&Map.map[1][j]==tp&&Map.map[3][j]==0){
                        Map.map[0][j]=0
                        Map.list.filter { it.i==0&&it.j==this.j }[0].destroy()
                    }
                }
                3 ->{
                    if(Map.map[0][j]==0&&Map.map[1][j]==_tp&&Map.map[2][j]==tp){
                        Map.map[1][j]=0
                        Map.list.filter { it.i==1&&it.j==this.j }[0].destroy()
                    }
                }
            }
            when(j){
                0 ->{
                    if(Map.map[i][1]==tp&&Map.map[i][2]==_tp&&Map.map[i][3]==0){
                        Map.map[i][2]=0
                        Map.list.filter { it.j==2&&it.i==this.i }[0].destroy()
                    }
                }
                1 ->{
                    if(Map.map[i][0]==tp&&Map.map[i][2]==_tp&&Map.map[i][3]==0){
                        Map.map[i][2]=0
                        Map.list.filter { it.j==2&&it.i==this.i }[0].destroy()
                    }
                    if(Map.map[i][0]==0&&Map.map[i][2]==tp&&Map.map[i][3]==_tp){
                        Map.map[i][3]=0
                        Map.list.filter { it.j==3&&it.i==this.i }[0].destroy()
                    }
                }
                2 ->{
                    if(Map.map[i][0]==0&&Map.map[i][1]==_tp&&Map.map[i][3]==tp){
                        Map.map[i][1]=0
                        Map.list.filter { it.j==1&&it.i==this.i }[0].destroy()
                    }
                    if(Map.map[i][0]==_tp&&Map.map[i][1]==tp&&Map.map[i][3]==0){
                        Map.map[i][0]=0
                        Map.list.filter { it.j==0&&it.i==this.i }[0].destroy()
                    }
                }
                3 ->{
                    if(Map.map[i][0]==0&&Map.map[i][1]==_tp&&Map.map[i][2]==tp){
                        Map.map[i][1]=0
                        Map.list.filter { it.j==1&&it.i==this.i }[0].destroy()
                    }
                }
            }

        }

        /**
         * 销毁
         */
        fun destroy(){
            Map.list.remove(this)

        }
    }

    /**
     *绘制棋盘
     */
    fun drawChessBoard(){
        val paint=Paint()
        paint.color=Color.BLUE
        paint.style=Paint.Style.STROKE
        paint.strokeWidth=8f
        for (i in 0..2){
            canvas.drawRect(
                RectF(rect.left,rect.top+i*block.toFloat(),rect.right,rect.top+(i+1)*block.toFloat()),
                paint
            )
            canvas.drawRect(
                RectF(rect.left+i*block.toFloat(),rect.top,rect.left+(i+1)*block.toFloat(),rect.bottom),
                paint
            )
        }
    }

    /**
     * 更新棋盘
     */
     fun update(){
        this.drawChessBoard()
        synchronized(Map){
            if(Map.list.isEmpty()){
                Map.list.removeAll(Map.list)
                for (i in 0..3){
                    for (j in 0..3){
                        if(Map.map[i][j]==1){
                            Map.list.add(ChessMan(i,j,ChessManType.WHITE))
                        }
                        if(Map.map[i][j]==2){
                            Map.list.add(ChessMan(i,j,ChessManType.BLACK))
                        }
                    }
                }
            }


            Map.list.forEach {
                //println(it.toString())
                it.draw()
            }
        }

    }

    /**
     * 将x,y转换成i,j
     */
    fun xy2ij(pair: Pair<Float,Float>): Pair<Int, Int>? {
        val x=pair.first
        val y=pair.second
        if(x>rect.right+block/2){
            return null
        }
        val j=((x-rect.left+block/2)/block).toInt()
        val i=((y-rect.top+block/2)/block).toInt()
        return Pair(i,j)
    }
}

棋盘类:包含一个棋子内部类,棋盘类实现了绘制棋盘,更新棋盘等功能,棋子类实现了绘制棋子,移动棋子等功能

Map:
package com.zcx.liu_zi_qi

object Map {
    var map= Array<Array<Int>>(4,{it -> Array<Int>(4,{it -> 0})})//4X4数组.0:空,1:白子,2:黑子
    var list = arrayListOf<ChessBoard.ChessMan>()//list集合存储棋子数据
    val gameModel=GameModel.RENJI//游戏模式
    var whoChess=ChessManType.WHITE//轮到哪方下子
    var history= arrayListOf<Pair<ArrayList<ChessBoard.ChessMan>,Array<Array<Int>>>>()//记录历史信息

    fun copyMap(m: Array<Array<Int>> = map):Array<Array<Int>>{
        val temp=Array<Array<Int>>(4,{it -> Array<Int>(4,{it -> 0})})
        for (i in 0..3){
            for (j in 0..3) {
                temp[i][j]=m[i][j]
            }
        }
        return temp
    }

    fun copyList(l: ArrayList<ChessBoard.ChessMan> = list):ArrayList<ChessBoard.ChessMan>{
        val temp=ArrayList<ChessBoard.ChessMan>()
        l.forEach {
            temp.add(it.copy())
        }
        return temp
    }
    /**
     * 打印Map信息
     */
    fun printMap(m: Array<Array<Int>> = map){
        print("[")
        for (i in 0..3){
            print("[")
            for (j in 0..3) {
                if (j == 3) {
                    print("${m[i][j]}")
                } else {
                    print("${m[i][j]},")
                }
            }
            print("]")

        }
        println("]")
    }

    /**
     * 打印List信息
     */
    fun printList(l: ArrayList<ChessBoard.ChessMan> = list){
        print("[")
        l.forEach {
            print(it.toString())
        }
        println("]")
    }
}

一个object类(相当于单例,只有一个实例):1.map属性:4X4数组,记录棋盘信息(0:空,1:白子,2:黑子),2.属性list:一个list集合,存储所有棋子信息(坐标信息,棋子类型,是否被选中)

ChessManType:
package com.zcx.liu_zi_qi

/**
 * 棋子类型
 */
enum class ChessManType {
    WHITE,BLACK
}

棋子类型:一个枚举类,WHITE:表示白子,BLACK:表示黑子

GameModel:
package com.zcx.liu_zi_qi

/**
 * 对战模式
 */
enum class GameModel {
    RENJI,TWO,LANYA
}

游戏模式:一个枚举类,RENJI:人机模式,TWO:双人对战,LANYA:蓝牙对战

8.注意:

所有的ui更改(比如Toast)必须在主线程中进行,可以通过发送Message给主线程处理.
特别注意类的深拷贝和浅拷贝问题(Map类里的map和list有涉及)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值