Android 蓝牙开发(扫描设备、绑定、解绑)Kotlin版

<View

android:background=“#EBEBEB”

android:layout_marginLeft=“54dp”

android:layout_width=“match_parent”

android:layout_height=“1dp”/>

③ 编码


一切准备工作都已经就绪了,下面就进入编码环节,前面的内容其实和Kotlin的关系都不大,下面上正菜,Kotlin相比于Java来说的优势就是简洁,这一点会在下面的编码过程中体现。

1. 通知栏样式修改

首先修改状态栏的文字颜色,如果你现在运行这个项目在手机上时,你会发现状态栏是白色的背景以及白色的文字。如下图所示:

在这里插入图片描述

这样的用户体验是很不好的,而在Android6.0以后支持设置高亮状态栏样式。在之前我写Java版的时候特别弄了一个工具类,里面有针对性状态栏的一些样式和颜色改动,但实际上我只用了其中的一个方法,为了一个方法而去写一个工具类显然多此一举了。所以在Kotlin中我想到了更简单的办法,直接在MainActivity中修改状态栏样式。

代码如下:

//设置亮色状态栏模式 systemUiVisibility在Android11中弃用了,可以尝试一下。

window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or

View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR

放在onCreate方法中,然后运行。

在这里插入图片描述

是不是立竿见影,这个效果一行代码解决问题还不用写工具类,完全调用系统的方法,请注意我是Android10.0版本的手机,也是我自己用的手机。

下面写列表的适配器,因为你扫描蓝牙肯定会是一个列表,既然是一个列表那么肯定要有适配器。

2. 蓝牙设备列表适配器编写

创建一个adapter包,包下创建一个DeviceAdapter.kt文件,如下所示

在这里插入图片描述

DeviceAdapter的代码如下:

package com.llw.bluetooth.adapter

import android.bluetooth.BluetoothClass

import android.bluetooth.BluetoothClass.Device.*

import android.bluetooth.BluetoothClass.Device.Major.*

import android.bluetooth.BluetoothDevice

import android.widget.ImageView

import android.widget.TextView

import com.chad.library.adapter.base.BaseQuickAdapter

import com.chad.library.adapter.base.BaseViewHolder

import com.llw.bluetooth.R

/**

  • 蓝牙设备适配器

*/

class DeviceAdapter(layoutResId: Int, data: MutableList?) :

BaseQuickAdapter<BluetoothDevice, BaseViewHolder>(layoutResId, data) {

override fun convert(helper: BaseViewHolder?, item: BluetoothDevice?) {

val tvName = helper!!.getView(R.id.tv_name)

val icon = helper.getView(R.id.iv_device_type)

//根据设备类型设置图标

getDeviceType(item!!.bluetoothClass.majorDeviceClass, icon)

tvName.text = if (item.name == null) “无名” else item.name

//蓝牙设备绑定状态判断

val tvState = helper!!.getView(R.id.tv_bond_state)

tvState.text = when (item.bondState) {

10 -> “未配对”

11 -> “正在配对…”

12 -> “已配对”

else -> “未配对”

}

//添加item点击事件

helper.addOnClickListener(R.id.item_device)

}

/**

  • 根据类型设置图标

  • @param type 类型码

  • @param icon 图标

*/

private fun getDeviceType(type: Int, icon: ImageView) {

when (type) {

AUDIO_VIDEO_HEADPHONES,//耳机

AUDIO_VIDEO_WEARABLE_HEADSET,//穿戴式耳机

AUDIO_VIDEO_HANDSFREE,//蓝牙耳机

AUDIO_VIDEO //音频设备

-> icon.setImageResource(R.mipmap.icon_headset)

COMPUTER //电脑

-> icon.setImageResource(R.mipmap.icon_computer)

PHONE //手机

-> icon.setImageResource(R.mipmap.icon_phone)

HEALTH //健康类设备

-> icon.setImageResource(R.mipmap.icon_health)

AUDIO_VIDEO_CAMCORDER, //照相机录像机

AUDIO_VIDEO_VCR //录像机

-> icon.setImageResource(R.mipmap.icon_vcr)

AUDIO_VIDEO_CAR_AUDIO //车载设备

-> icon.setImageResource(R.mipmap.icon_car)

AUDIO_VIDEO_LOUDSPEAKER //扬声器

-> icon.setImageResource(R.mipmap.icon_loudspeaker)

AUDIO_VIDEO_MICROPHONE //麦克风

-> icon.setImageResource(R.mipmap.icon_microphone)

AUDIO_VIDEO_PORTABLE_AUDIO //打印机

-> icon.setImageResource(R.mipmap.icon_printer)

AUDIO_VIDEO_SET_TOP_BOX //音频视频机顶盒

-> icon.setImageResource(R.mipmap.icon_top_box)

AUDIO_VIDEO_VIDEO_CONFERENCING //音频视频视频会议

-> icon.setImageResource(R.mipmap.icon_meeting)

AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER //显示器和扬声器

-> icon.setImageResource(R.mipmap.icon_tv)

AUDIO_VIDEO_VIDEO_GAMING_TOY //游戏

-> icon.setImageResource(R.mipmap.icon_game)

AUDIO_VIDEO_VIDEO_MONITOR //可穿戴设备

-> icon.setImageResource(R.mipmap.icon_wearable_devices)

else -> icon.setImageResource(R.mipmap.icon_bluetooth)

}

}

/**

  • 刷新适配器

*/

fun changeBondDevice() {

notifyDataSetChanged()

}

}

