Skip to content

Commit 651d62d

Browse files
committed
changep play/pause buton and haptic
1 parent c6207e3 commit 651d62d

File tree

6 files changed

+312
-78
lines changed

6 files changed

+312
-78
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ dependencies {
170170
implementation(libs.androidx.lifecycle.process)
171171
implementation(libs.androidx.material3)
172172
implementation(libs.androidx.foundation)
173+
implementation(libs.androidx.foundation.layout)
173174
runtimeOnly(libs.androidx.profileinstaller)
174175
implementation(project(":libs:cropper"))
175176
"baselineProfile"(project(mapOf("path" to ":baselineprofile")))
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.dot.gallery.core.presentation.components
2+
3+
import androidx.compose.animation.core.Spring
4+
import androidx.compose.animation.core.animateFloatAsState
5+
import androidx.compose.animation.core.spring
6+
import androidx.compose.foundation.layout.Box
7+
import androidx.compose.foundation.layout.fillMaxSize
8+
import androidx.compose.foundation.layout.height
9+
import androidx.compose.foundation.layout.size
10+
import androidx.compose.foundation.layout.width
11+
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
12+
import androidx.compose.material3.LoadingIndicator
13+
import androidx.compose.material3.MaterialTheme
14+
import androidx.compose.runtime.Composable
15+
import androidx.compose.runtime.LaunchedEffect
16+
import androidx.compose.runtime.getValue
17+
import androidx.compose.runtime.mutableStateOf
18+
import androidx.compose.runtime.remember
19+
import androidx.compose.runtime.setValue
20+
import androidx.compose.ui.Alignment
21+
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.graphics.Color
23+
import androidx.compose.ui.unit.dp
24+
import kotlinx.coroutines.delay
25+
26+
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
27+
@Composable
28+
fun ExpressiveScreenLoader(
29+
modifier: Modifier = Modifier,
30+
color: Color = MaterialTheme.colorScheme.primary
31+
) {
32+
// Стан для анімації (0 -> 1 -> 0)
33+
var targetProgress by remember { mutableStateOf(0.1f) }
34+
35+
// Запускаємо цикл анімації "дихання"
36+
LaunchedEffect(Unit) {
37+
while (true) {
38+
targetProgress = 1f // Заповнюємо
39+
delay(1500) // Чекаємо поки пружина відпрацює
40+
targetProgress = 0.05f // Спустошуємо (майже до нуля, щоб не зникав повністю)
41+
delay(1500)
42+
}
43+
}
44+
45+
// Твоя улюблена фізика пружини
46+
val animatedProgress by animateFloatAsState(
47+
targetValue = targetProgress,
48+
animationSpec = spring(
49+
dampingRatio = Spring.DampingRatioNoBouncy, // Плавність без відскоку
50+
stiffness = Spring.StiffnessVeryLow, // Дуже м'яко і повільно
51+
visibilityThreshold = 1 / 1000f,
52+
),
53+
label = "ExpressiveLoaderAnimation"
54+
)
55+
56+
Box(
57+
modifier = modifier.fillMaxSize(),
58+
contentAlignment = Alignment.Center
59+
) {
60+
LoadingIndicator(
61+
modifier = Modifier.size(48.dp),
62+
)
63+
}
64+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.dot.gallery.feature_node.presentation
2+
3+
import androidx.compose.animation.core.Spring
4+
import androidx.compose.animation.core.animateFloatAsState
5+
import androidx.compose.animation.core.spring
6+
import androidx.compose.foundation.layout.Box
7+
import androidx.compose.foundation.layout.fillMaxSize
8+
import androidx.compose.foundation.layout.height
9+
import androidx.compose.foundation.layout.width
10+
import androidx.compose.material3.LinearProgressIndicator
11+
import androidx.compose.material3.MaterialTheme
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.runtime.LaunchedEffect
14+
import androidx.compose.runtime.getValue
15+
import androidx.compose.runtime.mutableFloatStateOf
16+
import androidx.compose.runtime.remember
17+
import androidx.compose.runtime.setValue
18+
import androidx.compose.ui.Alignment
19+
import androidx.compose.ui.Modifier
20+
import androidx.compose.ui.unit.dp
21+
import kotlinx.coroutines.delay
22+
23+
@Composable
24+
fun ExpressiveScreenLoader() {
25+
// Стан для анімації прогресу
26+
var targetProgress by remember { mutableFloatStateOf(0.1f) }
27+
28+
// Запускаємо цикл: заповнити -> почекати -> очистити
29+
LaunchedEffect(Unit) {
30+
while (true) {
31+
targetProgress = 1f
32+
delay(1500) // Час, поки воно повне
33+
targetProgress = 0.05f
34+
delay(1500) // Час, поки воно пусте
35+
}
36+
}
37+
38+
// Твоя анімація з фізикою пружини
39+
val animatedProgress by animateFloatAsState(
40+
targetValue = targetProgress,
41+
animationSpec = spring(
42+
dampingRatio = Spring.DampingRatioNoBouncy,
43+
stiffness = Spring.StiffnessVeryLow,
44+
visibilityThreshold = 1 / 1000f,
45+
),
46+
label = "expressive_loader"
47+
)
48+
49+
// Центруємо на весь екран
50+
Box(
51+
modifier = Modifier.fillMaxSize(),
52+
contentAlignment = Alignment.Center
53+
) {
54+
LinearProgressIndicator(
55+
progress = { animatedProgress },
56+
modifier = Modifier
57+
.width(80.dp) // Ширина індикатора
58+
.height(6.dp), // Товщина лінії
59+
color = MaterialTheme.colorScheme.primary,
60+
trackColor = MaterialTheme.colorScheme.surfaceContainerHighest
61+
)
62+
}
63+
}

app/src/main/kotlin/com/dot/gallery/feature_node/presentation/mediaview/components/video/VideoPlayerController.kt

Lines changed: 137 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,20 @@ import com.dot.gallery.R
7171
import com.dot.gallery.feature_node.domain.model.PlaybackSpeed
7272
import com.dot.gallery.feature_node.presentation.util.formatMinSec
7373
import kotlinx.coroutines.launch
74+
import androidx.compose.animation.animateColorAsState
75+
import androidx.compose.animation.core.animateIntAsState
76+
import androidx.compose.animation.core.Spring
77+
import androidx.compose.animation.core.spring
78+
import androidx.compose.foundation.interaction.MutableInteractionSource
79+
import androidx.compose.foundation.interaction.collectIsPressedAsState
80+
import androidx.compose.ui.draw.scale
81+
import androidx.compose.ui.draw.clip
82+
import androidx.compose.foundation.shape.RoundedCornerShape
83+
import androidx.compose.foundation.clickable
84+
import android.view.HapticFeedbackConstants
85+
import androidx.compose.animation.core.animateDpAsState
86+
import androidx.compose.ui.platform.LocalView
87+
import kotlinx.coroutines.delay
7488

7589
@OptIn(ExperimentalMaterial3Api::class)
7690
@Composable
@@ -166,13 +180,13 @@ fun VideoPlayerController(
166180
)
167181
}
168182
}
169-
IconButton(onClick = { showMenu = !showMenu }) {
170-
Icon(
171-
imageVector = Icons.Outlined.Speed,
172-
tint = Color.White,
173-
contentDescription = stringResource(R.string.change_playback_speed_cd)
174-
)
175-
}
183+
// IconButton(onClick = { showMenu = !showMenu }) {
184+
// Icon(
185+
// imageVector = Icons.Outlined.Speed,
186+
// tint = Color.White,
187+
// contentDescription = stringResource(R.string.change_playback_speed_cd)
188+
// )
189+
// }
176190
}
177191

