feat: basic control

This commit is contained in:
玖叁 2024-12-24 18:59:12 +08:00
parent d0fe216097
commit 4ab4487197
16 changed files with 417 additions and 96 deletions

View File

@ -18,6 +18,12 @@ android {
vectorDrawables {
useSupportLibrary = true
}
buildConfigField(
type = "String",
name = "BUILD_TIME",
value = "\"${System.currentTimeMillis()}\""
)
}
buildTypes {
@ -38,6 +44,7 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"

View File

@ -16,12 +16,20 @@ import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.automirrored.outlined.Send
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.app.ActivityCompat
import androidx.core.view.WindowCompat
import androidx.lifecycle.ViewModel
@ -30,6 +38,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navigation
import icu.fur93.esp32_car.data.PreferencesDataStore
import icu.fur93.esp32_car.page.ControlGamepadModePage
import icu.fur93.esp32_car.page.ControlPage
@ -37,8 +46,10 @@ import icu.fur93.esp32_car.page.ControlPathfinderModePage
import icu.fur93.esp32_car.page.ControlSingleJoystickModePage
import icu.fur93.esp32_car.page.HomePage
import icu.fur93.esp32_car.page.SettingsPage
import icu.fur93.esp32_car.provider.LocalNavigation
import icu.fur93.esp32_car.repository.BluetoothRepositoryImpl
import icu.fur93.esp32_car.ui.component.BottomNavigationBar
import icu.fur93.esp32_car.ui.component.BottomNavigationItem
import icu.fur93.esp32_car.ui.theme.Esp32carTheme
import icu.fur93.esp32_car.viewmodel.CarControlUseCase
import icu.fur93.esp32_car.viewmodel.CarViewModel
@ -165,9 +176,31 @@ fun App(viewModel: CarViewModel) {
onDispose {}
}
CompositionLocalProvider(
LocalNavigation provides navController,
) {
Scaffold(
bottomBar = {
BottomNavigationBar(navController)
BottomNavigationBar(
navController = navController,
navItems = listOf(
BottomNavigationItem(
icon = Icons.Outlined.Home,
selectedIcon = Icons.Default.Home,
route = Route.Home
),
BottomNavigationItem(
icon = Icons.AutoMirrored.Outlined.Send,
selectedIcon = Icons.AutoMirrored.Filled.Send,
route = Route.Control
),
BottomNavigationItem(
icon = Icons.Outlined.Settings,
selectedIcon = Icons.Default.Settings,
route = Route.Settings
)
)
)
},
modifier = Modifier
.fillMaxSize(),
@ -178,19 +211,30 @@ fun App(viewModel: CarViewModel) {
navController = navController,
startDestination = Route.Home.route
) {
// 主导航路由
composable(Route.Home.route) { HomePage(viewModel) }
composable(Route.Control.route) { ControlPage(viewModel) }
composable(Route.Settings.route) { SettingsPage(viewModel) }
// Control 相关的导航图
navigation(
startDestination = Route.Control.route,
route = "control_graph"
) {
composable(Route.Control.route) { ControlPage(viewModel) }
// Control 的子页面
composable(Route.ControlPathfinderMode.route) {
ControlPathfinderModePage(viewModel)
}
composable(Route.ControlSingleJoystickMode.route) {
ControlSingleJoystickModePage(viewModel)
}
composable(Route.ControlGamepadMode.route) { ControlGamepadModePage(viewModel) }
composable(Route.ControlGamepadMode.route) {
ControlGamepadModePage(viewModel)
}
}
}
}
}
}
}

View File

