Android退出APP拦截

import android.app.AlertDialog import android.os.Bundle import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import androidx.activity.viewModels import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber class MainActivity : AppCompatActivity() { private val viewModel: MainViewModel by viewModels() private val TAG = "MainActivity" private val backCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { handleAppExit() } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) onBackPressedDispatcher.addCallback(this, backCallback) } private fun handleAppExit() { backCallback.isEnabled = false lifecycleScope.launch(Dispatchers.IO) { try { //判断是否需要提示 val isBeConfirm = true if (isBeConfirm) { withContext(Dispatchers.Main) { showExitConfirmationDialog() } } else { withContext(Dispatchers.Main) { finish() } } } catch (e: Exception) { Timber.tag(TAG).e(e) if (e !is CancellationException) { withContext(Dispatchers.Main) { finish() } } } finally { withContext(Dispatchers.Main) { backCallback.isEnabled = true } } } } private fun showExitConfirmationDialog() { AlertDialog.Builder(this) .setTitle("还不能退出") .setMessage("退出后,会如何?") .setPositiveButton("继续退出") { dialog, which -> finish() } .setNegativeButton("取消") { dialog, which -> dialog.dismiss() } .setCancelable(false) .show() } }

2025年11月28日

Android 蓝牙连接

Android 蓝牙连接 RxAndroidBle连接库只能扫描出Ble类型设备 使用原生Android扫描出设备获取Mac地址,然后用RxAndroidBle连接 fun scanBleDeviceNative(deviceName: String) { val adapter = (context.getSystemService(Context.BLUETOOTH_SERVICE) as android.bluetooth.BluetoothManager).adapter if (adapter == null || !adapter.isEnabled) { Timber.tag(TAG).w("蓝牙未开启") return } // 如果之前正在扫描,先取消 if (adapter.isDiscovering) adapter.cancelDiscovery() // 监听经典蓝牙发现广播 val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { BluetoothDevice.ACTION_FOUND -> { val device: BluetoothDevice? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra( BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java ) } else { @Suppress("DEPRECATION") intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE) } device?.let { val name = it.name ?: return if (name.contains(deviceName, ignoreCase = true)) { Timber.tag(TAG) .d("找到设备: $name ${device.address}") // 将经典 BluetoothDevice 转为 RxBleDevice 对象 val bleDevice = rxBleClient.getBleDevice(device.address) // 连接 到设备.... // 停止扫描 try { adapter.cancelDiscovery() } catch (_: Exception) { } context.unregisterReceiver(this) } } } BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> { Timber.tag(TAG).d("扫描结束") context.unregisterReceiver(this) } } } } val filter = IntentFilter(BluetoothDevice.ACTION_FOUND).apply { addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) } context.registerReceiver(receiver, filter) // 启动蓝牙扫描 adapter.startDiscovery() Timber.tag(TAG).d("开始扫描") // 设置 10 秒超时停止扫描 Handler(Looper.getMainLooper()).postDelayed({ try { if (adapter.isDiscovering) adapter.cancelDiscovery() Timber.tag(TAG).d("扫描超时,已停止") context.unregisterReceiver(receiver) } catch (_: Exception) { } }, 10_000) }

2025年11月07日

Android include布局文件传递viewModel

📚 Data Binding: <include> 传递 ViewModel 笔记 核心原理 在 Android Data Binding 中,ViewModel 的传递是通过 <include> 标签上的 bind: 属性实现的一种显式赋值机制。 父布局中声明的变量不会自动传递给子布局。 要传递,必须在 <include> 标签中指明:bind:【子布局变量名】="@{【父布局变量实例】}"。 ViewModel 的生命周期和响应式更新(ObservableField / LiveData)由父布局设置的 lifecycleOwner 统一管理。 MVVM中的<include>布局复用:viewModel传递 - 简书 1. ViewModel (数据源) 假设我们需要一个包含布尔状态的 ViewModel。 // MyComponentViewModel.kt import androidx.lifecycle.ViewModel import androidx.databinding.ObservableField class MyComponentViewModel : ViewModel() { // 示例:需要被子布局观察和操作的状态 val isToggled = ObservableField(false) fun toggle() { isToggled.set(!isToggled.get()) } } 2. 子布局 (layout_reusable_component.xml) 子布局必须使用 <data> 块声明一个 <variable> 作为接口来接收 ViewModel。 ...

2025年09月26日

Android蓝牙设备

Android蓝牙设备 [蓝牙设备特征UUID] 对于蓝牙HID输入设备直接拦截事件 原本设备JOPREE是在打开相机时控制拍照,未打开相机时增加音量 APP内直接拦截增加音量事件,不用管理蓝牙设备连接 直接连接手机即可 Activity内 override fun dispatchKeyEvent(event: KeyEvent): Boolean { val deviceName = event.device?.name if (deviceName != null && deviceName.contains("DEVICE_NAME", ignoreCase = true)) { if (event.action == KeyEvent.ACTION_DOWN) { mySubmitButton.performClick() return true } } return super.dispatchKeyEvent(event) }

2025年09月26日

Android原生侧滑

完整的 BaseActivity 原生侧滑返回方案文档,基于 AndroidX + Kotlin + 原生手势返回,可直接在项目中使用。文档包含 概念说明、实现代码、配置说明和调整方法。 BaseActivity 原生侧滑返回完整文档 1️⃣ 功能概述 实现 从屏幕左边缘滑动返回上一页 完全原生,不依赖第三方库 支持 AndroidX 可在 BaseActivity 中统一配置,所有继承的 Activity 自动生效 可自定义触发区域宽度(edgeSize)和滑动阈值(swipeThreshold) 支持 退出动画(使用 finishAfterTransition + Window ExitTransition) 2️⃣ 核心原理 手势检测:通过 onTouchEvent 监听用户触摸事件 滑动判断条件: 用户从屏幕左边缘滑动 水平滑动距离大于阈值 水平位移大于垂直位移(避免误触上下滑动) 触发返回: 调用 finishAfterTransition() 完成页面退出 可结合 Window ExitTransition 实现平滑滑出动画 3️⃣ BaseActivity 示例代码 open class BaseActivity : AppCompatActivity() { // 左边缘触发滑动区域(dp) private val edgeSizeDp = 20 private val swipeThresholdDp = 100 private var startX = 0f private var startY = 0f private var edgeSize = 0 private var swipeThreshold = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 将 dp 转 px,保证不同屏幕一致 val density = resources.displayMetrics.density edgeSize = (edgeSizeDp * density + 0.5f).toInt() swipeThreshold = (swipeThresholdDp * density + 0.5f).toInt() // 设置窗口退出动画(从右向左滑出) window.enterTransition = null window.exitTransition = android.transition.Slide(Gravity.END).apply { duration = 200 } } override fun onTouchEvent(event: MotionEvent): Boolean { when (event.action) { MotionEvent.ACTION_DOWN -> { startX = event.rawX startY = event.rawY } MotionEvent.ACTION_UP -> { val endX = event.rawX val endY = event.rawY val deltaX = endX - startX val deltaY = endY - startY // 条件:左边缘滑动 + 水平位移大于阈值 + 水平大于垂直位移 if (startX < edgeSize && deltaX > swipeThreshold && Math.abs(deltaX) > Math.abs(deltaY)) { finishAfterTransition() return true } } } return super.onTouchEvent(event) } } 4️⃣ 配置说明 参数 类型 默认值 作用 edgeSizeDp Int (dp) 20 左边缘触发滑动返回的宽度 swipeThresholdDp Int (dp) 100 滑动超过该距离才触发返回 window.exitTransition Transition Slide(Gravity.END) 页面退出动画,配合 finishAfterTransition() 使用 调整依据: ...

2025年09月26日