178192
IconButton(
@@ -186,21 +200,22 @@ fun VideoPlayerController(
186200
isMuted = true
187201
}
188202
}
189-
) {
190-
Icon(
191-
imageVector = if (isMuted) Icons.AutoMirrored.Outlined.VolumeMute else Icons.AutoMirrored.Outlined.VolumeUp,
192-
tint = Color.White,
193-
contentDescription = stringResource(R.string.toggle_audio_cd)
194-
)
203+
)
204+
{
205+
// Icon(
206+
// imageVector = if (isMuted) Icons.AutoMirrored.Outlined.VolumeMute else Icons.AutoMirrored.Outlined.VolumeUp,
207+
// tint = Color.White,
208+
// contentDescription = stringResource(R.string.toggle_audio_cd)
209+
// )
195210
}
196211

197-
IconButton(onClick = { toggleRotate() }) {
198-
Icon(
199-
imageVector = Icons.Outlined.ScreenRotation,
200-
tint = Color.White,
201-
contentDescription = stringResource(R.string.rotate_screen_cd)
202-
)
203-
}
212+
// IconButton(onClick = { toggleRotate() }) {
213+
// Icon(
214+
// imageVector = Icons.Outlined.ScreenRotation,
215+
// tint = Color.White,
216+
// contentDescription = stringResource(R.string.rotate_screen_cd)
217+
// )
218+
// }
204219