代码讲解:

class DeviceAdapter(layoutResId: Int, data: MutableList?) :

BaseQuickAdapter<BluetoothDevice, BaseViewHolder>(layoutResId, data)

首先看这个类,在Kotlin继承和实现都是通过 :(英文下的冒号)来操作的,而Java中继承是extends,实现是implements。在上面的代码中DeviceAdapter继承了BaseQuickAdapter,这一点和Java的相似,如下图所示

在这里插入图片描述

而Kotlin的语法可以让你把构造方法的参数作为类参数使用,这样解释不知道是不是对的,这里传了一个布局id和数据源。

然后重写里面convert方法

override fun convert(helper: BaseViewHolder?, item: BluetoothDevice?) {

val tvName = helper!!.getView(R.id.tv_name)

val icon = helper.getView(R.id.iv_device_type)

getDeviceType(item!!.bluetoothClass.majorDeviceClass, icon)

tvName.text = if (item.name == null) “无名” else item.name

//蓝牙设备绑定状态判断

val tvState = helper!!.getView(R.id.tv_bond_state)

tvState.text = when (item.bondState) {

10 -> “未配对”

11 -> “正在配对…”

12 -> “已配对”

else -> “未配对”

}

//添加item点击事件

helper.addOnClickListener(R.id.item_device)

}

在代码中你会看到 !!? 这个你就不明所以了,因为Java中是没有的,这里解释一下,首先是Kotlin对于空安全做了处理, !! 表示当前对象不会空的情况下执行,而 ? 表示当前对象可以为空。

val tvName = helper!!.getView(R.id.tv_name)

val icon = helper.getView(R.id.iv_device_type)

而这两行代码,可以看到,第一行我给了!!,第二行没有给,这是因为在Kotlin中只要一开始做了处理之后后面就可以不用再次处理,当然你加上!!也没有问题。val 表示不可变量,而通过Kotlin的类型推导机制,tvName此时代表的就是一个通过R.id.tv_name实例化之后的TextView。

//根据设备类型设置图标

getDeviceType(item!!.bluetoothClass.majorDeviceClass, icon)

这行代码调用getDeviceType方法,传入两个参数,这两个参数都已经做了非空的处理,所以在getDeviceType的里面就不用在做空处理了。下面看这个方法的代码:

/**

  • 根据类型设置图标

  • @param type 类型码

  • @param icon 图标

*/

