diff --git a/app/src/main/java/org/akanework/gramophone/ui/components/FullBottomSheet.kt b/app/src/main/java/org/akanework/gramophone/ui/components/FullBottomSheet.kt index 6b73482706..b924e17d4a 100644 --- a/app/src/main/java/org/akanework/gramophone/ui/components/FullBottomSheet.kt +++ b/app/src/main/java/org/akanework/gramophone/ui/components/FullBottomSheet.kt @@ -17,6 +17,7 @@ package org.akanework.gramophone.ui.components +import android.animation.ObjectAnimator import android.animation.ValueAnimator import android.annotation.SuppressLint import android.content.Context @@ -36,14 +37,18 @@ import android.util.AttributeSet import android.view.AbsSavedState import android.view.Gravity import android.view.KeyEvent +import android.view.MotionEvent import android.view.View +import android.view.ViewConfiguration import android.view.ViewGroup import android.view.ViewPropertyAnimator import android.view.WindowInsets +import android.view.animation.LinearInterpolator import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.CheckBox import android.widget.EditText +import android.widget.ImageView import android.widget.LinearLayout import android.widget.ListView import android.widget.SeekBar @@ -140,6 +145,7 @@ import uk.akane.libphonograph.items.artistId import uk.akane.libphonograph.manipulator.PlaylistSerializer.Entry import java.text.NumberFormat import java.text.ParseException +import kotlin.math.abs import kotlin.math.max import kotlin.math.min @@ -170,6 +176,9 @@ class FullBottomSheet private val prefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) private var currentFormat: AudioFormatDetector.AudioFormats? = null + private var rotateCookieButton = false + private var swipeToSwitchTrackEnabled = false + private var cookieRotationAnimator: ObjectAnimator? = null companion object { const val SLIDER_UPDATE_INTERVAL: Long = 100 @@ -252,6 +261,7 @@ class FullBottomSheet private val bottomSheetFullTitle: TextView private val bottomSheetFullSubtitle: TextView private val bottomSheetFullControllerButton: MaterialButton + private val bottomSheetFullControllerButtonBg: ImageView private val bottomSheetFullNextButton: MaterialButton private val bottomSheetFullPreviousButton: MaterialButton private val bottomSheetFullDuration: TextView @@ -280,6 +290,7 @@ class FullBottomSheet bottomSheetFullSubtitle = findViewById(R.id.full_song_artist) bottomSheetFullPreviousButton = findViewById(R.id.sheet_previous_song) bottomSheetFullControllerButton = findViewById(R.id.sheet_mid_button) + bottomSheetFullControllerButtonBg = findViewById(R.id.sheet_mid_button_bg) bottomSheetFullNextButton = findViewById(R.id.sheet_next_song) bottomSheetFullPosition = findViewById(R.id.position) bottomSheetFullDuration = findViewById(R.id.duration) @@ -356,6 +367,52 @@ class FullBottomSheet } } + var startX = 0f + var startY = 0f + val touchSlop = ViewConfiguration.get(context).scaledTouchSlop + val swipeThresholdPx = 50.dpToPx(context) + bottomSheetFullCover.setOnTouchListener { v, event -> + if (!swipeToSwitchTrackEnabled) return@setOnTouchListener false + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + startX = event.x + startY = event.y + true + } + MotionEvent.ACTION_MOVE -> { + val dx = event.x - startX + val dy = event.y - startY + if (abs(dx) > touchSlop && abs(dx) > abs(dy)) { + v.parent?.requestDisallowInterceptTouchEvent(true) + } + true + } + MotionEvent.ACTION_UP -> { + v.parent?.requestDisallowInterceptTouchEvent(false) + val dx = event.x - startX + val dy = event.y - startY + val absDx = abs(dx) + val absDy = abs(dy) + if (absDx > absDy && absDx > swipeThresholdPx) { + ViewCompat.performHapticFeedback(v, HapticFeedbackConstantsCompat.GESTURE_END) + if (dx < 0) { + instance?.seekToNext() + } else { + instance?.seekToPrevious() + } + } else { + v.performClick() + } + true + } + MotionEvent.ACTION_CANCEL -> { + v.parent?.requestDisallowInterceptTouchEvent(false) + false + } + else -> false + } + } + bottomSheetFullTitle.setOnClickListener { minimize?.invoke() activity.startFragment(GeneralSubFragment()) { @@ -623,6 +680,42 @@ class FullBottomSheet ), ) bottomSheetFullSeekBar.progressTintList = ColorStateList.valueOf(colorPrimary) + if (rotateCookieButton && instance?.isPlaying == true) { + startButtonRotation() + } + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + stopButtonRotation(reset = false) + } + + private fun startButtonRotation() { + if (!rotateCookieButton) return + if (cookieRotationAnimator == null) { + val currentRotation = bottomSheetFullControllerButtonBg.rotation % 360f + cookieRotationAnimator = ObjectAnimator.ofFloat( + bottomSheetFullControllerButtonBg, + View.ROTATION, + currentRotation, + currentRotation + 360f + ).apply { + duration = 36000L + repeatCount = ValueAnimator.INFINITE + interpolator = LinearInterpolator() + start() + } + } else if (cookieRotationAnimator?.isStarted != true) { + cookieRotationAnimator?.start() + } + } + + private fun stopButtonRotation(reset: Boolean = false) { + cookieRotationAnimator?.cancel() + cookieRotationAnimator = null + if (reset) { + bottomSheetFullControllerButtonBg.rotation = 0f + } } override fun onSaveInstanceState(): Parcelable { @@ -711,6 +804,17 @@ class FullBottomSheet if (key == null || key == "cookie_cover") { bottomSheetFullCover.setClip(prefs.getBooleanStrict("cookie_cover", false)) } + if (key == null || key == "rotate_cookie_button") { + rotateCookieButton = prefs.getBooleanStrict("rotate_cookie_button", false) + if (rotateCookieButton && instance?.isPlaying == true) { + startButtonRotation() + } else { + stopButtonRotation(reset = true) + } + } + if (key == null || key == "swipe_to_switch_track") { + swipeToSwitchTrackEnabled = prefs.getBooleanStrict("swipe_to_switch_track", false) + } } private fun showPlaybackSpeedDialog() { @@ -1139,7 +1243,8 @@ class FullBottomSheet ) val secondaryContainerTransition = ValueAnimator.ofArgb( - bottomSheetFullControllerButton.backgroundTintList!!.defaultColor, + bottomSheetFullControllerButtonBg.imageTintList?.defaultColor + ?: colorSecondaryContainer, colorSecondaryContainer ) @@ -1226,7 +1331,7 @@ class FullBottomSheet secondaryContainerTransition.apply { addUpdateListener { animation -> val progressColor = animation.animatedValue as Int - bottomSheetFullControllerButton.backgroundTintList = + bottomSheetFullControllerButtonBg.imageTintList = ColorStateList.valueOf(progressColor) } duration = BACKGROUND_COLOR_TRANSITION_SEC @@ -1350,7 +1455,7 @@ class FullBottomSheet bottomSheetFullSubtitle.setTextColor( colorSecondary ) - bottomSheetFullControllerButton.backgroundTintList = + bottomSheetFullControllerButtonBg.imageTintList = ColorStateList.valueOf(colorSecondaryContainer) bottomSheetFullControllerButton.iconTint = ColorStateList.valueOf(colorOnSecondaryContainer) @@ -1537,10 +1642,14 @@ class FullBottomSheet wrappedContext ?: context, R.drawable.play_anim ) - bottomSheetFullControllerButton.background = + bottomSheetFullControllerButtonBg.setImageDrawable( AppCompatResources.getDrawable(context, R.drawable.bg_play_anim) + ) bottomSheetFullControllerButton.icon.startAnimation() - bottomSheetFullControllerButton.background.startAnimation() + bottomSheetFullControllerButtonBg.drawable?.startAnimation() + val pauseDesc = context.getString(R.string.pause) + bottomSheetFullControllerButton.contentDescription = pauseDesc + TooltipCompat.setTooltipText(bottomSheetFullControllerButton, pauseDesc) bottomSheetFullControllerButton.setTag(R.id.play_next, 1) } if (!isUserTracking) { @@ -1551,6 +1660,7 @@ class FullBottomSheet handler.postDelayed(positionRunnable, SLIDER_UPDATE_INTERVAL) } bottomSheetFullCover.startRotation() + startButtonRotation() } else if (playbackState != Player.STATE_BUFFERING) { if (bottomSheetFullControllerButton.getTag(R.id.play_next) as Int? != 2) { bottomSheetFullControllerButton.icon = @@ -1558,12 +1668,17 @@ class FullBottomSheet wrappedContext ?: context, R.drawable.pause_anim ) - bottomSheetFullControllerButton.background = + bottomSheetFullControllerButtonBg.setImageDrawable( AppCompatResources.getDrawable(context, R.drawable.bg_pause_anim) + ) bottomSheetFullControllerButton.icon.startAnimation() - bottomSheetFullControllerButton.background.startAnimation() + bottomSheetFullControllerButtonBg.drawable?.startAnimation() + val playDesc = context.getString(R.string.play) + bottomSheetFullControllerButton.contentDescription = playDesc + TooltipCompat.setTooltipText(bottomSheetFullControllerButton, playDesc) bottomSheetFullControllerButton.setTag(R.id.play_next, 2) bottomSheetFullCover.stopRotation() + stopButtonRotation(reset = false) } if (!isUserTracking) { progressDrawable.animate = false diff --git a/app/src/main/res/layout-w600dp-land/full_player.xml b/app/src/main/res/layout-w600dp-land/full_player.xml index bd831d0458..5ade7121f6 100644 --- a/app/src/main/res/layout-w600dp-land/full_player.xml +++ b/app/src/main/res/layout-w600dp-land/full_player.xml @@ -1,4 +1,8 @@ + - + app:layout_constraintTop_toBottomOf="@id/duration_frame" /> diff --git a/app/src/main/res/layout/full_player.xml b/app/src/main/res/layout/full_player.xml index 0fa6a1c4a3..f9d62b2246 100644 --- a/app/src/main/res/layout/full_player.xml +++ b/app/src/main/res/layout/full_player.xml @@ -207,7 +207,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="36dp" - app:layout_constraintBottom_toTopOf="@id/sheet_mid_button" + app:layout_constraintBottom_toTopOf="@id/sheet_mid_button_frame" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/slider" @@ -217,7 +217,6 @@ android:id="@+id/position" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="start" android:fontFamily="sans-serif" android:text="@string/default_time" android:textColor="?colorOnSurfaceVariant" @@ -230,7 +229,6 @@ android:id="@+id/quality_details" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="center" android:drawablePadding="4dp" android:fontFamily="sans-serif" android:gravity="center" @@ -250,7 +248,6 @@ android:id="@+id/duration" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="end" android:fontFamily="sans-serif" android:text="@string/default_time" android:textColor="?colorOnSurfaceVariant" @@ -261,37 +258,22 @@ - + app:layout_constraintTop_toBottomOf="@id/duration_frame" /> diff --git a/app/src/main/res/layout/sheet_cookie_button.xml b/app/src/main/res/layout/sheet_cookie_button.xml new file mode 100644 index 0000000000..41ae5553e5 --- /dev/null +++ b/app/src/main/res/layout/sheet_cookie_button.xml @@ -0,0 +1,50 @@ + + + + + + + + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index e560b9b47b..cd861b39cf 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -148,6 +148,7 @@ 移除 更多 播放 + 暂停 排序 全部播放 主屏幕 @@ -157,6 +158,10 @@ 启用魅族状态栏歌词(仅适用于部分设备) 启用新歌词界面 显示当前播放媒体歌词的小部件 + 旋转播放按钮 + 播放时让饼干样式的播放/暂停按钮细微地转动 + 滑动切歌 + 在播放器封面上左右滑动以切换上一曲/下一曲 滚动到专辑 滚动到歌曲 关闭 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0173690f0f..883a9802b7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -374,6 +374,8 @@ Scroll to songs Play + + Pause More @@ -404,6 +406,14 @@ Always skip to previous track Directly switch to previous track when pressing previous button instead of seeking to start + + Rotate play button + + Subtly rotate the cookie-style play/pause button while playing + + Swipe to switch track + + Swipe left or right on the album cover to switch tracks Delete diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml index f83d70ca24..eea66380cc 100644 --- a/app/src/main/res/xml/settings_player.xml +++ b/app/src/main/res/xml/settings_player.xml @@ -85,6 +85,24 @@ android:title="@string/settings_cookie_info" android:widgetLayout="@layout/preference_switch_widget" /> + + + +