1. 准备工作
如果可以使用自己的移动设备协作完成团队项目或者分享视频、在线播放内容、玩多人游戏(即使未接入互联网也没问题),那岂不是很好吗?您真的可以做到。在此 Codelab 中,您将学习如何做到这一点。
为了简单起见,我们将构建一个无需连接到互联网就能玩的多人石头剪刀布游戏。此 Codelab 将教您如何使用 Nearby Connections API(Google Play 服务的一部分)让用户能够在离得很近时相互通信。用户之间的距离必须在大约 100 米以内。对用户可以分享的数据类型或数据量没有限制,即使未连接到互联网也是如此。用户可以播放流式视频、发送和接收语音消息、发送短信,等等。
前提条件
- 具备 Kotlin 和 Android 开发方面的基础知识
- 了解如何在 Android Studio 中创建和运行应用
- 两台或多台用于运行和测试代码的 Android 设备
- 运行的是 Android API 级别 16 或更高级别
- 已安装 Google Play 服务
- 最新版本的 Android Studio。
学习内容
- 如何将 Google Play 服务 Nearby Connections 库添加到您的应用
- 如何通告您有兴趣与附近的设备通信
- 如何发现附近感兴趣的设备
- 如何与已连接的设备通信
- 隐私设置和数据保护的最佳实践
构建内容
此 Codelab 为您展示了如何构建单个 Activity 应用,让用户能够寻找对手来玩石头剪刀布游戏。该应用具有以下界面元素:
- 一个用于寻找对手的按钮
- 带有三个按钮的游戏控制器,可让用户选择“石头”“布”或“剪刀”来玩游戏
- 用于显示得分的
TextView
- 用于显示状态的
TextView
图 1
2. 创建 Android Studio 项目
- 启动一个新的 Android Studio 项目。
- 选择 Empty Activity。
- 将项目命名为 Rock Paper Scissors,并将语言设置为 Kotlin。
3. 设置代码
- 将最新版本的 Nearby 依赖项添加到您的应用级
build.gradle
文件中。这样可让您的应用使用 Nearby Connections API 通告您有兴趣连接、发现附近的设备以及通信。
implementation 'com.google.android.gms:play-services-nearby:LATEST_VERSION'
- 在
android
代码块中将 viewBinding build 选项设置为true
以启用视图绑定,这样您就不必使用findViewById
与视图交互了。
android {
...
buildFeatures {
viewBinding true
}
}
- 点击 Sync Now 或绿色锤子按钮,以便 Android Studio 将这些 Gradle 更改考虑在内。
- 我们对石头、布和剪刀图片使用矢量可绘制对象。将以下三个 XML 文件添加到您的
res/drawable
目录。
res/drawables/rock.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tintMode="multiply"
android:viewportHeight="48.0"
android:viewportWidth="48.0"
android:width="24dp">
<path
android:fillColor="#ffffff"
android:pathData="M28,12l-7.5,10 5.7,7.6L23,32c-3.38,-4.5 -9,-12 -9,-12L2,36h44L28,12z"/>
</vector>
res/drawables/paper.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tintMode="multiply"
android:viewportHeight="48.0"
android:viewportWidth="48.0"
android:width="24dp">
<path
android:fillColor="#ffffff"
android:pathData="M28,4L12,4C9.79,4 8.02,5.79 8.02,8L8,40c0,2.21 1.77,4 3.98,4L36,44c2.21,0 4,-1.79 4,-4L40,16L28,4zM26,18L26,7l11,11L26,18z"/>
</vector>
res/drawables/scissors.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tintMode="multiply"
android:viewportWidth="48.0"
android:viewportHeight="48.0">
<path
android:fillColor="#ffffff"
android:pathData="M19.28,15.28c0.45,-1 0.72,-2.11 0.72,-3.28 0,-4.42 -3.58,-8 -8,-8s-8,3.58 -8,8 3.58,8 8,8c1.17,0 2.28,-0.27 3.28,-0.72L20,24l-4.72,4.72c-1,-0.45 -2.11,-0.72 -3.28,-0.72 -4.42,0 -8,3.58 -8,8s3.58,8 8,8 8,-3.58 8,-8c0,-1.17 -0.27,-2.28 -0.72,-3.28L24,28l14,14h6v-2L19.28,15.28zM12,16c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4zM12,40c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4zM24,25c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM38,6L26,18l4,4L44,8L44,6z" />
</vector>
- 为游戏屏幕添加游戏控制器(换句话说,玩游戏的按钮)、得分和状态
TextView
。在activity_main.xml
文件中,将代码替换为以下内容:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="searching..."
app:layout_constraintBottom_toTopOf="@+id/myName"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
<TextView
android:id="@+id/myName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="You (codeName)"
android:textAlignment="center"
android:textAppearance="?android:textAppearanceMedium"
app:layout_constraintEnd_toStartOf="@+id/opponentName"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/status"
/>
<TextView
android:id="@+id/opponentName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Opponent (codeName)"
android:textAlignment="center"
android:textAppearance="?android:textAppearanceMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/myName"
app:layout_constraintTop_toBottomOf="@+id/status"
/>
<TextView
android:id="@+id/score"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="16dp"
android:text=":"
android:textAlignment="center"
android:textSize="80sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/myName"
/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/rock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/rock"
android:text="Rock"
app:layout_constraintEnd_toStartOf="@+id/paper"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/score"
/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/paper"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/paper"
android:text="Paper"
app:layout_constraintEnd_toStartOf="@+id/scissors"
app:layout_constraintStart_toEndOf="@+id/rock"
app:layout_constraintTop_toBottomOf="@+id/score"
/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/scissors"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/scissors"
android:text="Scissors"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/paper"
app:layout_constraintTop_toBottomOf="@+id/score"
/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/disconnect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="32dp"
android:text="disconnect"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/paper"
/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/findOpponent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="32dp"
android:text="find opponent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/paper"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
您的布局现在应如上面的图 1 所示。
注意:在您自己的项目中,应将值(如 16dp
)更改为资源(如 @dimen/activity_vertical_margin
)。
4. 将 Nearby Connections 添加到您的应用
准备 manifest.xml 文件
将以下权限添加到清单文件中。由于 ACCESS_FINE_LOCATION
是一项危险权限,因此您的应用将包含相关的代码,这些代码将触发系统提示用户代表您的应用准许或拒绝访问。Wi-Fi 权限适用于点对点连接,而不适用于互联网连接。
<!-- Required for Nearby Connections →
<!-- Because ACCESS_FINE_LOCATION is a dangerous permission, the app will have to-->
<!-- request it at runtime, and the user will be prompted to grant or deny access.-->
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
选择 Strategy
Nearby Connections API 要求您选择一种 Strategy
,用来确定您的应用如何与其他附近的设备连接。选择 P2P_CLUSTER
、P2P_STAR
或 P2P_POINT_TO_POINT
。
出于我们的目的,我们将选择 P2P_STAR
,因为我们希望能够看到来自玩家的许多传入请求,这些玩家想要挑战我们,但一次只能与另外一个人对战。
您选择的 Strategy
必须同时用于应用中的通告和发现。下图显示了每种 Strategy
的运作方式。
设备可以请求 N 个传出连接 | 设备可以接收 M 个传入连接 | |||
| N = 多个 | M = 多个 | 产生较低带宽的连接 | |
| N = 1 个 | M = 多个 | 产生较高带宽的连接 | |
| N = 1 个 | M = 1 个 | 尽可能高的吞吐量 |
在 MainActivity 中定义变量
- 在主 activity (
MainActivity.kt
) 内的onCreate()
函数上方,通过粘贴此代码段来定义以下变量。这些变量定义了游戏专用逻辑和运行时权限。
/**
* Enum class for defining the winning rules for Rock-Paper-Scissors. Each player will make a
* choice, then the beats function in this class will be used to determine whom to reward the
* point to.
*/
private enum class GameChoice {
ROCK, PAPER, SCISSORS;
fun beats(other: GameChoice): Boolean =
(this == ROCK && other == SCISSORS)
|| (this == SCISSORS && other == PAPER)
|| (this == PAPER && other == ROCK)
}
/**
* Instead of having each player enter a name, in this sample we will conveniently generate
* random human readable names for players.
*/
internal object CodenameGenerator {
private val COLORS = arrayOf(
"Red", "Orange", "Yellow", "Green", "Blue", "Indigo", "Violet", "Purple", "Lavender"
)
private val TREATS = arrayOf(
"Cupcake", "Donut", "Eclair", "Froyo", "Gingerbread", "Honeycomb",
"Ice Cream Sandwich", "Jellybean", "Kit Kat", "Lollipop", "Marshmallow", "Nougat",
"Oreo", "Pie"
)
private val generator = Random()
/** Generate a random Android agent codename */
fun generate(): String {
val color = COLORS[generator.nextInt(COLORS.size)]
val treat = TREATS[generator.nextInt(TREATS.size)]
return "$color $treat"
}
}
/**
* Strategy for telling the Nearby Connections API how we want to discover and connect to
* other nearby devices. A star shaped strategy means we want to discover multiple devices but
* only connect to and communicate with one at a time.
*/
private val STRATEGY = Strategy.P2P_STAR
/**
* Our handle to the [Nearby Connections API][ConnectionsClient].
*/
private lateinit var connectionsClient: ConnectionsClient
/**
* The request code for verifying our call to [requestPermissions]. Recall that calling
* [requestPermissions] leads to a callback to [onRequestPermissionsResult]
*/
private val REQUEST_CODE_REQUIRED_PERMISSIONS = 1
/*
The following variables are convenient ways of tracking the data of the opponent that we
choose to play against.
*/
private var opponentName: String? = null
private var opponentEndpointId: String? = null
private var opponentScore = 0
private var opponentChoice: GameChoice? = null
/*
The following variables are for tracking our own data
*/
private var myCodeName: String = CodenameGenerator.generate()
private var myScore = 0
private var myChoice: GameChoice? = null
/**
* This is for wiring and interacting with the UI views.
*/
private lateinit var binding: ActivityMainBinding
- 更改您的
onCreate()
函数,以将ViewBinding
对象传入setContentView()
。这会显示activity_main.xml
布局文件的内容。此外,初始化connectionsClient
,以便您的应用可以与 API 通信。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
connectionsClient = Nearby.getConnectionsClient(this)
}
验证必需的权限
一般来讲,在 AndroidManifest.xml
文件中声明危险权限,但必须在运行时请求这些权限。对于其他必要的权限,您仍应在运行时对其进行验证,以确保结果符合您的预期。如果用户拒绝其中任一权限,应显示一个消息框,通知他们不授予这些权限就无法继续操作,因为我们的示例应用没有这些权限就无法使用。
- 在
onCreate()
函数下方,添加以下代码段,以验证我们是否具有这些权限:
@CallSuper
override fun onStart() {
super.onStart()
if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
REQUEST_CODE_REQUIRED_PERMISSIONS
)
}
}
@CallSuper
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
val errMsg = "Cannot start without required permissions"
if (requestCode == REQUEST_CODE_REQUIRED_PERMISSIONS) {
grantResults.forEach {
if (it == PackageManager.PERMISSION_DENIED) {
Toast.makeText(this, errMsg, Toast.LENGTH_LONG).show()
finish()
return
}
}
recreate()
}
}
此时,我们已经编写了用来完成以下任务的代码:
- 创建了布局文件
- 在清单中声明了必要的权限
- 在运行时验证了必需的危险权限
反向演示
现在我们已经完成了准备工作,接下来就可以开始编写 Nearby Connections 代码来与附近的用户连接和通信了。通常,您的应用必须先允许其他设备找到它并且它必须先扫描其他设备,然后您才能真正开始与附近的设备通信。
换句话说,在我们的石头剪刀布游戏的上下文中,您和您的对手必须先找到对方,然后才能开始玩游戏。
您可以通过一个称为通告的过程使您的设备可被检测到。同样,您可以通过一个称为发现的过程发现附近的对手。
为了理解该过程,最好按相反的顺序处理代码。为此,我们将按以下步骤操作:
- 我们将假装已经连接,首先编写用于发送和接收消息的代码。出于我们目前的目的,这意味着,编写用于真正玩石头剪刀布游戏的代码。
- 我们将编写用于通告我们有兴趣与附近的设备连接的代码。
- 我们将编写用于发现附近的设备的代码。
发送和接收数据
您使用 connectionsClient.sendPayload()
方法以 Payload
的形式发送数据,并使用 PayloadCallback
对象接收载荷。Payload
可以是任意内容:视频、照片、信息流,或其他任何类型的数据。而且,没有数据量限制。
- 在我们的游戏中,载荷是指用户选择的石头、布或剪刀。当用户点击某个控制器按钮时,应用会将他们的选择以载荷的形式发送到对手的应用。为了记录用户的选择,请在
onRequestPermissionsResult()
函数下方添加以下代码段。
/** Sends the user's selection of rock, paper, or scissors to the opponent. */
private fun sendGameChoice(choice: GameChoice) {
myChoice = choice
connectionsClient.sendPayload(
opponentEndpointId!!,
Payload.fromBytes(choice.name.toByteArray(UTF_8))
)
binding.status.text = "You chose ${choice.name}"
// For fair play, we will disable the game controller so that users don't change their
// choice in the middle of a game.
setGameControllerEnabled(false)
}
/**
* Enables/Disables the rock, paper and scissors buttons. Disabling the game controller
* prevents users from changing their minds after making a choice.
*/
private fun setGameControllerEnabled(state: Boolean) {
binding.apply {
rock.isEnabled = state
paper.isEnabled = state
scissors.isEnabled = state
}
}
- 设备通过
PayloadCallback
对象接收载荷,该对象有两个方法。onPayloadReceived()
方法可告知您的应用何时在接收消息,onPayloadTransferUpdate()
方法可跟踪收到的消息和外发的消息的状态。
出于我们的目的,我们将读取从 onPayloadReceived()
收到的消息作为对手的选择,并使用 onPayloadTransferUpdate()
方法跟踪和确认两个玩家何时做出了选择。在 onCreate()
方法上方添加以下代码段。
/** callback for receiving payloads */
private val payloadCallback: PayloadCallback = object : PayloadCallback() {
override fun onPayloadReceived(endpointId: String, payload: Payload) {
payload.asBytes()?.let {
opponentChoice = GameChoice.valueOf(String(it, UTF_8))
}
}
override fun onPayloadTransferUpdate(endpointId: String, update: PayloadTransferUpdate) {
// Determines the winner and updates game state/UI after both players have chosen.
// Feel free to refactor and extract this code into a different method
if (update.status == PayloadTransferUpdate.Status.SUCCESS
&& myChoice != null && opponentChoice != null) {
val mc = myChoice!!
val oc = opponentChoice!!
when {
mc.beats(oc) -> { // Win!
binding.status.text = "${mc.name} beats ${oc.name}"
myScore++
}
mc == oc -> { // Tie
binding.status.text = "You both chose ${mc.name}"
}
else -> { // Loss
binding.status.text = "${mc.name} loses to ${oc.name}"
opponentScore++
}
}
binding.score.text = "$myScore : $opponentScore"
myChoice = null
opponentChoice = null
setGameControllerEnabled(true)
}
}
}
通告
您通告您在附近或有兴趣,希望附近有人会注意到您并请求与您连接。因此,Nearby Connections API 的 startAdvertising()
方法需要一个回调对象。当注意到了您的通告的某个人想要与您连接时,该回调(即 ConnectionLifecycleCallback
)会通知您。该回调对象有三个方法:
onConnectionInitiated()
方法可告知您有人注意到了您的通告并且想要与您连接。因此,您可以选择使用connectionsClient.acceptConnection()
接受连接。- 当有人注意到您的通告时,他们会向您发送连接请求。您和发送者必须都接受连接请求才能真正连接。
onConnectionResult()
方法可告知您是否建立了连接。 onDisconnected()
函数可告知您连接不再处于活跃状态。例如,如果您或对手决定终止连接,就会发生这种情况。
如需通告,请执行以下操作:
- 对于我们的应用,获得
onConnectionInitiated()
回调时,我们将接受连接。然后,在onConnectionResult()
内,如果建立了连接,我们将停止通告和发现,因为我们只需要与一个对手连接来玩游戏。最后,在onConnectionResult()
中,我们将重置游戏。
在 onCreate()
方法前面粘贴以下代码段。
// Callbacks for connections to other devices
private val connectionLifecycleCallback = object : ConnectionLifecycleCallback() {
override fun onConnectionInitiated(endpointId: String, info: ConnectionInfo) {
// Accepting a connection means you want to receive messages. Hence, the API expects
// that you attach a PayloadCall to the acceptance
connectionsClient.acceptConnection(endpointId, payloadCallback)
opponentName = "Opponent\n(${info.endpointName})"
}
override fun onConnectionResult(endpointId: String, result: ConnectionResolution) {
if (result.status.isSuccess) {
connectionsClient.stopAdvertising()
connectionsClient.stopDiscovery()
opponentEndpointId = endpointId
binding.opponentName.text = opponentName
binding.status.text = "Connected"
setGameControllerEnabled(true) // we can start playing
}
}
override fun onDisconnected(endpointId: String) {
resetGame()
}
}
- 由于
resetGame()
在不同的时刻调用起来非常方便,因此我们使其成为它自己的子例程。在MainActivity
类的底部添加以下代码。
/** Wipes all game state and updates the UI accordingly. */
private fun resetGame() {
// reset data
opponentEndpointId = null
opponentName = null
opponentChoice = null
opponentScore = 0
myChoice = null
myScore = 0
// reset state of views
binding.disconnect.visibility = View.GONE
binding.findOpponent.visibility = View.VISIBLE
setGameControllerEnabled(false)
binding.opponentName.text="opponent\n(none yet)"
binding.status.text ="..."
binding.score.text = ":"
}
- 以下代码段是实际的通告调用,您在该调用中告知 Nearby Connections API 您想要进入通告模式。在
onCreate()
方法下方添加该代码段。
private fun startAdvertising() {
val options = AdvertisingOptions.Builder().setStrategy(STRATEGY).build()
// Note: Advertising may fail. To keep this demo simple, we don't handle failures.
connectionsClient.startAdvertising(
myCodeName,
packageName,
connectionLifecycleCallback,
options
)
}
发现
通告的补充是发现。这两个调用非常相似,只不过它们使用不同的回调。startDiscovery()
调用的回调是一个 EndpointDiscoveryCallback
对象。此对象有两个回调方法:每次检测到通告时都会调用 onEndpointFound()
;每次通告不再可用时都会调用 onEndpointLost()
。
- 我们的应用将与我们检测到的第一个通告者连接。这意味着,我们将在
onEndpointFound()
方法内发出连接请求,而不使用onEndpointLost()
方法执行任何操作。在onCreate()
方法前面添加以下回调。
// Callbacks for finding other devices
private val endpointDiscoveryCallback = object : EndpointDiscoveryCallback() {
override fun onEndpointFound(endpointId: String, info: DiscoveredEndpointInfo) {
connectionsClient.requestConnection(myCodeName, endpointId, connectionLifecycleCallback)
}
override fun onEndpointLost(endpointId: String) {
}
}
- 此外,还应添加用于实际告知 Nearby Connections API 您想要进入发现模式的代码段。在
MainActivity
类的底部添加该代码段。
private fun startDiscovery(){
val options = DiscoveryOptions.Builder().setStrategy(STRATEGY).build()
connectionsClient.startDiscovery(packageName,endpointDiscoveryCallback,options)
}
- 此时,我们工作中的 Nearby Connections 部分已经完成了!我们可以通告、发现附近的设备以及与之通信。但是,我们还不太能玩游戏。我们必须完成界面视图的连接:
- 当用户点击寻找对手按钮时,应用应同时调用
startAdvertising()
和startDiscovery()
。这样,您既会发现,又会被发现。 - 当用户点击某个控制器按钮(即石头、布或剪刀)时,应用需要调用
sendGameChoice()
以将该数据传输给对手。 - 当任一用户点击断开连接按钮时,应用应重置游戏。
更新 onCreate()
方法以反映这些互动。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
connectionsClient = Nearby.getConnectionsClient(this)
binding.myName.text = "You\n($myCodeName)"
binding.findOpponent.setOnClickListener {
startAdvertising()
startDiscovery()
binding.status.text = "Searching for opponents..."
// "find opponents" is the opposite of "disconnect" so they don't both need to be
// visible at the same time
binding.findOpponent.visibility = View.GONE
binding.disconnect.visibility = View.VISIBLE
}
// wire the controller buttons
binding.apply {
rock.setOnClickListener { sendGameChoice(GameChoice.ROCK) }
paper.setOnClickListener { sendGameChoice(GameChoice.PAPER) }
scissors.setOnClickListener { sendGameChoice(GameChoice.SCISSORS) }
}
binding.disconnect.setOnClickListener {
opponentEndpointId?.let { connectionsClient.disconnectFromEndpoint(it) }
resetGame()
}
resetGame() // we are about to start a new game
}
清理
当不再需要 Nearby API 时,您应停止使用它。对于我们的示例游戏,我们在 onStop()
activity 生命周期函数内释放所有资源。
@CallSuper
override fun onStop(){
connectionsClient.apply {
stopAdvertising()
stopDiscovery()
stopAllEndpoints()
}
resetGame()
super.onStop()
}
5. 运行应用
在两台设备上运行应用,尽情畅玩吧!
6. 隐私设置最佳实践
我们的石头剪刀布游戏不会分享任何敏感数据。甚至连代号都是随机生成的。这就是我们在 onConnectionInitiated(String, ConnectionInfo)
内自动接受连接的原因。
ConnectionInfo
对象包含每个连接的唯一令牌,您的应用可通过 getAuthenticationDigits()
访问该令牌。您可以向两个用户显示令牌以进行视觉验证。作为替代方案,您也可以先在一台设备上加密原始令牌,并将其以载荷的形式发送到另一台设备上进行解密,然后再开始分享敏感数据。如需详细了解 Android 加密,请查看这篇名为“改进应用的加密 - 从消息身份验证到用户存在”的博文。
7. 恭喜
恭喜!现在,您已经知道了如何在未连接到互联网的情况下通过 Nearby Connections API 连接用户。
总之,如需使用 Nearby Connections API,您需要添加对 play-services-nearby
的依赖关系。此外,您还需要在 AndroidManifest.xml
文件中请求权限,并在运行时检查这些权限。您还学习了如何执行以下操作:
- 通告您有兴趣与附近的用户连接
- 发现希望连接的附近用户
- 接受连接
- 发送消息
- 接收消息
- 保护用户隐私
后续操作
查看我们的博客系列和示例