205220
// --- Timeline Row (Wavy Scrubber) ---
206221
Row(
@@ -266,36 +281,23 @@ fun VideoPlayerController(
266281
}
267282

268283
// Center Play/Pause button
269-
IconButton(
270-
onClick = {
271-
val newState = !isPlaying.value
272-
isPlaying.value = newState
273-
if (newState) {
274-
player.playWhenReady = true
275-
player.play()
276-
} else {
277-
player.pause()
278-
}
279-
},
284+
Box(
280285
modifier = Modifier
281286
.align(Alignment.Center)
282-
.size(64.dp)
283287
) {
284-
if (isPlaying.value && player.isPlaying) {
285-
Image(
286-
modifier = Modifier.fillMaxSize(),
287-
imageVector = Icons.Filled.PauseCircleFilled,
288-
contentDescription = stringResource(R.string.pause_video),
289-
colorFilter = ColorFilter.tint(Color.White)
290-
)
291-
} else {
292-
Image(
293-
modifier = Modifier.fillMaxSize(),
294-
imageVector = Icons.Filled.PlayCircleFilled,
295-
contentDescription = stringResource(R.string.play_video),
296-
colorFilter = ColorFilter.tint(Color.White)
297-
)
298-
}
288+
ExpressivePlayButton(
289+
isPlaying = isPlaying.value && player.isPlaying,
290+
onClick = {
291+
val newState = !isPlaying.value
292+
isPlaying.value = newState
293+
if (newState) {
294+
player.playWhenReady = true
295+
player.play()
296+
} else {
297+
player.pause()
298+
}
299+
}
300+
)
299301
}
300302
}
301303
}
@@ -372,4 +374,91 @@ fun WavyVideoScrubber(
372374
trackColor = Color.White.copy(alpha = 0.3f)
373375
)
374376
}
375-
}
377+
}
378+
379+
@Composable
380+
fun ExpressivePlayButton(
381+
isPlaying: Boolean,
382+
onClick: () -> Unit
383+
) {
384+
val view = LocalView.current
385+
val scope = rememberCoroutineScope() // Для таймера анімації
386+
387+
val interactionSource = remember { MutableInteractionSource() }
388+
val isPressed by interactionSource.collectIsPressedAsState()
389+
390+
// Додатковий стан для "удару" при швидкому кліку
391+
var isAnimatingClick by remember { mutableStateOf(false) }
392+
393+
// Анімація ширини
394+
// Логіка: Якщо тиснеш АБО тільки що клікнув -> розширюємось
395+
val width by animateDpAsState(
396+
targetValue = if (isPressed || isAnimatingClick) 115.dp else 90.dp,
397+
animationSpec = spring(
398+
dampingRatio = 0.4f, // Пружний відскок
399+
stiffness = Spring.StiffnessMedium
400+
),
401+
label = "buttonWidth"
402+
)
403+
404+
// Анімація форми (без змін)
405+
val cornerPercent by animateIntAsState(
406+
targetValue = if (isPlaying) 50 else 30,
407+
animationSpec = spring(
408+
dampingRatio = Spring.DampingRatioNoBouncy,
409+
stiffness = Spring.StiffnessLow
410+
),
411+
label = "buttonShape"
412+
)
413+
414+
// Анімація кольору (без змін)
415+
val containerColor by animateColorAsState(
416+
targetValue = if (isPlaying)
417+
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f)
418+
else
419+
MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.9f),
420+
label = "buttonColor"
421+
)
422+
423+
val contentColor by animateColorAsState(
424+
targetValue = if (isPlaying)
425+
MaterialTheme.colorScheme.onPrimaryContainer
426+
else
427+
MaterialTheme.colorScheme.onTertiaryContainer,
428+
label = "iconColor"
429+
)
430+
431+
Box(
432+
modifier = Modifier
433+
.size(width = width, height = 64.dp)
434+
.clip(RoundedCornerShape(cornerPercent))
435+
.background(containerColor)
436+
.clickable(
437+
interactionSource = interactionSource,
438+
indication = null
439+
) {
440+
// 1. Хаптик (CONFIRM, як ти хотів)
441+
view.performHapticFeedback(HapticFeedbackConstants.CONFIRM)
442+
443+
// 2. Виконуємо дію
444+
onClick()
445+
446+
// 3. Запускаємо візуальний "поштовх"
447+
scope.launch {
448+
isAnimatingClick = true
449+
// Чекаємо 100мс, щоб пружина встигла візуально розширити кнопку
450+
delay(100)
451+
view.performHapticFeedback(HapticFeedbackConstants.CONFIRM)
452+
isAnimatingClick = false
453+
}
454+
},
455+
contentAlignment = Alignment.Center
456+
) {
457+
Icon(
458+
imageVector = if (isPlaying) Icons.Filled.PauseCircleFilled else Icons.Filled.PlayCircleFilled,
459+
contentDescription = null,
460+
tint = contentColor,
461+
modifier = Modifier.size(32.dp)
462+
)
463+
}
464+
}

0 commit comments

Comments
 (0)