@ -1,10 +1,33 @@
package icu.fur93.esp32_car
sealed class Route(val route: String) {
data object Home : Route("home")
data object Control : Route("control")
data object Settings : Route("settings")
data object ControlPathfinderMode : Route("control_pathfinder_mode")
data object ControlSingleJoystickMode : Route("control_single_joystick_mode")
data object ControlGamepadMode : Route("control_gamepad_mode")
sealed class Route(
val route: String,
val title: String = route
) {
object Home : Route(
route = "home",
title = "首页"
)
object Settings : Route(
route = "settings",
title = "设置"
)
// Control 相关路由
object Control : Route(
route = "control",
title = "控制"
)
object ControlPathfinderMode : Route(
route = "control/pathfinder",
title = "循迹模式"
)
object ControlSingleJoystickMode : Route(
route = "control/single_joystick",
title = "单摇杆模式"
)
object ControlGamepadMode : Route(
route = "control/gamepad",
title = "单向按键模式"
)
}

View File

@ -0,0 +1,8 @@
package icu.fur93.esp32_car.entity.control
data class JoystickState(
val x: Float = 0f, // -1 到 1
val y: Float = 0f, // -1 到 1
val angle: Float = 0f, // 角度(弧度)
val distance: Float = 0f // 0 到 1
)

View File

@ -13,6 +13,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import icu.fur93.esp32_car.R
import icu.fur93.esp32_car.Route
import icu.fur93.esp32_car.provider.LocalNavigation
import icu.fur93.esp32_car.repository.ConnectionState
import icu.fur93.esp32_car.ui.card.UnconnectedStatusCard
import icu.fur93.esp32_car.ui.component.CardButtonGroup
@ -26,7 +28,7 @@ fun ControlPage(viewModel: CarViewModel) {
val connectionState by viewModel.connectionState.collectAsState()
Column(LayoutContentModifier) {
PageTitle("控制")
PageTitle(Route.Control.title)
if (connectionState == ConnectionState.CONNECTED) {
ControlPageStatusCard()
} else {
@ -49,7 +51,7 @@ fun ControlPageAutoControl() {
title = "自动控制",
buttons = listOf(
CardButtonItem(
text = "循迹模式",
text = Route.ControlPathfinderMode.title,
icon = ImageVector.vectorResource(R.drawable.outline_route_24),
onClick = {
Toast.makeText(context, "暂未实现", Toast.LENGTH_SHORT).show()
@ -62,16 +64,23 @@ fun ControlPageAutoControl() {
@Composable
fun ControlPageManualControl() {
val context = LocalContext.current
val navController = LocalNavigation.current
CardButtonGroup(
title = "手动控制",
buttons = listOf(
CardButtonItem(
text = "单摇杆模式",
icon = ImageVector.vectorResource(R.drawable.outline_joystick_24)
text = Route.ControlSingleJoystickMode.title,
icon = ImageVector.vectorResource(R.drawable.outline_joystick_24),
onClick = {
navController.navigate(Route.ControlSingleJoystickMode.route)
}
),
CardButtonItem(
text = "单向按键模式",
icon = ImageVector.vectorResource(R.drawable.outline_gamepad_24)
text = Route.ControlGamepadMode.title,
icon = ImageVector.vectorResource(R.drawable.outline_gamepad_24),
onClick = {
navController.navigate(Route.ControlGamepadMode.route)
}
),
CardButtonItem(
text = "陀螺仪模式",

View File

@ -1,8 +1,188 @@
package icu.fur93.esp32_car.page
import android.view.MotionEvent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import icu.fur93.esp32_car.R
import icu.fur93.esp32_car.Route
import icu.fur93.esp32_car.entity.control.JoystickState
import icu.fur93.esp32_car.ui.carditem.StatusCardInfo
import icu.fur93.esp32_car.ui.carditem.StatusCardJoystickStatus
import icu.fur93.esp32_car.ui.carditem.StatusCardMotorStatus
import icu.fur93.esp32_car.ui.component.Joystick
import icu.fur93.esp32_car.ui.component.PageTitle
import icu.fur93.esp32_car.ui.component.StatusCard
import icu.fur93.esp32_car.ui.theme.LayoutContentModifier
import icu.fur93.esp32_car.viewmodel.CarViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun ControlSingleJoystickModePage(viewModel: CarViewModel) {
var joystickState by remember { mutableStateOf(JoystickState()) }
Column(LayoutContentModifier) {
PageTitle(Route.ControlSingleJoystickMode.title)
ControlSingleJoystickModeStatusCard(viewModel, joystickState)
ControlSingleJoystickModeControlPanel(viewModel) {
joystickState = it
}
}
}
@Composable
fun ControlSingleJoystickModeStatusCard(viewModel: CarViewModel, joystickState: JoystickState) {
val deviceInfo by viewModel.connectionInfoState.collectAsState()
val carState by viewModel.carState.collectAsState()
StatusCard(
cardItems = listOf(
StatusCardInfo(deviceInfo),
StatusCardMotorStatus(carState),
StatusCardJoystickStatus(joystickState)
)
)
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun ControlSingleJoystickModeControlPanel(
viewModel: CarViewModel,
onJoystickStateChange: (JoystickState) -> Unit
) {
Spacer(Modifier.height(24.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
FilledIconButton(
onClick = {},
modifier = Modifier.pointerInteropFilter { event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
viewModel.sendXYR(0, 0, 50)
true
}
MotionEvent.ACTION_UP -> {
viewModel.stop()
true
}
else -> false
}
}
) {
Icon(ImageVector.vectorResource(R.drawable.rotate_left_24), "逆时针旋转")
}
ControlJoystick(
viewModel = viewModel,
onJoystickStateChange = onJoystickStateChange
)
FilledIconButton(
onClick = {},
modifier = Modifier.pointerInteropFilter { event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
viewModel.sendXYR(0, 0, -50)
true
}
MotionEvent.ACTION_UP -> {
viewModel.stop()
true
}
else -> false
}
}
) {
Icon(ImageVector.vectorResource(R.drawable.rotate_right_24), "顺时针旋转")
}
}
}
@Composable
fun ControlJoystick(
viewModel: CarViewModel,
onJoystickStateChange: (JoystickState) -> Unit
) {
// 记住上一次发送的状态和时间
val lastSentState = remember { mutableStateOf(JoystickState()) }
val lastSentTime = remember { mutableStateOf(0L) }
// 用于停止命令重发的计数器
val stopCommandCount = remember { mutableStateOf(0) }
val coroutineScope = rememberCoroutineScope()
Joystick(
size = 175.dp,
knobScale = 0.25f
) { state ->
// 更新状态
onJoystickStateChange(state)
val currentTime = System.currentTimeMillis()
// 检查是否是停止状态(摇杆回正)
val isStopCommand = state.x == 0f && state.y == 0f
// 如果是停止命令,启动重发机制
// 否则检查时间间隔和状态变化
if (isStopCommand) {
if (stopCommandCount.value == 0) {
// 第一次发送停止命令
viewModel.stop()
stopCommandCount.value = 1
// 启动延时重发
coroutineScope.launch {
delay(50) // 延时50ms
viewModel.stop() // 再次发送停止命令
stopCommandCount.value = 2
delay(50) // 再次延时50ms
viewModel.stop() // 第三次发送停止命令
stopCommandCount.value = 0 // 重置计数器
}
}
} else if ((state.x != lastSentState.value.x || state.y != lastSentState.value.y) &&
currentTime - lastSentTime.value >= 50
) {
// 非停止命令的正常处理
stopCommandCount.value = 0 // 重置停止命令计数器
val x = (state.x * 100).toInt()
val y = (state.y * 100).toInt()
viewModel.sendXYR(x, y, 0)
// 更新上次发送的状态和时间
lastSentState.value = state
lastSentTime.value = currentTime
}
}
}

View File

@ -19,11 +19,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.navigation.NavHostController
import icu.fur93.esp32_car.BuildConfig
import icu.fur93.esp32_car.R
import icu.fur93.esp32_car.Route
import icu.fur93.esp32_car.repository.BluetoothRepositoryImpl
import icu.fur93.esp32_car.repository.ConnectionState
import icu.fur93.esp32_car.ui.component.PageTitle
import icu.fur93.esp32_car.ui.component.TipText
import icu.fur93.esp32_car.ui.dialog.BleDeviceScanDialog
import icu.fur93.esp32_car.ui.theme.LayoutContentPadding
import icu.fur93.esp32_car.ui.theme.LayoutTopPadding
@ -39,9 +41,16 @@ fun SettingsPage(viewModel: CarViewModel) {
end = LayoutContentPadding
)
) {
PageTitle("设置")
PageTitle(Route.Settings.title)
}
SettingsList(viewModel)
TipText("GitHub: colour93/esp32-car-android")
TipText("Build: ${
android.text.format.DateFormat.format(
"yyyy/MM/dd HH:mm:ss",
BuildConfig.BUILD_TIME.toLong()
)
}")
}
}

View File

@ -0,0 +1,6 @@
package icu.fur93.esp32_car.provider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.navigation.NavHostController
val LocalNavigation = staticCompositionLocalOf<NavHostController> { error("Not provided") }

View File

@ -7,12 +7,14 @@ import android.bluetooth.BluetoothDevice.TRANSPORT_LE
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import android.util.Log
import android.widget.Toast
import icu.fur93.esp32_car.const.CarCommands
import icu.fur93.esp32_car.entity.BleDevice
@ -189,15 +191,21 @@ class BluetoothRepositoryImpl(
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
super.onServicesDiscovered(gatt, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
// 找到特定的服务和特征
gatt.services?.forEach { service ->
service.characteristics?.forEach { characteristic ->
// 这里需要根据你的设备具体的UUID来匹配
if (characteristic.uuid == UUID.fromString(rxCharUUID)) {
rxCharacteristic = characteristic
}
gatt?.getService(UUID.fromString(serviceUUID))?.let { service ->
bluetoothGatt = gatt
rxCharacteristic =
service.getCharacteristic(UUID.fromString(rxCharUUID))
// 注册通知监听器
val txCharacteristic =
service.getCharacteristic(UUID.fromString(txCharUUID))
gatt.setCharacteristicNotification(txCharacteristic, true)
txCharacteristic.descriptors.forEach { descriptor ->
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
}
}
}
@ -208,6 +216,7 @@ class BluetoothRepositoryImpl(
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
super.onCharacteristicChanged(gatt, characteristic, value)
if (characteristic.uuid == UUID.fromString(txCharUUID)) {
onReceivePacket(value)
}

View File

@ -2,17 +2,19 @@ package icu.fur93.esp32_car.ui.carditem
import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import icu.fur93.esp32_car.entity.control.JoystickState
import icu.fur93.esp32_car.ui.component.CardItem
import icu.fur93.esp32_car.ui.component.StatusCardItemText
import icu.fur93.esp32_car.ui.component.format
@SuppressLint("ComposableNaming")
@Composable
fun StatusCardJoystickStatus () : CardItem {
fun StatusCardJoystickStatus (joystickState: JoystickState) : CardItem {
return CardItem(
title = "摇杆状态",
content = {
StatusCardItemText("X 0.00")
StatusCardItemText("Y 0.00")
StatusCardItemText("X ${joystickState.x.format(2)}")
StatusCardItemText("Y ${joystickState.y.format(2)}")
}
)
}

View File

@ -17,11 +17,8 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.navigation.NavHostController
import icu.fur93.esp32_car.Route
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.ui.unit.dp
data class BottomNavigationItem(
val title: String,
val icon: ImageVector,
val selectedIcon: ImageVector,
val route: Route
@ -29,29 +26,9 @@ data class BottomNavigationItem(
@Composable
fun BottomNavigationBar(
navController: NavHostController
navController: NavHostController,
navItems: List<BottomNavigationItem>
) {
val navItems = listOf(
BottomNavigationItem(
title = "主页",
icon = Icons.Outlined.Home,
selectedIcon = Icons.Default.Home,
route = Route.Home
),
BottomNavigationItem(
title = "控制",
icon = Icons.AutoMirrored.Outlined.Send,
selectedIcon = Icons.AutoMirrored.Filled.Send,
route = Route.Control
),
BottomNavigationItem(
title = "设置",
icon = Icons.Outlined.Settings,
selectedIcon = Icons.Default.Settings,
route = Route.Settings
)
)
val currentRoute = navController
.currentBackStackEntryFlow
.collectAsState(initial = navController.currentBackStackEntry)
@ -66,13 +43,21 @@ fun BottomNavigationBar(
Icon(
if ((currentRoute
?: "").startsWith(item.route.route)
) item.selectedIcon else item.icon, item.title
) item.selectedIcon else item.icon, item.route.title
)
},
label = { Text(item.title) },
label = { Text(item.route.title) },
selected = (currentRoute?:"").startsWith(item.route.route),
onClick = {
navController.navigate(item.route.route)
navController.navigate(item.route.route) {
// 弹出到导航图的起始位置
popUpTo(item.route.route) { inclusive = true }
launchSingleTop = true
// 避免创建同一目标的多个副本
launchSingleTop = true
// 恢复之前的状态
restoreState = false
}
}
)
}

View File

@ -4,30 +4,24 @@ import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import icu.fur93.esp32_car.entity.control.JoystickState
import kotlin.math.atan2
import kotlin.math.hypot
import kotlin.math.PI
// 摇杆数据类
data class JoystickState(
val x: Float = 0f, // -1 到 1
val y: Float = 0f, // -1 到 1
val angle: Float = 0f, // 角度(弧度)
val distance: Float = 0f // 0 到 1
)
@Composable
fun Joystick(
modifier: Modifier = Modifier,
size: Float = 200f,
size: Dp = 200.dp,
knobScale: Float = 0.4f,
onJoystickMove: (JoystickState) -> Unit
) {
// 状态管理
@ -35,23 +29,23 @@ fun Joystick(
var stickPosition by remember { mutableStateOf(Offset.Zero) }
var isDragging by remember { mutableStateOf(false) }
// 计算实际尺寸
val radius = size / 2
val innerRadius = radius * 0.4f
val surfaceColor = MaterialTheme.colorScheme.surfaceDim
val primaryColor = MaterialTheme.colorScheme.primary
// 创建摇杆
Canvas(
modifier = modifier
.size(size.dp)
.padding(8.dp)
.size(size)
.pointerInput(Unit) {
// 将 size 转换为像素
val sizePx = size.toPx()
val radius = sizePx / 2
val innerRadius = radius * knobScale
detectDragGestures(
onDragStart = { offset ->
isDragging = true
centerPoint = Offset(size / 2, size / 2)
centerPoint = Offset(sizePx / 2, sizePx / 2)
updateStickPosition(offset, centerPoint, radius, innerRadius) { newPos ->
stickPosition = newPos
val state = calculateJoystickState(centerPoint, newPos, radius)
@ -74,9 +68,14 @@ fun Joystick(
)
}
) {
// 获取实际像素尺寸
val sizePx = size.toPx()
val radius = sizePx / 2
val innerRadius = radius * knobScale
// 初始化中心点
if (centerPoint == Offset.Zero) {
centerPoint = Offset(size / 2, size / 2)
centerPoint = Offset(sizePx / 2, sizePx / 2)
stickPosition = centerPoint
}

View File

@ -0,0 +1,29 @@
package icu.fur93.esp32_car.ui.component
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.sp
@Composable()
fun TipText(
text: String
) {
Row (
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Text(
text = text,
style = TextStyle(
fontSize = 12.sp,
color = MaterialTheme.colorScheme.tertiary
),
)
}
}

View File

@ -2,6 +2,7 @@ package icu.fur93.esp32_car.viewmodel
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import icu.fur93.esp32_car.const.CarCommands

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="@android:color/white" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M7.11,8.53L5.7,7.11C4.8,8.27 4.24,9.61 4.07,11h2.02c0.14,-0.87 0.49,-1.72 1.02,-2.47zM6.09,13L4.07,13c0.17,1.39 0.72,2.73 1.62,3.89l1.41,-1.42c-0.52,-0.75 -0.87,-1.59 -1.01,-2.47zM7.1,18.32c1.16,0.9 2.51,1.44 3.9,1.61L11,17.9c-0.87,-0.15 -1.71,-0.49 -2.46,-1.03L7.1,18.32zM13,4.07L13,1L8.45,5.55 13,10L13,6.09c2.84,0.48 5,2.94 5,5.91s-2.16,5.43 -5,5.91v2.02c3.95,-0.49 7,-3.85 7,-7.93s-3.05,-7.44 -7,-7.93z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="@android:color/white" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M15.55,5.55L11,1v3.07C7.06,4.56 4,7.92 4,12s3.05,7.44 7,7.93v-2.02c-2.84,-0.48 -5,-2.94 -5,-5.91s2.16,-5.43 5,-5.91L11,10l4.55,-4.45zM19.93,11c-0.17,-1.39 -0.72,-2.73 -1.62,-3.89l-1.42,1.42c0.54,0.75 0.88,1.6 1.02,2.47h2.02zM13,17.9v2.02c1.39,-0.17 2.74,-0.71 3.9,-1.61l-1.44,-1.44c-0.75,0.54 -1.59,0.89 -2.46,1.03zM16.89,15.48l1.42,1.41c0.9,-1.16 1.45,-2.5 1.62,-3.89h-2.02c-0.14,0.87 -0.48,1.72 -1.02,2.48z"/>
</vector>