private fun getDeviceType(type: Int, icon: ImageView) {

when (type) {

AUDIO_VIDEO_HEADPHONES,//耳机

AUDIO_VIDEO_WEARABLE_HEADSET,//穿戴式耳机

AUDIO_VIDEO_HANDSFREE,//蓝牙耳机

AUDIO_VIDEO //音频设备

-> icon.setImageResource(R.mipmap.icon_headset)

COMPUTER //电脑

-> icon.setImageResource(R.mipmap.icon_computer)

PHONE //手机

-> icon.setImageResource(R.mipmap.icon_phone)

HEALTH //健康类设备

-> icon.setImageResource(R.mipmap.icon_health)

AUDIO_VIDEO_CAMCORDER, //照相机录像机

AUDIO_VIDEO_VCR //录像机

-> icon.setImageResource(R.mipmap.icon_vcr)

AUDIO_VIDEO_CAR_AUDIO //车载设备

-> icon.setImageResource(R.mipmap.icon_car)

AUDIO_VIDEO_LOUDSPEAKER //扬声器

-> icon.setImageResource(R.mipmap.icon_loudspeaker)

AUDIO_VIDEO_MICROPHONE //麦克风

-> icon.setImageResource(R.mipmap.icon_microphone)

AUDIO_VIDEO_PORTABLE_AUDIO //打印机

-> icon.setImageResource(R.mipmap.icon_printer)

AUDIO_VIDEO_SET_TOP_BOX //音频视频机顶盒

-> icon.setImageResource(R.mipmap.icon_top_box)

AUDIO_VIDEO_VIDEO_CONFERENCING //音频视频视频会议

-> icon.setImageResource(R.mipmap.icon_meeting)

AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER //显示器和扬声器

-> icon.setImageResource(R.mipmap.icon_tv)

AUDIO_VIDEO_VIDEO_GAMING_TOY //游戏

-> icon.setImageResource(R.mipmap.icon_game)

AUDIO_VIDEO_VIDEO_MONITOR //可穿戴设备

-> icon.setImageResource(R.mipmap.icon_wearable_devices)

else -> icon.setImageResource(R.mipmap.icon_bluetooth)

}

}

其实就是条件分支,根据不同的条件做不同的事情,在Java中使用switch/case,而在Kotlin中使用when。when的语法结构更加的简洁明了,通过 -> 代替了 : ,冒号前面是条件,冒号后面是执行业务。而当多个条件对应一个执行业务时,条件之间用英文逗号隔开,一行代码完成一个条件分支,很简洁,但是不要忘了加上else,这是标准写法,你不加也没事,就如同你写switch/case不加default一样。相信这么一解释你已经理解了when的基本用法了,当然还有很多其他的用法由于业务的原因无法展示,自行百度吧。

tvName.text = if (item.name == null) “无名” else item.name

这行代码等同于

if (item.getName() == null) {

helper.setText(R.id.tv_name, “无名”);

} else {

helper.setText(R.id.tv_name, item.getName());

}

这么一看是不是觉得Kotlin的语法很简单,它允许你的返回值一致的判断进行直接赋值,比如这里判断设备名称为空则显示无名二字,不为空则显示设备名,这两个返回都是String类型,而tvName.text设置的就是String类型,所以就有了上面的简洁代码,有点像三目运算符。

//蓝牙设备绑定状态判断

val tvState = helper!!.getView(R.id.tv_bond_state)

tvState.text = when (item.bondState) {

10 -> “未配对”

11 -> “正在配对…”

12 -> “已配对”

else -> “未配对”

}

//添加item点击事件

helper.addOnClickListener(R.id.item_device)

这几行代码也没有什么好讲解的了,都讲过了,这也是when的另一种使用方法,可以直接赋值使用。

/**

  • 刷新适配器

*/

fun changeBondDevice() {

notifyDataSetChanged()

}

最后一个方法就是刷新适配器,当页面的数据有变动是及时刷新。好了这个适配器就讲完了,应该够详细了吧。

3. 权限请求

不管你是用的什么语言来开发Android,你都得遵守Android制定的规则,因此也是要做Android版本大于6.0时动态请求权限。

于是就有了如下这个方法

/**

  • 检查Android版本

*/

private fun checkVersion() {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

//6.0或6.0以上

permissionsRequest() //动态权限申请

} else {

//6.0以下

initBlueTooth() //初始化蓝牙配置

}

}

记得在OnCreate中调用喔!

在这里插入图片描述

然后来看看这个权限请求的方法

/**

  • 动态权限申请

*/

private fun permissionsRequest() {

val rxPermissions = RxPermissions(this)

rxPermissions.request(Manifest.permission.ACCESS_FINE_LOCATION)

.subscribe {

if (it) {

initBlueTooth()

} else {

showMsg(“权限未开启”)

}

}

}

showMsg方法

/**

  • 显示提示消息

*/

private fun showMsg(llw: String) {

Toast.makeText(this, llw, Toast.LENGTH_SHORT).show()

}

