添加相关权限
AndroidManifest.xml
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<!-- 对于 Android 6.0+ 需要位置权限来扫描蓝牙设备 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- 对于 Android 12+ 需要添加以下权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
MainActivity.java
package com.example.test;
import android.Manifest;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
//请求码,可以随便设置,但是在软件中必须唯一
private static final int REQUEST_ENABLE_BT = 1;
private static final int PERMISSION_REQUEST_FINE_LOCATION = 101;
private static final long SCAN_PERIOD = 10000; // 10 seconds
private BluetoothAdapter mBluetoothAdapter;//蓝牙适配器
private LeDeviceListAdapter mLeDeviceListAdapter;//列表适配器
private ListView listView;
private Button scanButton;
private boolean mScanning;
private Handler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandler = new Handler();//初始化Handler
// 检查设备是否支持蓝牙
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, "蓝牙低功耗不支持", Toast.LENGTH_SHORT).show();
finish();
}
// 初始化蓝牙适配器(通过BluetoothManager去获取BluetoothAdapter的实例)
final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();
// 检查设备是否支持蓝牙
if (mBluetoothAdapter == null) {
Toast.makeText(this, "蓝牙不可用", Toast.LENGTH_SHORT).show();
finish();
return;
}
// 初始化视图组件
listView = findViewById(R.id.list_view);
scanButton = findViewById(R.id.scan_button);
scanButton.setOnClickListener(new View.OnClickListener() {//扫描按钮的点击事件
@Override
public void onClick(View v) {
if (mScanning) {
stopScan();
} else {
startScan();
}
}
});
// 初始化列表适配器
mLeDeviceListAdapter = new LeDeviceListAdapter();
listView.setAdapter(mLeDeviceListAdapter);//给列表设置适配器
// 设置列表项点击事件
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final BluetoothDevice device = mLeDeviceListAdapter.getDevice(position);
if (device == null) return;
// 这里可以实现连接蓝牙设备的逻辑
if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return;
}
Toast.makeText(MainActivity.this,
"设备名称: " + device.getName() + "\n设备地址: " + device.getAddress(),
Toast.LENGTH_SHORT).show();
}
});
// 注册蓝牙状态变化的广播接收器
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(mReceiver, filter);
}
@Override
protected void onResume() {
super.onResume();
// 确保蓝牙已开启
if (!mBluetoothAdapter.isEnabled()) {
//蓝牙不可用则请求用户开启蓝牙
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
// 检查定位权限(Android 6.0及以上需要)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {//检查
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
PERMISSION_REQUEST_FINE_LOCATION);//请求开启定位权限
} else {
startScan();
}
} else {
startScan();
}
}
@Override
protected void onPause() {
super.onPause();
stopScan();
mLeDeviceListAdapter.clear();
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(mReceiver);//注销广播接收器
}
//开启定位的系统回调方法
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case PERMISSION_REQUEST_FINE_LOCATION:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startScan();
} else {
Toast.makeText(this, "需要定位权限来扫描蓝牙设备", Toast.LENGTH_SHORT).show();
}
break;
}
}
//跳转开启蓝牙界面的系统回调
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_ENABLE_BT && resultCode == RESULT_CANCELED) {
finish();
return;
}
}
private void startScan() {
mLeDeviceListAdapter.clear();
mScanning = true;
scanButton.setText("停止扫描");
// 如果是Android 5.0及以上版本,使用新的蓝牙扫描API
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
return;
}
mBluetoothAdapter.getBluetoothLeScanner().startScan(mLeScanCallback);
} else {
// Android 5.0以下版本使用旧的蓝牙扫描API
mBluetoothAdapter.startLeScan(mLeScanCallbackCompat);
}
// 停止扫描的计时器
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
stopScan();
}
}, SCAN_PERIOD);
}
private void stopScan() {
mScanning = false;
scanButton.setText("开始扫描");
// 如果是Android 5.0及以上版本,使用新的蓝牙扫描API
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
return;
}
mBluetoothAdapter.getBluetoothLeScanner().stopScan(mLeScanCallback);
} else {
// Android 5.0以下版本使用旧的蓝牙扫描API
mBluetoothAdapter.stopLeScan(mLeScanCallbackCompat);
}
}
// 蓝牙状态变化的广播接收器
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
BluetoothAdapter.ERROR);
switch (state) {
case BluetoothAdapter.STATE_OFF:
break;
case BluetoothAdapter.STATE_TURNING_OFF:
break;
case BluetoothAdapter.STATE_ON:
break;
case BluetoothAdapter.STATE_TURNING_ON:
break;
}
}
}
};
// 新的蓝牙扫描回调(Android 5.0及以上)
private android.bluetooth.le.ScanCallback mLeScanCallback =
new android.bluetooth.le.ScanCallback() {
@Override
public void onScanResult(int callbackType, android.bluetooth.le.ScanResult result) {
super.onScanResult(callbackType, result);
runOnUiThread(new Runnable() {
@Override
public void run() {
mLeDeviceListAdapter.addDevice(result.getDevice(), result.getRssi());
mLeDeviceListAdapter.notifyDataSetChanged();
}
});
}
};
// 旧的蓝牙扫描回调(Android 5.0以下)
private BluetoothAdapter.LeScanCallback mLeScanCallbackCompat =
new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, final int rssi, byte[] scanRecord) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mLeDeviceListAdapter.addDevice(device, rssi);
mLeDeviceListAdapter.notifyDataSetChanged();
}
});
}
};
// 蓝牙设备列表适配器
private class LeDeviceListAdapter extends BaseAdapter {
private ArrayList<BluetoothDevice> mLeDevices;// 存储蓝牙设备对象
//BluetoothDevice:Android 蓝牙设备类,包含设备名称、地址等信息。
private ArrayList<Integer> mRssiValues; // 存储对应设备的信号强度(RSSI)
//RSSI:接收信号强度指示(Received Signal Strength Indication),单位为 dBm,数值越小表示信号越弱。
private LayoutInflater mInflator;// 用于加载布局文件
//初始化
public LeDeviceListAdapter() {
super();
mLeDevices = new ArrayList<>();
mRssiValues = new ArrayList<>();
mInflator = MainActivity.this.getLayoutInflater();
}
//添加设备
public void addDevice(BluetoothDevice device, int rssi) {
if (!mLeDevices.contains(device)) {//两个列表同步添加数据,保证索引一致
mLeDevices.add(device);// 添加设备到列表末尾
mRssiValues.add(rssi);// 将 RSSI 添加到相同索引位置
} else {
// 设备已存在则更新RSSI值
//1. ArrayList.indexOf(Object o) 方法:这是 Java 集合框架中 ArrayList 类的一个方法,用于查找某个对象在列表中的位置。
int index = mLeDevices.indexOf(device);
mRssiValues.set(index, rssi);
}
}
public BluetoothDevice getDevice(int position) {
return mLeDevices.get(position);
}
public void clear() {
mLeDevices.clear();
mRssiValues.clear();
}
@Override
public int getCount() {
return mLeDevices.size();
}
@Override
public Object getItem(int i) {
return mLeDevices.get(i);
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
ViewHolder viewHolder;
// ViewHolder:通过 setTag() 和 getTag() 复用视图组件,避免重复调用 findViewById(),提高性能。
// 一般的视图优化
if (view == null) {
// 1. 首次创建 View
view = mInflator.inflate(R.layout.listitem_device, null);
// 2. 创建 ViewHolder 并缓存控件引用
viewHolder = new ViewHolder();
viewHolder.deviceAddress = view.findViewById(R.id.device_address);
viewHolder.deviceName = view.findViewById(R.id.device_name);
viewHolder.deviceRssi = view.findViewById(R.id.device_rssi);
// 3. 将 ViewHolder 绑定到 View
view.setTag(viewHolder);
} else {
// 4. 复用 View 时,直接从 Tag 中获取 ViewHolder
viewHolder = (ViewHolder) view.getTag();
}
BluetoothDevice device = mLeDevices.get(i);
if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
}
// 5. 使用缓存的控件引用,无需再次 findViewById()
final String deviceName = device.getName();
if (deviceName != null && deviceName.length() > 0) {
viewHolder.deviceName.setText(deviceName);// 显示真实设备名称
}else {
viewHolder.deviceName.setText("未知设备");// 名称为空时显示默认文本
}
viewHolder.deviceAddress.setText(device.getAddress());
// 显示信号强度
int rssi = mRssiValues.get(i);
viewHolder.deviceRssi.setText("信号强度: " + rssi + " dBm");
return view;
}
}
//创建一个静态内部类 ViewHolder,用于存储 View 中的所有控件引用。
//当第一次创建 View 时,将控件引用存入 ViewHolder,并通过 setTag() 将 ViewHolder 绑定到 View 上。
//当复用 View 时,通过 getTag() 直接获取已缓存的 ViewHolder,无需重新查找控件。
static class ViewHolder {
TextView deviceName;
TextView deviceAddress;
TextView deviceRssi;
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<Button
android:id="@+id/scan_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="开始扫描"
android:textSize="18sp"
android:background="#ffff00"
android:textColor="#FFFFFF"
android:layout_marginBottom="16dp"/>
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
listitem_device.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/device_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/device_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="#666666"/>
<TextView
android:id="@+id/device_rssi"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="#666666"
android:layout_marginTop="4dp"/>
</LinearLayout>