feat: фирменная иконка приложения + внутренний плеер записей
- адаптивная иконка лаунчера: градиентный фон (C2F25B->6FA53C) + монограмма R (foreground + monochrome для тем Android 13), манифест -> @mipmap - воспроизведение своих записей ВНУТРИ приложения вместо внешнего плеера: RecordingPlaybackController (отдельный ExoPlayer, останавливает радио), RecordingPlayerSheet с перемоткой (Slider), play/pause, +/-15с, таймеры
This commit is contained in:
@@ -14,9 +14,9 @@
|
|||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@drawable/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Radiola"
|
android:theme="@style/Theme.Radiola"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.radiola.service
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.media3.common.C
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import com.radiola.domain.model.Recording
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Контроллер воспроизведения записей эфира (отдельный ExoPlayer, независимый от радио).
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class RecordingPlaybackController @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val playerController: PlayerController
|
||||||
|
) {
|
||||||
|
private val scope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
|
private val _current = MutableStateFlow<Recording?>(null)
|
||||||
|
val current: StateFlow<Recording?> = _current
|
||||||
|
|
||||||
|
private val _isPlaying = MutableStateFlow(false)
|
||||||
|
val isPlaying: StateFlow<Boolean> = _isPlaying
|
||||||
|
|
||||||
|
private val _positionMs = MutableStateFlow(0L)
|
||||||
|
val positionMs: StateFlow<Long> = _positionMs
|
||||||
|
|
||||||
|
private val _durationMs = MutableStateFlow(0L)
|
||||||
|
val durationMs: StateFlow<Long> = _durationMs
|
||||||
|
|
||||||
|
private val exoPlayer: ExoPlayer = ExoPlayer.Builder(context).build().apply {
|
||||||
|
addListener(object : Player.Listener {
|
||||||
|
override fun onIsPlayingChanged(playing: Boolean) {
|
||||||
|
_isPlaying.value = playing
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
|
when (playbackState) {
|
||||||
|
Player.STATE_READY -> {
|
||||||
|
val dur = duration
|
||||||
|
_durationMs.value = if (dur == C.TIME_UNSET) 0L else dur
|
||||||
|
}
|
||||||
|
Player.STATE_ENDED -> {
|
||||||
|
_isPlaying.value = false
|
||||||
|
_positionMs.value = _durationMs.value
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private var positionPollingJob: Job? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Цикл обновления позиции каждые 500мс
|
||||||
|
positionPollingJob = scope.launch {
|
||||||
|
while (isActive) {
|
||||||
|
if (exoPlayer.isPlaying) {
|
||||||
|
_positionMs.value = exoPlayer.currentPosition
|
||||||
|
val dur = exoPlayer.duration
|
||||||
|
if (dur != C.TIME_UNSET) {
|
||||||
|
_durationMs.value = dur
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Начать воспроизведение записи. Сначала останавливает радио.
|
||||||
|
*/
|
||||||
|
fun play(recording: Recording) {
|
||||||
|
// Останавливаем радиоплеер
|
||||||
|
playerController.pause()
|
||||||
|
playerController.stop()
|
||||||
|
|
||||||
|
_current.value = recording
|
||||||
|
_positionMs.value = 0L
|
||||||
|
_durationMs.value = recording.duration ?: 0L
|
||||||
|
|
||||||
|
val mediaItem = MediaItem.fromUri(android.net.Uri.fromFile(File(recording.filePath)))
|
||||||
|
exoPlayer.setMediaItem(mediaItem)
|
||||||
|
exoPlayer.prepare()
|
||||||
|
exoPlayer.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Переключить паузу/воспроизведение. */
|
||||||
|
fun togglePlayPause() {
|
||||||
|
if (exoPlayer.isPlaying) {
|
||||||
|
exoPlayer.pause()
|
||||||
|
} else {
|
||||||
|
exoPlayer.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Перейти к позиции в мс. */
|
||||||
|
fun seekTo(ms: Long) {
|
||||||
|
val target = ms.coerceIn(0L, _durationMs.value.coerceAtLeast(1L))
|
||||||
|
exoPlayer.seekTo(target)
|
||||||
|
_positionMs.value = target
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Перемотать на deltaMs (может быть отрицательным). */
|
||||||
|
fun seekBy(deltaMs: Long) {
|
||||||
|
seekTo(_positionMs.value + deltaMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Остановить воспроизведение и сбросить текущую запись. */
|
||||||
|
fun stop() {
|
||||||
|
exoPlayer.stop()
|
||||||
|
_current.value = null
|
||||||
|
_isPlaying.value = false
|
||||||
|
_positionMs.value = 0L
|
||||||
|
_durationMs.value = 0L
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package com.radiola.ui.recordings
|
||||||
|
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.composables.icons.lucide.Lucide
|
||||||
|
import com.composables.icons.lucide.Pause
|
||||||
|
import com.composables.icons.lucide.Play
|
||||||
|
import com.composables.icons.lucide.RotateCcw
|
||||||
|
import com.composables.icons.lucide.RotateCw
|
||||||
|
import com.radiola.domain.model.Recording
|
||||||
|
import com.radiola.ui.theme.Motion
|
||||||
|
import com.radiola.ui.theme.RadiolaTheme
|
||||||
|
import com.radiola.ui.theme.pressScale
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Контент нижнего листа (bottom sheet) для воспроизведения записи эфира.
|
||||||
|
* Оборачивать в [ModalBottomSheet] на стороне вызывающего.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun RecordingPlayerSheet(
|
||||||
|
recording: Recording,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
viewModel: RecordingPlayerViewModel
|
||||||
|
) {
|
||||||
|
val isPlaying by viewModel.isPlaying.collectAsState()
|
||||||
|
val positionMs by viewModel.positionMs.collectAsState()
|
||||||
|
val durationMs by viewModel.durationMs.collectAsState()
|
||||||
|
val haptic = LocalHapticFeedback.current
|
||||||
|
val colors = RadiolaTheme.colors
|
||||||
|
val dateFormat = remember { SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.getDefault()) }
|
||||||
|
|
||||||
|
// Запуск воспроизведения при открытии листа
|
||||||
|
LaunchedEffect(recording) {
|
||||||
|
viewModel.play(recording)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp)
|
||||||
|
.padding(bottom = 40.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Метка «ЗАПИСЬ ЭФИРА»
|
||||||
|
Text(
|
||||||
|
text = "ЗАПИСЬ ЭФИРА",
|
||||||
|
style = MaterialTheme.typography.labelSmall.copy(
|
||||||
|
letterSpacing = 2.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
),
|
||||||
|
color = colors.accent
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Название станции
|
||||||
|
Text(
|
||||||
|
text = recording.stationName,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = colors.textPrimary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
// Трек (если есть) + дата
|
||||||
|
val meta = buildString {
|
||||||
|
if (!recording.trackName.isNullOrBlank()) {
|
||||||
|
append(recording.trackName)
|
||||||
|
append(" · ")
|
||||||
|
}
|
||||||
|
append(dateFormat.format(Date(recording.startTime)))
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = meta,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = colors.textMuted,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Seekbar
|
||||||
|
val effectiveDuration = durationMs.coerceAtLeast(recording.duration ?: 1L).coerceAtLeast(1L)
|
||||||
|
Slider(
|
||||||
|
value = positionMs.toFloat(),
|
||||||
|
onValueChange = { viewModel.seekTo(it.toLong()) },
|
||||||
|
valueRange = 0f..effectiveDuration.toFloat(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
thumbColor = colors.accent,
|
||||||
|
activeTrackColor = colors.accent,
|
||||||
|
inactiveTrackColor = colors.surface2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Время: текущее и общее
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = formatMs(positionMs),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = colors.textMuted
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = formatMs(effectiveDuration),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = colors.textMuted
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Ряд управления: rewind15, play/pause, forward15
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(32.dp)
|
||||||
|
) {
|
||||||
|
// Перемотка назад на 15 секунд
|
||||||
|
IconButton(
|
||||||
|
onClick = { viewModel.rewind15() },
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Lucide.RotateCcw,
|
||||||
|
contentDescription = "Назад 15 сек",
|
||||||
|
tint = colors.textSecondary,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка play/pause
|
||||||
|
val playInteraction = remember { MutableInteractionSource() }
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(64.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.pressScale(interactionSource = playInteraction),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
viewModel.togglePlayPause()
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
shape = CircleShape,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = colors.accent,
|
||||||
|
contentColor = colors.bgBase
|
||||||
|
),
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
|
interactionSource = playInteraction
|
||||||
|
) {
|
||||||
|
Crossfade(
|
||||||
|
targetState = isPlaying,
|
||||||
|
animationSpec = tween(Motion.Fast),
|
||||||
|
label = "playPauseCrossfade"
|
||||||
|
) { playing ->
|
||||||
|
Icon(
|
||||||
|
imageVector = if (playing) Lucide.Pause else Lucide.Play,
|
||||||
|
contentDescription = if (playing) "Пауза" else "Воспроизвести",
|
||||||
|
modifier = Modifier.size(28.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Перемотка вперёд на 15 секунд
|
||||||
|
IconButton(
|
||||||
|
onClick = { viewModel.forward15() },
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Lucide.RotateCw,
|
||||||
|
contentDescription = "Вперёд 15 сек",
|
||||||
|
tint = colors.textSecondary,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Форматирует миллисекунды в строку mm:ss. */
|
||||||
|
private fun formatMs(ms: Long): String {
|
||||||
|
val totalSeconds = (ms / 1000).coerceAtLeast(0)
|
||||||
|
val minutes = totalSeconds / 60
|
||||||
|
val seconds = totalSeconds % 60
|
||||||
|
return "%02d:%02d".format(minutes, seconds)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.radiola.ui.recordings
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.radiola.domain.model.Recording
|
||||||
|
import com.radiola.service.RecordingPlaybackController
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel экрана воспроизведения записи. Проксирует состояние и команды
|
||||||
|
* к [RecordingPlaybackController].
|
||||||
|
*/
|
||||||
|
@HiltViewModel
|
||||||
|
class RecordingPlayerViewModel @Inject constructor(
|
||||||
|
private val controller: RecordingPlaybackController
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
val current: StateFlow<Recording?> = controller.current
|
||||||
|
val isPlaying: StateFlow<Boolean> = controller.isPlaying
|
||||||
|
val positionMs: StateFlow<Long> = controller.positionMs
|
||||||
|
val durationMs: StateFlow<Long> = controller.durationMs
|
||||||
|
|
||||||
|
fun play(recording: Recording) = controller.play(recording)
|
||||||
|
|
||||||
|
fun togglePlayPause() = controller.togglePlayPause()
|
||||||
|
|
||||||
|
fun seekTo(ms: Long) = controller.seekTo(ms)
|
||||||
|
|
||||||
|
/** Перемотка назад на 15 секунд. */
|
||||||
|
fun rewind15() = controller.seekBy(-15_000L)
|
||||||
|
|
||||||
|
/** Перемотка вперёд на 15 секунд. */
|
||||||
|
fun forward15() = controller.seekBy(15_000L)
|
||||||
|
|
||||||
|
/** Закрыть плеер и остановить воспроизведение. */
|
||||||
|
fun close() = controller.stop()
|
||||||
|
}
|
||||||
@@ -19,7 +19,6 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
@@ -40,15 +39,15 @@ import java.util.Date
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RecordingsScreen(
|
fun RecordingsScreen(
|
||||||
viewModel: RecordingsViewModel = hiltViewModel()
|
viewModel: RecordingsViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
val recordings by viewModel.recordings.collectAsState()
|
val recordings by viewModel.recordings.collectAsState()
|
||||||
val isRecording by viewModel.isRecording.collectAsState()
|
val isRecording by viewModel.isRecording.collectAsState()
|
||||||
val context = LocalContext.current
|
|
||||||
val colors = RadiolaTheme.colors
|
val colors = RadiolaTheme.colors
|
||||||
|
var playing by remember { mutableStateOf<Recording?>(null) }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -119,21 +118,7 @@ fun RecordingsScreen(
|
|||||||
items(recordings, key = { it.id }) { recording ->
|
items(recordings, key = { it.id }) { recording ->
|
||||||
RecordingItem(
|
RecordingItem(
|
||||||
recording = recording,
|
recording = recording,
|
||||||
onPlay = {
|
onPlay = { playing = recording },
|
||||||
// TODO: воспроизвести запись через ExoPlayer или внешний плеер
|
|
||||||
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW).apply {
|
|
||||||
setDataAndType(
|
|
||||||
androidx.core.content.FileProvider.getUriForFile(
|
|
||||||
context,
|
|
||||||
"${context.packageName}.fileprovider",
|
|
||||||
java.io.File(recording.filePath)
|
|
||||||
),
|
|
||||||
"audio/*"
|
|
||||||
)
|
|
||||||
addFlags(android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
context.startActivity(intent)
|
|
||||||
},
|
|
||||||
onDelete = { viewModel.deleteRecording(recording.id) },
|
onDelete = { viewModel.deleteRecording(recording.id) },
|
||||||
modifier = Modifier.animateItemPlacement()
|
modifier = Modifier.animateItemPlacement()
|
||||||
)
|
)
|
||||||
@@ -142,6 +127,20 @@ fun RecordingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Встроенный плеер в нижнем листе
|
||||||
|
playing?.let { rec ->
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = { playing = null },
|
||||||
|
containerColor = colors.bgBase
|
||||||
|
) {
|
||||||
|
RecordingPlayerSheet(
|
||||||
|
recording = rec,
|
||||||
|
onDismiss = { playing = null },
|
||||||
|
viewModel = hiltViewModel()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
18
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
18
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path android:pathData="M0,0 H108 V108 H0 Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="14" android:startY="14"
|
||||||
|
android:endX="94" android:endY="94"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FFC2F25B" />
|
||||||
|
<item android:offset="1" android:color="#FF6FA53C" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
</vector>
|
||||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@mipmap/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@mipmap/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 725 B |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
Reference in New Issue
Block a user