这个方法的代码很好理解,和Java的逻辑如出一辙,无非就是不知道这个it是什么意思,it就是它本身的意思,结合方法中的逻辑来看就容易理解了,权限请求自然会有两种结果,同意和不同意,也就是结果是true和false的结果,而这个it就代表这个请求的结果。

4. 初始化蓝牙

首先声明一些成员变量,这里用的是MutableList,表示可变列表,可以有很多方法。

//蓝牙广播接收器

private var bluetoothReceiver: BluetoothReceiver? = null

//蓝牙适配器

private var bluetoothAdapter: BluetoothAdapter? = null

//蓝牙设备适配器

private var mAdapter: DeviceAdapter? = null

//可变列表

private var list: MutableList = mutableListOf()

//请求码

private val REQUEST_ENABLE_BLUETOOTH = 1

BluetoothReceiver报红没有关系不要慌,下面会写的,先看这个初始化蓝牙的方法,比较简单,我想不用讲代码了。

/**

  • 初始化蓝牙

*/

private fun initBlueTooth() {

var intentFilter = IntentFilter()

intentFilter.addAction(BluetoothDevice.ACTION_FOUND) //获得扫描结果

intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED) //绑定状态变化

intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED) //开始扫描

intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) //扫描结束

bluetoothReceiver = BluetoothReceiver() //实例化广播接收器

registerReceiver(bluetoothReceiver, intentFilter) //注册广播接收器

bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() //获取蓝牙适配器

}

5. 扫描蓝牙

在布局中底部我放了一个TextView,点击之后扫描蓝牙,

<TextView

android:id=“@+id/scan_devices”

android:layout_width=“match_parent”

android:layout_height=“50dp”

android:background=“?android:attr/selectableItemBackground”

android:gravity=“center”

android:onClick=“scanBluetooth”

android:text=“扫描蓝牙” />

注意看这一句话

android:onClick=“scanBluetooth”

通过在布局中点击触发MainActivity中的方法,在MainActivity中写入,或者Alt+Enter

在这里插入图片描述

然后你就会看到这样的一个方法

fun scanBluetooth(view: View) {}

首先想清楚这个里面要做什么?难道仅仅只有扫描蓝牙吗?当然不是,首先要看你的设备是否支持蓝牙,其次蓝牙是否打开,最后才是扫描蓝牙

于是里面的代码就可以这样写

/**

  • 扫描蓝牙

*/

fun scanBluetooth(view: View) {

if (bluetoothAdapter != null) { //是否支持蓝牙

if (bluetoothAdapter!!.isEnabled) { //打开

//开始扫描周围的蓝牙设备,如果扫描到蓝牙设备,通过广播接收器发送广播

if (mAdapter != null) { //当适配器不为空时,这时就说明已经有数据了,所以清除列表数据,再进行扫描

list.clear()

mAdapter!!.notifyDataSetChanged()

}

bluetoothAdapter!!.startDiscovery()

} else { //未打开

val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)

startActivityForResult(intent, REQUEST_ENABLE_BLUETOOTH)

}

} else {

showMsg(“你的设备不支持蓝牙”)

}

}

6. 广播接收器

点击扫描蓝牙之后会这行扫描事件,会发送一个广播出去,发送出去了自然要有一个地方来接收,这就是广播接收器,在MainActivity定义一个内部类,通过inner关键字

/**

  • 广播接收器

*/

inner class BluetoothReceiver : BroadcastReceiver() {

override fun onReceive(context: Context?, intent: Intent?) {

when (intent?.action) {

//显示蓝牙设备

BluetoothDevice.ACTION_FOUND -> showDevicesData(context, intent)

//当有蓝牙绑定状态发生改变时,刷新列表数据

BluetoothDevice.ACTION_BOND_STATE_CHANGED -> mAdapter?.changeBondDevice()

//开始扫描

BluetoothAdapter.ACTION_DISCOVERY_STARTED -> loading_lay.visibility = View.VISIBLE

//停止扫描

BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> loading_lay.visibility = View.GONE

else -> showMsg(“未知”)

}

}

}

另外别忘了在页面销毁的时候解注册广播

/**

  • 销毁

*/

override fun onDestroy() {

super.onDestroy()

//卸载广播接收器

unregisterReceiver(bluetoothReceiver)

}

当开始扫描的时候发送ACTION_FOUND,而作为接收方,自然要有相应的处理方法,这个时候注意到showDevicesData(context, intent),通过这个方法显示扫描到的蓝牙设备信息。

7. 显示蓝牙设备信息

/**

  • 显示蓝牙设备信息

  • @param context 上下文参数

  • @param intent 意图

*/

private fun showDevicesData(context: Context?, intent: Intent) {

//获取已绑定的设备

getBondedDevice()

//获取周围蓝牙设备

val device =

intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)

if (list.indexOf(device) == -1) { //防止重复添加

if (device.name != null) { //过滤掉设备名称为null的设备

list.add(device)

}

}

mAdapter = DeviceAdapter(R.layout.item_device_list, list)

rv.layoutManager = LinearLayoutManager(context)

rv.adapter = mAdapter

//item的点击事件

mAdapter!!.setOnItemChildClickListener { adapter, view, position ->

//点击时获取状态,如果已经配对过了就不需要在配对

if (list[position].bondState == BluetoothDevice.BOND_NONE) {

createOrRemoveBond(1, list[position]) //开始匹配

} else {

showDialog(

“确定要取消配对吗?”,

DialogInterface.OnClickListener { dialog, which ->

//取消配对

createOrRemoveBond(2, list[position]) //取消匹配

})

}

}

}

在这里,有一个可以注意的地方

在这里插入图片描述

这句话的意思是,参数未被使用,可以使用_代替。然后来看里面的代码,首先第一句是getBondedDevice()

/**

  • 获取已绑定设备

*/

private fun getBondedDevice() {

val pairedDevices = bluetoothAdapter!!.bondedDevices

if (pairedDevices.size > 0) { //如果获取的结果大于0,则开始逐个解析

for (device in pairedDevices) {

if (list.indexOf(device) == -1) { //防止重复添加

if (device.name != null) { //过滤掉设备名称为null的设备

list.add(device)

}

}

}

}

}

这里用到了in关键字,使用in来检查一个值是否在一个区间内。与他的代码已经有了注释了,就不过多的解释了。

剩下的代码分为两部分,一部分是数据的处理,点击处理。避免重复添加和添加null的设备进入列表,而点击item,根据绑定状态而定,绑定过蓝牙的点击就是取消绑定,这里调用了一个方法。显示一个弹窗

/**

  • 弹窗

  • @param dialogTitle 标题

  • @param onClickListener 按钮的点击事件

*/

private fun showDialog(dialogTitle: String, onClickListener: DialogInterface.OnClickListener) {

val builder =

AlertDialog.Builder(this)

builder.setMessage(dialogTitle)

builder.setPositiveButton(“确定”, onClickListener)

builder.setNegativeButton(“取消”, null)

builder.create().show()

}

而未绑定的点击绑定,调用createOrRemoveBond方法,也可以说是配对,当你取消绑定是也会调用这个方法,只是传递的类型不同而已。

/**

  • 创建或者取消匹配

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

其实要轻松掌握很简单,要点就两个:

  1. 找到一套好的视频资料,紧跟大牛梳理好的知识框架进行学习。
  2. 多练。 (视频优势是互动感强,容易集中注意力)

你不需要是天才,也不需要具备强悍的天赋,只要做到这两点,短期内成功的概率是非常高的。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。下面资料部分截图是我花费几个月时间整理的,诚意满满:特别适合有3-5年开发经验的Android程序员们学习。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

/**

  • 创建或者取消匹配

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-gq9lE1aB-1712534847926)]

[外链图片转存中…(img-65ICfsxX-1712534847926)]

[外链图片转存中…(img-iaWOoW2R-1712534847927)]

[外链图片转存中…(img-3u9YjJN2-1712534847927)]

[外链图片转存中…(img-qaOULj1o-1712534847927)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

其实要轻松掌握很简单,要点就两个:

  1. 找到一套好的视频资料,紧跟大牛梳理好的知识框架进行学习。
  2. 多练。 (视频优势是互动感强,容易集中注意力)

你不需要是天才,也不需要具备强悍的天赋,只要做到这两点,短期内成功的概率是非常高的。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。下面资料部分截图是我花费几个月时间整理的,诚意满满:特别适合有3-5年开发经验的Android程序员们学习。

[外链图片转存中…(img-bXWKznQD-1712534847928)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值