diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1b3345f1..f5216398 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,7 +21,7 @@ android { minSdk = 24 targetSdk = 34 versionCode = 10 - versionName = "1.1-beta04" + versionName = "1.1-beta05" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -32,8 +32,6 @@ android { signingConfigs { create("release") { - // You need to specify either an absolute path or include the - // keystore file in the same directory as the build.gradle file. @Suppress("UNCHECKED_CAST") val sign = ((extra["secret"] as Map<*, *>)["sign"] as Map) storeFile = file("../key.jks") diff --git a/app/src/main/java/com/skyd/anivu/model/db/AppDatabase.kt b/app/src/main/java/com/skyd/anivu/model/db/AppDatabase.kt index 9812d7cf..7361a23d 100644 --- a/app/src/main/java/com/skyd/anivu/model/db/AppDatabase.kt +++ b/app/src/main/java/com/skyd/anivu/model/db/AppDatabase.kt @@ -64,6 +64,7 @@ abstract class AppDatabase : RoomDatabase() { ) .addMigrations(*migrations) .build() + .apply { instance = this } } else { instance as AppDatabase } diff --git a/app/src/main/java/com/skyd/anivu/ui/activity/PlayActivity.kt b/app/src/main/java/com/skyd/anivu/ui/activity/PlayActivity.kt index 3e0b55de..42aef769 100644 --- a/app/src/main/java/com/skyd/anivu/ui/activity/PlayActivity.kt +++ b/app/src/main/java/com/skyd/anivu/ui/activity/PlayActivity.kt @@ -1,6 +1,7 @@ package com.skyd.anivu.ui.activity import android.content.Intent +import android.media.MediaMetadataRetriever import android.net.Uri import android.os.Bundle import android.view.WindowManager @@ -16,6 +17,7 @@ import com.skyd.anivu.ext.dataStore import com.skyd.anivu.ext.getOrDefault import com.skyd.anivu.model.preference.player.PlayerShow85sButtonPreference + class PlayActivity : BaseActivity() { companion object { const val VIDEO_URI_KEY = "videoUri" @@ -57,7 +59,11 @@ class PlayActivity : BaseActivity() { override fun ActivityPlayBinding.initView() { playerView.setForward85sButton(dataStore.getOrDefault(PlayerShow85sButtonPreference)) - +// playerView.setOnScreenshotListener { +// val retriever = MediaMetadataRetriever() +// retriever.setDataSource(player.curren) +// val bitmap = retriever.getFrameAtTime(simpleExoPlayer.getCurrentPosition()) +// } videoUri = IntentCompat.getParcelableExtra(intent, VIDEO_URI_KEY, Uri::class.java) ?: intent.data diff --git a/app/src/main/java/com/skyd/anivu/ui/adapter/decoration/AniVuItemDecoration.kt b/app/src/main/java/com/skyd/anivu/ui/adapter/decoration/AniVuItemDecoration.kt index de710d2f..d325ce7b 100644 --- a/app/src/main/java/com/skyd/anivu/ui/adapter/decoration/AniVuItemDecoration.kt +++ b/app/src/main/java/com/skyd/anivu/ui/adapter/decoration/AniVuItemDecoration.kt @@ -141,7 +141,7 @@ class AniVuItemDecoration( } companion object { - val H_ITEM_SPACE: Int = 12.dp + val H_ITEM_SPACE: Int = 16.dp val HORIZONTAL_PADDING: Int = 16.dp val VERTICAL_PADDING: Int = 16.dp diff --git a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/AniSpanSize.kt b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/AniSpanSize.kt index 0ef9b3ea..21bd006d 100644 --- a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/AniSpanSize.kt +++ b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/AniSpanSize.kt @@ -4,6 +4,7 @@ import androidx.recyclerview.widget.GridLayoutManager import com.skyd.anivu.appContext import com.skyd.anivu.ext.screenIsLand import com.skyd.anivu.model.bean.FeedBean +import com.skyd.anivu.model.bean.LicenseBean import com.skyd.anivu.model.bean.MoreBean import com.skyd.anivu.model.bean.OtherWorksBean @@ -17,14 +18,13 @@ class AniSpanSize( fun getSpanSize(data: Any?, enableLandScape: Boolean): Int { return if (enableLandScape && appContext.screenIsLand) { when (data) { - is FeedBean -> MAX_SPAN_SIZE is MoreBean -> MAX_SPAN_SIZE / 3 is OtherWorksBean -> MAX_SPAN_SIZE / 2 + is LicenseBean -> MAX_SPAN_SIZE / 2 else -> MAX_SPAN_SIZE } } else { when (data) { - is FeedBean -> MAX_SPAN_SIZE is MoreBean -> MAX_SPAN_SIZE / 2 else -> MAX_SPAN_SIZE } diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/license/LicenseFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/license/LicenseFragment.kt index 1d46f65c..888b3853 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/license/LicenseFragment.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/license/LicenseFragment.kt @@ -9,8 +9,10 @@ import androidx.recyclerview.widget.GridLayoutManager import com.skyd.anivu.base.BaseFragment import com.skyd.anivu.databinding.FragmentLicenseBinding import com.skyd.anivu.ext.addInsetsByPadding +import com.skyd.anivu.ext.dp import com.skyd.anivu.ext.popBackStackWithLifecycle import com.skyd.anivu.model.bean.LicenseBean +import com.skyd.anivu.ui.adapter.decoration.AniVuItemDecoration import com.skyd.anivu.ui.adapter.variety.AniSpanSize import com.skyd.anivu.ui.adapter.variety.VarietyAdapter import com.skyd.anivu.ui.adapter.variety.proxy.License1Proxy @@ -38,6 +40,7 @@ class LicenseFragment : BaseFragment() { ).apply { spanSizeLookup = AniSpanSize(adapter) } + rvLicenseFragment.addItemDecoration(AniVuItemDecoration()) rvLicenseFragment.adapter = adapter } diff --git a/app/src/main/java/com/skyd/anivu/ui/player/PlayerControlView.java b/app/src/main/java/com/skyd/anivu/ui/player/PlayerControlView.java index 561ecbb6..e5dbcd93 100644 --- a/app/src/main/java/com/skyd/anivu/ui/player/PlayerControlView.java +++ b/app/src/main/java/com/skyd/anivu/ui/player/PlayerControlView.java @@ -364,6 +364,8 @@ public interface OnFullScreenModeChangedListener { @Nullable private final View forward85sView; @Nullable + private final View screenshotView; + @Nullable private final ViewGroup brightnessControlsView; @Nullable private final LinearProgressIndicator brightnessProgressView; @@ -576,6 +578,11 @@ public PlayerControlView( }); } + screenshotView = findViewById(com.skyd.anivu.R.id.exo_screenshot); + if (screenshotView != null) { + if (!screenshotView.hasOnClickListeners()) screenshotView.setVisibility(View.GONE); + } + longPressPlaybackSpeedView = findViewById(com.skyd.anivu.R.id.exo_long_press_playback_speed); if (longPressPlaybackSpeedView != null) { longPressPlaybackSpeedView.setVisibility(View.GONE); @@ -1126,6 +1133,13 @@ public void setForward85sButton(boolean visible) { } } + public void setOnScreenshotListener(@Nullable View.OnClickListener listener) { + if (screenshotView != null) { + screenshotView.setVisibility(listener == null ? View.GONE : View.VISIBLE); + screenshotView.setOnClickListener(listener); + } + } + public void setLongPressPlaybackSpeedVisibility(int visibility) { if (longPressPlaybackSpeedView != null) { longPressPlaybackSpeedView.setVisibility(visibility); @@ -1901,7 +1915,7 @@ public boolean dispatchMediaKeyEvent(KeyEvent event) { @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - controlViewLayoutManager.onLayout(changed, left, top, right, bottom); + controlViewLayoutManager.onLayout(left, top, right, bottom); } private void onLayoutChange( diff --git a/app/src/main/java/com/skyd/anivu/ui/player/PlayerControlViewLayoutManager.java b/app/src/main/java/com/skyd/anivu/ui/player/PlayerControlViewLayoutManager.java deleted file mode 100644 index 9b85af17..00000000 --- a/app/src/main/java/com/skyd/anivu/ui/player/PlayerControlViewLayoutManager.java +++ /dev/null @@ -1,783 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.skyd.anivu.ui.player; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; -import android.annotation.SuppressLint; -import android.content.res.Resources; -import android.view.View; -import android.view.View.OnLayoutChangeListener; -import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; -import android.view.ViewGroup.MarginLayoutParams; -import android.view.animation.LinearInterpolator; - -import androidx.annotation.Nullable; -import androidx.media3.ui.DefaultTimeBar; - -import com.skyd.anivu.R; - -import java.util.ArrayList; -import java.util.List; - -@SuppressLint({"UnsafeOptInUsageError", "PrivateResource"}) -/* package */ final class PlayerControlViewLayoutManager { - private static final long ANIMATION_INTERVAL_MS = 2_000; - private static final long DURATION_FOR_HIDING_ANIMATION_MS = 250; - private static final long DURATION_FOR_SHOWING_ANIMATION_MS = 250; - - // Int for defining the UX state where all the views (ProgressBar, BottomBar) are - // all visible. - private static final int UX_STATE_ALL_VISIBLE = 0; - // Int for defining the UX state where only the ProgressBar view is visible. - private static final int UX_STATE_ONLY_PROGRESS_VISIBLE = 1; - // Int for defining the UX state where none of the views are visible. - private static final int UX_STATE_NONE_VISIBLE = 2; - // Int for defining the UX state where the views are being animated to be hidden. - private static final int UX_STATE_ANIMATING_HIDE = 3; - // Int for defining the UX state where the views are being animated to be shown. - private static final int UX_STATE_ANIMATING_SHOW = 4; - - private final PlayerControlView playerControlView; - - private final ViewGroup autoHiddenControllerView; - @Nullable - private final View controlsBackground; - @Nullable - private final ViewGroup centerControls; - @Nullable - private final ViewGroup bottomBar; - @Nullable - private final ViewGroup topBar; - @Nullable - private final View resetZoomButton; - @Nullable - private final View forward85sButton; - @Nullable - private final ViewGroup minimalControls; - @Nullable - private final ViewGroup basicControls; - @Nullable - private final ViewGroup extraControls; - @Nullable - private final ViewGroup extraControlsScrollView; - @Nullable - private final ViewGroup timeView; - @Nullable - private final View timeBar; - @Nullable - private final View overflowShowButton; - - private final AnimatorSet hideMainBarAnimator; - private final AnimatorSet hideProgressBarAnimator; - private final AnimatorSet hideAllBarsAnimator; - private final AnimatorSet showMainBarAnimator; - private final AnimatorSet showAllBarsAnimator; - private final ValueAnimator overflowShowAnimator; - private final ValueAnimator overflowHideAnimator; - - private final Runnable showAllBarsRunnable; - private final Runnable hideAllBarsRunnable; - private final Runnable hideProgressBarRunnable; - private final Runnable hideMainBarRunnable; - private final Runnable hideControllerRunnable; - private final OnLayoutChangeListener onLayoutChangeListener; - - private final List shownButtons; - - private int uxState; - private boolean isMinimalMode; - private boolean needToShowBars; - private boolean animationEnabled; - - @SuppressWarnings({"nullness:method.invocation", "nullness:methodref.receiver.bound"}) - public PlayerControlViewLayoutManager(PlayerControlView playerControlView) { - this.playerControlView = playerControlView; - showAllBarsRunnable = this::showAllBars; - hideAllBarsRunnable = this::hideAllBars; - hideProgressBarRunnable = this::hideProgressBar; - hideMainBarRunnable = this::hideMainBar; - hideControllerRunnable = this::hideController; - onLayoutChangeListener = this::onLayoutChange; - animationEnabled = true; - uxState = UX_STATE_ALL_VISIBLE; - shownButtons = new ArrayList<>(); - - // 可以自动隐藏的ViewGroup - autoHiddenControllerView = playerControlView.findViewById(com.skyd.anivu.R.id.exo_auto_hidden_control_view); - - // Relating to Center View - controlsBackground = playerControlView.findViewById(androidx.media3.ui.R.id.exo_controls_background); - centerControls = playerControlView.findViewById(androidx.media3.ui.R.id.exo_center_controls); - - // Relating to Minimal Layout - minimalControls = playerControlView.findViewById(androidx.media3.ui.R.id.exo_minimal_controls); - - // Relating to Bottom Bar View - bottomBar = playerControlView.findViewById(androidx.media3.ui.R.id.exo_bottom_bar); - - // Relating to Top Bar View - topBar = playerControlView.findViewById(com.skyd.anivu.R.id.exo_top_bar); - - // Relating to Reset Zoom View - resetZoomButton = playerControlView.findViewById(R.id.exo_reset_zoom); - - // Relating to Forward 85s View - forward85sButton = playerControlView.findViewById(R.id.exo_forward_85s); - - // Relating to Bottom Bar Left View - timeView = playerControlView.findViewById(androidx.media3.ui.R.id.exo_time); - timeBar = playerControlView.findViewById(androidx.media3.ui.R.id.exo_progress); - - // Relating to Bottom Bar Right View - basicControls = playerControlView.findViewById(androidx.media3.ui.R.id.exo_basic_controls); - extraControls = playerControlView.findViewById(androidx.media3.ui.R.id.exo_extra_controls); - extraControlsScrollView = playerControlView.findViewById(androidx.media3.ui.R.id.exo_extra_controls_scroll_view); - overflowShowButton = playerControlView.findViewById(androidx.media3.ui.R.id.exo_overflow_show); - View overflowHideButton = playerControlView.findViewById(androidx.media3.ui.R.id.exo_overflow_hide); - if (overflowShowButton != null && overflowHideButton != null) { - overflowShowButton.setOnClickListener(this::onOverflowButtonClick); - overflowHideButton.setOnClickListener(this::onOverflowButtonClick); - } - - ValueAnimator fadeOutAnimator = ValueAnimator.ofFloat(1.0f, 0.0f); - fadeOutAnimator.setInterpolator(new LinearInterpolator()); - fadeOutAnimator.addUpdateListener( - animation -> { - float animatedValue = (float) animation.getAnimatedValue(); - if (controlsBackground != null) { - controlsBackground.setAlpha(animatedValue); - } - if (centerControls != null) { - centerControls.setAlpha(animatedValue); - } - if (minimalControls != null) { - minimalControls.setAlpha(animatedValue); - } - }); - fadeOutAnimator.addListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - if (timeBar instanceof DefaultTimeBar && !isMinimalMode) { - ((DefaultTimeBar) timeBar).hideScrubber(DURATION_FOR_HIDING_ANIMATION_MS); - } - } - - @Override - public void onAnimationEnd(Animator animation) { - if (controlsBackground != null) { - controlsBackground.setVisibility(View.INVISIBLE); - } - if (centerControls != null) { - centerControls.setVisibility(View.INVISIBLE); - } - if (minimalControls != null) { - minimalControls.setVisibility(View.INVISIBLE); - } - } - }); - - ValueAnimator fadeInAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); - fadeInAnimator.setInterpolator(new LinearInterpolator()); - fadeInAnimator.addUpdateListener( - animation -> { - float animatedValue = (float) animation.getAnimatedValue(); - if (controlsBackground != null) { - controlsBackground.setAlpha(animatedValue); - } - if (centerControls != null) { - centerControls.setAlpha(animatedValue); - } - if (minimalControls != null) { - minimalControls.setAlpha(animatedValue); - } - }); - fadeInAnimator.addListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - if (controlsBackground != null) { - controlsBackground.setVisibility(View.VISIBLE); - } - if (centerControls != null) { - centerControls.setVisibility(View.VISIBLE); - } - if (minimalControls != null) { - minimalControls.setVisibility(isMinimalMode ? View.VISIBLE : View.INVISIBLE); - } - if (timeBar instanceof DefaultTimeBar && !isMinimalMode) { - ((DefaultTimeBar) timeBar).showScrubber(DURATION_FOR_SHOWING_ANIMATION_MS); - } - } - }); - - Resources resources = playerControlView.getResources(); - float translationYForProgressBar = - resources.getDimension(androidx.media3.ui.R.dimen.exo_styled_bottom_bar_height) - - resources.getDimension(androidx.media3.ui.R.dimen.exo_styled_progress_bar_height); - float translationYForNoBars = resources.getDimension(androidx.media3.ui.R.dimen.exo_styled_bottom_bar_height); - - hideMainBarAnimator = new AnimatorSet(); - hideMainBarAnimator.setDuration(DURATION_FOR_HIDING_ANIMATION_MS); - hideMainBarAnimator.addListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - setUxState(UX_STATE_ANIMATING_HIDE); - } - - @Override - public void onAnimationEnd(Animator animation) { - setUxState(UX_STATE_ONLY_PROGRESS_VISIBLE); - if (needToShowBars) { - playerControlView.post(showAllBarsRunnable); - needToShowBars = false; - } - } - }); - hideMainBarAnimator - .play(fadeOutAnimator) - .with(ofAlpha(1, 0, timeBar)) - .with(ofAlpha(1, 0, bottomBar)) - .with(ofAlpha(1, 0, topBar)) - .with(ofAlpha(1, 0, resetZoomButton)) - .with(ofAlpha(1, 0, forward85sButton)); - - hideProgressBarAnimator = new AnimatorSet(); - hideProgressBarAnimator.setDuration(DURATION_FOR_HIDING_ANIMATION_MS); - hideProgressBarAnimator.addListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - setUxState(UX_STATE_ANIMATING_HIDE); - } - - @Override - public void onAnimationEnd(Animator animation) { - setUxState(UX_STATE_NONE_VISIBLE); - if (needToShowBars) { - playerControlView.post(showAllBarsRunnable); - needToShowBars = false; - } - } - }); - hideProgressBarAnimator - .play(ofAlpha(1, 0, timeBar)) - .with(ofAlpha(1, 0, bottomBar)) - .with(ofAlpha(1, 0, topBar)) - .with(ofAlpha(1, 0, resetZoomButton)) - .with(ofAlpha(1, 0, forward85sButton)); - - hideAllBarsAnimator = new AnimatorSet(); - hideAllBarsAnimator.setDuration(DURATION_FOR_HIDING_ANIMATION_MS); - hideAllBarsAnimator.addListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - setUxState(UX_STATE_ANIMATING_HIDE); - } - - @Override - public void onAnimationEnd(Animator animation) { - setUxState(UX_STATE_NONE_VISIBLE); - if (needToShowBars) { - playerControlView.post(showAllBarsRunnable); - needToShowBars = false; - } - } - }); - hideAllBarsAnimator - .play(fadeOutAnimator) - .with(ofAlpha(1, 0, timeBar)) - .with(ofAlpha(1, 0, bottomBar)) - .with(ofAlpha(1, 0, topBar)) - .with(ofAlpha(1, 0, resetZoomButton)) - .with(ofAlpha(1, 0, forward85sButton)); - - showMainBarAnimator = new AnimatorSet(); - showMainBarAnimator.setDuration(DURATION_FOR_SHOWING_ANIMATION_MS); - showMainBarAnimator.addListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - setUxState(UX_STATE_ANIMATING_SHOW); - } - - @Override - public void onAnimationEnd(Animator animation) { - setUxState(UX_STATE_ALL_VISIBLE); - } - }); - showMainBarAnimator - .play(fadeInAnimator) - .with(ofAlpha(0, 1, timeBar)) - .with(ofAlpha(0, 1, bottomBar)) - .with(ofAlpha(0, 1, topBar)) - .with(ofAlpha(0, 1, resetZoomButton)) - .with(ofAlpha(0, 1, forward85sButton)); - - showAllBarsAnimator = new AnimatorSet(); - showAllBarsAnimator.setDuration(DURATION_FOR_SHOWING_ANIMATION_MS); - showAllBarsAnimator.addListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - setUxState(UX_STATE_ANIMATING_SHOW); - } - - @Override - public void onAnimationEnd(Animator animation) { - setUxState(UX_STATE_ALL_VISIBLE); - } - }); - showAllBarsAnimator - .play(fadeInAnimator) - .with(ofAlpha(0, 1, timeBar)) - .with(ofAlpha(0, 1, bottomBar)) - .with(ofAlpha(0, 1, topBar)) - .with(ofAlpha(0, 1, resetZoomButton)) - .with(ofAlpha(0, 1, forward85sButton)); - - overflowShowAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); - overflowShowAnimator.setDuration(DURATION_FOR_SHOWING_ANIMATION_MS); - overflowShowAnimator.addUpdateListener( - animation -> animateOverflow((float) animation.getAnimatedValue())); - overflowShowAnimator.addListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - if (extraControlsScrollView != null) { - extraControlsScrollView.setVisibility(View.VISIBLE); - extraControlsScrollView.setTranslationX(extraControlsScrollView.getWidth()); - extraControlsScrollView.scrollTo(extraControlsScrollView.getWidth(), 0); - } - } - - @Override - public void onAnimationEnd(Animator animation) { - if (basicControls != null) { - basicControls.setVisibility(View.INVISIBLE); - } - } - }); - - overflowHideAnimator = ValueAnimator.ofFloat(1.0f, 0.0f); - overflowHideAnimator.setDuration(DURATION_FOR_SHOWING_ANIMATION_MS); - overflowHideAnimator.addUpdateListener( - animation -> animateOverflow((float) animation.getAnimatedValue())); - overflowHideAnimator.addListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - if (basicControls != null) { - basicControls.setVisibility(View.VISIBLE); - } - } - - @Override - public void onAnimationEnd(Animator animation) { - if (extraControlsScrollView != null) { - extraControlsScrollView.setVisibility(View.INVISIBLE); - } - } - }); - } - - public void show() { - if (!playerControlView.isAutoHiddenControllerVisible()) { - autoHiddenControllerView.setVisibility(View.VISIBLE); - playerControlView.updateAll(); - playerControlView.requestPlayPauseFocus(); - } - showAllBars(); - } - - public void hide() { - if (uxState == UX_STATE_ANIMATING_HIDE || uxState == UX_STATE_NONE_VISIBLE) { - return; - } - removeHideCallbacks(); - if (!animationEnabled) { - hideController(); - } else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) { - hideProgressBar(); - } else { - hideAllBars(); - } - } - - public void hideImmediately() { - if (uxState == UX_STATE_ANIMATING_HIDE || uxState == UX_STATE_NONE_VISIBLE) { - return; - } - removeHideCallbacks(); - hideController(); - } - - public void setAnimationEnabled(boolean animationEnabled) { - this.animationEnabled = animationEnabled; - } - - public boolean isAnimationEnabled() { - return animationEnabled; - } - - public void resetHideCallbacks() { - if (uxState == UX_STATE_ANIMATING_HIDE) { - return; - } - removeHideCallbacks(); - int showTimeoutMs = playerControlView.getShowTimeoutMs(); - if (showTimeoutMs > 0) { - if (!animationEnabled) { - postDelayedRunnable(hideControllerRunnable, showTimeoutMs); - } else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) { - postDelayedRunnable(hideProgressBarRunnable, ANIMATION_INTERVAL_MS); - } else { - postDelayedRunnable(hideMainBarRunnable, showTimeoutMs); - } - } - } - - public void removeHideCallbacks() { - playerControlView.removeCallbacks(hideControllerRunnable); - playerControlView.removeCallbacks(hideAllBarsRunnable); - playerControlView.removeCallbacks(hideMainBarRunnable); - playerControlView.removeCallbacks(hideProgressBarRunnable); - } - - public void onAttachedToWindow() { - playerControlView.addOnLayoutChangeListener(onLayoutChangeListener); - } - - public void onDetachedFromWindow() { - playerControlView.removeOnLayoutChangeListener(onLayoutChangeListener); - } - - public boolean isFullyVisible() { - return uxState == UX_STATE_ALL_VISIBLE && playerControlView.isAutoHiddenControllerVisible(); - } - - public void setShowButton(@Nullable View button, boolean showButton) { - if (button == null) { - return; - } - if (!showButton) { - button.setVisibility(View.GONE); - shownButtons.remove(button); - return; - } - if (isMinimalMode && shouldHideInMinimalMode(button)) { - button.setVisibility(View.INVISIBLE); - } else { - button.setVisibility(View.VISIBLE); - } - shownButtons.add(button); - } - - public boolean getShowButton(@Nullable View button) { - return button != null && shownButtons.contains(button); - } - - private void setUxState(int uxState) { - int prevUxState = this.uxState; - this.uxState = uxState; - if (uxState == UX_STATE_NONE_VISIBLE) { - autoHiddenControllerView.setVisibility(View.GONE); - } else if (prevUxState == UX_STATE_NONE_VISIBLE) { - autoHiddenControllerView.setVisibility(View.VISIBLE); - } - // TODO(insun): Notify specific uxState. Currently reuses legacy visibility listener for API - // compatibility. - if (prevUxState != uxState) { - playerControlView.notifyOnVisibilityChange(); - } - } - - public void onLayout(boolean changed, int left, int top, int right, int bottom) { - if (controlsBackground != null) { - // The background view should occupy the entirety of the parent. This is done in code rather - // than in layout XML to stop the background view from influencing the size of the parent if - // it uses "wrap_content". See: https://github.com/google/ExoPlayer/issues/8726. - controlsBackground.layout(0, 0, right - left, bottom - top); - } - } - - private void onLayoutChange( - View v, - int left, - int top, - int right, - int bottom, - int oldLeft, - int oldTop, - int oldRight, - int oldBottom) { - - boolean useMinimalMode = useMinimalMode(); - if (isMinimalMode != useMinimalMode) { - isMinimalMode = useMinimalMode; - v.post(this::updateLayoutForSizeChange); - } - boolean widthChanged = (right - left) != (oldRight - oldLeft); - if (!isMinimalMode && widthChanged) { - v.post(this::onLayoutWidthChanged); - } - } - - private void onOverflowButtonClick(View v) { - resetHideCallbacks(); - if (v.getId() == androidx.media3.ui.R.id.exo_overflow_show) { - overflowShowAnimator.start(); - } else if (v.getId() == androidx.media3.ui.R.id.exo_overflow_hide) { - overflowHideAnimator.start(); - } - } - - private void showAllBars() { - if (!animationEnabled) { - setUxState(UX_STATE_ALL_VISIBLE); - resetHideCallbacks(); - return; - } - - switch (uxState) { - case UX_STATE_NONE_VISIBLE -> showAllBarsAnimator.start(); - case UX_STATE_ONLY_PROGRESS_VISIBLE -> showMainBarAnimator.start(); - case UX_STATE_ANIMATING_HIDE -> needToShowBars = true; - case UX_STATE_ANIMATING_SHOW -> { - return; - } - default -> { - } - } - resetHideCallbacks(); - } - - private void hideAllBars() { - hideAllBarsAnimator.start(); - } - - private void hideProgressBar() { - hideProgressBarAnimator.start(); - } - - private void hideMainBar() { - hideMainBarAnimator.start(); - postDelayedRunnable(hideProgressBarRunnable, ANIMATION_INTERVAL_MS); - } - - private void hideController() { - setUxState(UX_STATE_NONE_VISIBLE); - } - - private static ObjectAnimator ofTranslationY(float startValue, float endValue, View target) { - return ObjectAnimator.ofFloat(target, "translationY", startValue, endValue); - } - - private static ObjectAnimator ofAlpha(float startValue, float endValue, View target) { - return ObjectAnimator.ofFloat(target, "alpha", startValue, endValue); - } - - private void postDelayedRunnable(Runnable runnable, long interval) { - if (interval >= 0) { - playerControlView.postDelayed(runnable, interval); - } - } - - private void animateOverflow(float animatedValue) { - if (extraControlsScrollView != null) { - int extraControlTranslationX = - (int) (extraControlsScrollView.getWidth() * (1 - animatedValue)); - extraControlsScrollView.setTranslationX(extraControlTranslationX); - } - - if (timeView != null) { - timeView.setAlpha(1 - animatedValue); - } - if (basicControls != null) { - basicControls.setAlpha(1 - animatedValue); - } - } - - private boolean useMinimalMode() { - int width = - playerControlView.getWidth() - - playerControlView.getPaddingLeft() - - playerControlView.getPaddingRight(); - int height = - playerControlView.getHeight() - - playerControlView.getPaddingBottom() - - playerControlView.getPaddingTop(); - - int centerControlWidth = - getWidthWithMargins(centerControls) - - (centerControls != null - ? (centerControls.getPaddingLeft() + centerControls.getPaddingRight()) - : 0); - int centerControlHeight = - getHeightWithMargins(centerControls) - - (centerControls != null - ? (centerControls.getPaddingTop() + centerControls.getPaddingBottom()) - : 0); - - int defaultModeMinimumWidth = - Math.max( - centerControlWidth, - getWidthWithMargins(timeView) + getWidthWithMargins(overflowShowButton)); - int defaultModeMinimumHeight = centerControlHeight + getHeightWithMargins(bottomBar) - + getHeightWithMargins(topBar)/* + (2 * getHeightWithMargins(bottomBar))*/; - - return width <= defaultModeMinimumWidth || height <= defaultModeMinimumHeight; - } - - private void updateLayoutForSizeChange() { - if (minimalControls != null) { - minimalControls.setVisibility(isMinimalMode ? View.VISIBLE : View.INVISIBLE); - } - - if (timeBar != null) { - int timeBarMarginBottom = - playerControlView - .getResources() - .getDimensionPixelSize(androidx.media3.ui.R.dimen.exo_styled_progress_margin_bottom); - @Nullable MarginLayoutParams timeBarParams = (MarginLayoutParams) timeBar.getLayoutParams(); - if (timeBarParams != null) { - timeBarParams.bottomMargin = (isMinimalMode ? 0 : timeBarMarginBottom); - timeBar.setLayoutParams(timeBarParams); - } - if (timeBar instanceof DefaultTimeBar defaultTimeBar) { - if (isMinimalMode) { - defaultTimeBar.hideScrubber(/* disableScrubberPadding= */ true); - } else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) { - defaultTimeBar.hideScrubber(/* disableScrubberPadding= */ false); - } else if (uxState != UX_STATE_ANIMATING_HIDE) { - defaultTimeBar.showScrubber(); - } - } - } - - for (View v : shownButtons) { - v.setVisibility(isMinimalMode && shouldHideInMinimalMode(v) ? View.INVISIBLE : View.VISIBLE); - } - } - - private boolean shouldHideInMinimalMode(View button) { - int id = button.getId(); - return (id == androidx.media3.ui.R.id.exo_bottom_bar - || id == androidx.media3.ui.R.id.exo_prev - || id == androidx.media3.ui.R.id.exo_next - || id == androidx.media3.ui.R.id.exo_rew - || id == androidx.media3.ui.R.id.exo_rew_with_amount - || id == androidx.media3.ui.R.id.exo_ffwd - || id == androidx.media3.ui.R.id.exo_ffwd_with_amount); - } - - private void onLayoutWidthChanged() { - if (basicControls == null || extraControls == null) { - return; - } - - int width = - playerControlView.getWidth() - - playerControlView.getPaddingLeft() - - playerControlView.getPaddingRight(); - - // Reset back to all controls being basic controls and the overflow not being needed. The last - // child of extraControls is the overflow hide button, which shouldn't be moved back. - while (extraControls.getChildCount() > 1) { - int controlViewIndex = extraControls.getChildCount() - 2; - View controlView = extraControls.getChildAt(controlViewIndex); - extraControls.removeViewAt(controlViewIndex); - basicControls.addView(controlView, /* index= */ 0); - } - if (overflowShowButton != null) { - overflowShowButton.setVisibility(View.GONE); - } - - // Calculate how much of the available width is occupied. The last child of basicControls is the - // overflow show button, which we're currently assuming will not be visible. - int occupiedWidth = getWidthWithMargins(timeView); - int endIndex = basicControls.getChildCount() - 1; - for (int i = 0; i < endIndex; i++) { - View controlView = basicControls.getChildAt(i); - occupiedWidth += getWidthWithMargins(controlView); - } - - if (occupiedWidth > width) { - // We need to move some controls to extraControls. - if (overflowShowButton != null) { - overflowShowButton.setVisibility(View.VISIBLE); - occupiedWidth += getWidthWithMargins(overflowShowButton); - } - ArrayList controlsToMove = new ArrayList<>(); - // The last child of basicControls is the overflow show button, which shouldn't be moved. - for (int i = 0; i < endIndex; i++) { - View control = basicControls.getChildAt(i); - occupiedWidth -= getWidthWithMargins(control); - controlsToMove.add(control); - if (occupiedWidth <= width) { - break; - } - } - if (!controlsToMove.isEmpty()) { - basicControls.removeViews(/* start= */ 0, controlsToMove.size()); - for (int i = 0; i < controlsToMove.size(); i++) { - // The last child of extraControls is the overflow hide button. Add controls before it. - int index = extraControls.getChildCount() - 1; - extraControls.addView(controlsToMove.get(i), index); - } - } - } else { - // If extraControls are visible, hide them since they're now empty. - if (extraControlsScrollView != null - && extraControlsScrollView.getVisibility() == View.VISIBLE - && !overflowHideAnimator.isStarted()) { - overflowShowAnimator.cancel(); - overflowHideAnimator.start(); - } - } - } - - private static int getWidthWithMargins(@Nullable View v) { - if (v == null) { - return 0; - } - int width = v.getWidth(); - LayoutParams layoutParams = v.getLayoutParams(); - if (layoutParams instanceof MarginLayoutParams marginLayoutParams) { - width += marginLayoutParams.leftMargin + marginLayoutParams.rightMargin; - } - return width; - } - - private static int getHeightWithMargins(@Nullable View v) { - if (v == null) { - return 0; - } - int height = v.getHeight(); - LayoutParams layoutParams = v.getLayoutParams(); - if (layoutParams instanceof MarginLayoutParams marginLayoutParams) { - height += marginLayoutParams.topMargin + marginLayoutParams.bottomMargin; - } - return height; - } -} diff --git a/app/src/main/java/com/skyd/anivu/ui/player/PlayerControlViewLayoutManager.kt b/app/src/main/java/com/skyd/anivu/ui/player/PlayerControlViewLayoutManager.kt new file mode 100644 index 00000000..249d5259 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/player/PlayerControlViewLayoutManager.kt @@ -0,0 +1,604 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.skyd.anivu.ui.player + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.animation.ValueAnimator +import android.annotation.SuppressLint +import android.view.View +import android.view.View.OnLayoutChangeListener +import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams +import android.view.animation.LinearInterpolator +import androidx.media3.ui.DefaultTimeBar +import com.skyd.anivu.R +import kotlin.math.max + +@SuppressLint("UnsafeOptInUsageError", "PrivateResource") +internal class PlayerControlViewLayoutManager(private val playerControlView: PlayerControlView) { + // ViewGroup that can be automatically hidden + private val autoHiddenControllerView: ViewGroup = + playerControlView.findViewById(R.id.exo_auto_hidden_control_view) + + // Relating to Center View + private val controlsBackground: View? = + playerControlView.findViewById(androidx.media3.ui.R.id.exo_controls_background) + private val centerControls: ViewGroup? = + playerControlView.findViewById(androidx.media3.ui.R.id.exo_center_controls) + + // Relating to Bottom Bar View + private val bottomBar: ViewGroup? = + playerControlView.findViewById(androidx.media3.ui.R.id.exo_bottom_bar) + + // Relating to Top Bar View + private val topBar: ViewGroup? = playerControlView.findViewById(R.id.exo_top_bar) + + // Relating to Reset Zoom View + private val resetZoomButton: View? = playerControlView.findViewById(R.id.exo_reset_zoom) + + // Relating to Forward 85s View + private val forward85sButton: View? = playerControlView.findViewById(R.id.exo_forward_85s) + + // Relating to Minimal Layout + private val minimalControls: ViewGroup? = + playerControlView.findViewById(androidx.media3.ui.R.id.exo_minimal_controls) + + // Relating to Bottom Bar Right View + private val basicControls: ViewGroup? = + playerControlView.findViewById(androidx.media3.ui.R.id.exo_basic_controls) + private val extraControls: ViewGroup? = + playerControlView.findViewById(androidx.media3.ui.R.id.exo_extra_controls) + private val extraControlsScrollView: ViewGroup? = + playerControlView.findViewById(androidx.media3.ui.R.id.exo_extra_controls_scroll_view) + private val overflowHideButton = + playerControlView.findViewById(androidx.media3.ui.R.id.exo_overflow_hide) + + // Relating to Bottom Bar Left View + private val timeView: ViewGroup? = + playerControlView.findViewById(androidx.media3.ui.R.id.exo_time) + private val timeBar: View? = + playerControlView.findViewById(androidx.media3.ui.R.id.exo_progress) + + private val overflowShowButton: View? = + playerControlView.findViewById(androidx.media3.ui.R.id.exo_overflow_show) + private val hideMainBarAnimator: AnimatorSet = AnimatorSet() + private val hideProgressBarAnimator: AnimatorSet = AnimatorSet() + private val hideAllBarsAnimator: AnimatorSet = AnimatorSet() + private val showMainBarAnimator: AnimatorSet = AnimatorSet() + private val showAllBarsAnimator: AnimatorSet = AnimatorSet() + private val overflowShowAnimator: ValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f) + private val overflowHideAnimator: ValueAnimator = ValueAnimator.ofFloat(1.0f, 0.0f) + private val showAllBarsRunnable: Runnable = Runnable { showAllBars() } + private val hideAllBarsRunnable: Runnable = Runnable { hideAllBars() } + private val hideProgressBarRunnable: Runnable = Runnable { hideProgressBar() } + private val hideMainBarRunnable: Runnable = Runnable { hideMainBar() } + private val hideControllerRunnable: Runnable = Runnable { hideController() } + private val onLayoutChangeListener: OnLayoutChangeListener = + OnLayoutChangeListener { v: View, left: Int, _: Int, right: Int, _: Int, + oldLeft: Int, _: Int, oldRight: Int, _: Int -> + onLayoutChange(v, left, right, oldLeft, oldRight) + } + private val shownButtons: MutableList = mutableListOf() + private var uxState: Int = UX_STATE_ALL_VISIBLE + private var isMinimalMode = false + private var needToShowBars = false + var isAnimationEnabled: Boolean = true + + init { + if (overflowShowButton != null && overflowHideButton != null) { + overflowShowButton.setOnClickListener(::onOverflowButtonClick) + overflowHideButton.setOnClickListener(::onOverflowButtonClick) + } + + val fadeOutAnimator = ValueAnimator.ofFloat(1.0f, 0.0f) + fadeOutAnimator.interpolator = LinearInterpolator() + fadeOutAnimator.addUpdateListener { animation: ValueAnimator -> + val animatedValue = animation.getAnimatedValue() as Float + controlsBackground?.setAlpha(animatedValue) + centerControls?.setAlpha(animatedValue) + minimalControls?.setAlpha(animatedValue) + } + fadeOutAnimator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + if (timeBar is DefaultTimeBar && !isMinimalMode) { + timeBar.hideScrubber(DURATION_FOR_HIDING_ANIMATION_MS) + } + } + + override fun onAnimationEnd(animation: Animator) { + controlsBackground?.visibility = View.INVISIBLE + centerControls?.visibility = View.INVISIBLE + minimalControls?.visibility = View.INVISIBLE + } + }) + + val fadeInAnimator = ValueAnimator.ofFloat(0.0f, 1.0f) + fadeInAnimator.interpolator = LinearInterpolator() + fadeInAnimator.addUpdateListener { animation -> + val animatedValue = animation.getAnimatedValue() as Float + controlsBackground?.setAlpha(animatedValue) + centerControls?.setAlpha(animatedValue) + minimalControls?.setAlpha(animatedValue) + } + fadeInAnimator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + controlsBackground?.visibility = View.VISIBLE + centerControls?.visibility = View.VISIBLE + minimalControls?.visibility = if (isMinimalMode) View.VISIBLE else View.INVISIBLE + if (timeBar is DefaultTimeBar && !isMinimalMode) { + timeBar.showScrubber(DURATION_FOR_SHOWING_ANIMATION_MS) + } + } + }) + + hideMainBarAnimator.setDuration(DURATION_FOR_HIDING_ANIMATION_MS) + hideMainBarAnimator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) = setUxState(UX_STATE_ANIMATING_HIDE) + + override fun onAnimationEnd(animation: Animator) { + setUxState(UX_STATE_ONLY_PROGRESS_VISIBLE) + if (needToShowBars) { + playerControlView.post(showAllBarsRunnable) + needToShowBars = false + } + } + }) + hideMainBarAnimator + .play(fadeOutAnimator) + .with(ofAlpha(1f, 0f, timeBar)) + .with(ofAlpha(1f, 0f, bottomBar)) + .with(ofAlpha(1f, 0f, topBar)) + .with(ofAlpha(1f, 0f, resetZoomButton)) + .with(ofAlpha(1f, 0f, forward85sButton)) + + hideProgressBarAnimator.setDuration(DURATION_FOR_HIDING_ANIMATION_MS) + hideProgressBarAnimator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) = setUxState(UX_STATE_ANIMATING_HIDE) + + override fun onAnimationEnd(animation: Animator) { + setUxState(UX_STATE_NONE_VISIBLE) + if (needToShowBars) { + playerControlView.post(showAllBarsRunnable) + needToShowBars = false + } + } + }) + hideProgressBarAnimator + .play(ofAlpha(1f, 0f, timeBar)) + .with(ofAlpha(1f, 0f, bottomBar)) + .with(ofAlpha(1f, 0f, topBar)) + .with(ofAlpha(1f, 0f, resetZoomButton)) + .with(ofAlpha(1f, 0f, forward85sButton)) + + hideAllBarsAnimator.setDuration(DURATION_FOR_HIDING_ANIMATION_MS) + hideAllBarsAnimator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) = setUxState(UX_STATE_ANIMATING_HIDE) + + override fun onAnimationEnd(animation: Animator) { + setUxState(UX_STATE_NONE_VISIBLE) + if (needToShowBars) { + playerControlView.post(showAllBarsRunnable) + needToShowBars = false + } + } + }) + hideAllBarsAnimator + .play(fadeOutAnimator) + .with(ofAlpha(1f, 0f, timeBar)) + .with(ofAlpha(1f, 0f, bottomBar)) + .with(ofAlpha(1f, 0f, topBar)) + .with(ofAlpha(1f, 0f, resetZoomButton)) + .with(ofAlpha(1f, 0f, forward85sButton)) + + showMainBarAnimator.setDuration(DURATION_FOR_SHOWING_ANIMATION_MS) + showMainBarAnimator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) = setUxState(UX_STATE_ANIMATING_SHOW) + override fun onAnimationEnd(animation: Animator) = setUxState(UX_STATE_ALL_VISIBLE) + }) + showMainBarAnimator + .play(fadeInAnimator) + .with(ofAlpha(0f, 1f, timeBar)) + .with(ofAlpha(0f, 1f, bottomBar)) + .with(ofAlpha(0f, 1f, topBar)) + .with(ofAlpha(0f, 1f, resetZoomButton)) + .with(ofAlpha(0f, 1f, forward85sButton)) + + showAllBarsAnimator.setDuration(DURATION_FOR_SHOWING_ANIMATION_MS) + showAllBarsAnimator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) = setUxState(UX_STATE_ANIMATING_SHOW) + override fun onAnimationEnd(animation: Animator) = setUxState(UX_STATE_ALL_VISIBLE) + }) + showAllBarsAnimator + .play(fadeInAnimator) + .with(ofAlpha(0f, 1f, timeBar)) + .with(ofAlpha(0f, 1f, bottomBar)) + .with(ofAlpha(0f, 1f, topBar)) + .with(ofAlpha(0f, 1f, resetZoomButton)) + .with(ofAlpha(0f, 1f, forward85sButton)) + + overflowShowAnimator.setDuration(DURATION_FOR_SHOWING_ANIMATION_MS) + overflowShowAnimator.addUpdateListener { animation -> + animateOverflow(animation.getAnimatedValue() as Float) + } + overflowShowAnimator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + extraControlsScrollView?.apply { + visibility = View.VISIBLE + translationX = width.toFloat() + scrollTo(width, 0) + } + } + + override fun onAnimationEnd(animation: Animator) { + basicControls?.visibility = View.INVISIBLE + } + }) + + overflowHideAnimator.setDuration(DURATION_FOR_SHOWING_ANIMATION_MS) + overflowHideAnimator.addUpdateListener { animation -> + animateOverflow(animation.getAnimatedValue() as Float) + } + overflowHideAnimator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + basicControls?.visibility = View.VISIBLE + } + + override fun onAnimationEnd(animation: Animator) { + extraControlsScrollView?.visibility = View.INVISIBLE + } + }) + } + + fun show() { + if (!playerControlView.isAutoHiddenControllerVisible) { + autoHiddenControllerView.visibility = View.VISIBLE + playerControlView.updateAll() + playerControlView.requestPlayPauseFocus() + } + showAllBars() + } + + fun hide() { + if (uxState == UX_STATE_ANIMATING_HIDE || uxState == UX_STATE_NONE_VISIBLE) { + return + } + removeHideCallbacks() + if (!isAnimationEnabled) { + hideController() + } else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) { + hideProgressBar() + } else { + hideAllBars() + } + } + + fun hideImmediately() { + if (uxState == UX_STATE_ANIMATING_HIDE || uxState == UX_STATE_NONE_VISIBLE) { + return + } + removeHideCallbacks() + hideController() + } + + fun resetHideCallbacks() { + if (uxState == UX_STATE_ANIMATING_HIDE) { + return + } + removeHideCallbacks() + val showTimeoutMs = playerControlView.showTimeoutMs + if (showTimeoutMs > 0) { + if (!isAnimationEnabled) { + postDelayedRunnable(hideControllerRunnable, showTimeoutMs.toLong()) + } else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) { + postDelayedRunnable(hideProgressBarRunnable, ANIMATION_INTERVAL_MS) + } else { + postDelayedRunnable(hideMainBarRunnable, showTimeoutMs.toLong()) + } + } + } + + fun removeHideCallbacks() { + playerControlView.apply { + removeCallbacks(hideControllerRunnable) + removeCallbacks(hideAllBarsRunnable) + removeCallbacks(hideMainBarRunnable) + removeCallbacks(hideProgressBarRunnable) + } + } + + fun onAttachedToWindow() { + playerControlView.addOnLayoutChangeListener(onLayoutChangeListener) + } + + fun onDetachedFromWindow() { + playerControlView.removeOnLayoutChangeListener(onLayoutChangeListener) + } + + val isFullyVisible: Boolean + get() = uxState == UX_STATE_ALL_VISIBLE && playerControlView.isAutoHiddenControllerVisible + + fun setShowButton(button: View?, showButton: Boolean) { + button ?: return + if (!showButton) { + button.visibility = View.GONE + shownButtons.remove(button) + return + } + button.visibility = if (isMinimalMode && shouldHideInMinimalMode(button)) { + View.INVISIBLE + } else { + View.VISIBLE + } + shownButtons.add(button) + } + + fun getShowButton(button: View?): Boolean { + return button != null && shownButtons.contains(button) + } + + private fun setUxState(uxState: Int) { + val prevUxState = this.uxState + this.uxState = uxState + if (uxState == UX_STATE_NONE_VISIBLE) { + autoHiddenControllerView.visibility = View.GONE + } else if (prevUxState == UX_STATE_NONE_VISIBLE) { + autoHiddenControllerView.visibility = View.VISIBLE + } + // TODO(insun): Notify specific uxState. Currently reuses legacy visibility listener for API + // compatibility. + if (prevUxState != uxState) { + playerControlView.notifyOnVisibilityChange() + } + } + + fun onLayout(left: Int, top: Int, right: Int, bottom: Int) { + controlsBackground?.layout(0, 0, right - left, bottom - top) + } + + private fun onLayoutChange(v: View, left: Int, right: Int, oldLeft: Int, oldRight: Int) { + val useMinimalMode = useMinimalMode() + if (isMinimalMode != useMinimalMode) { + isMinimalMode = useMinimalMode + v.post(::updateLayoutForSizeChange) + } + val widthChanged = right - left != oldRight - oldLeft + if (!isMinimalMode && widthChanged) { + v.post(::onLayoutWidthChanged) + } + } + + private fun onOverflowButtonClick(v: View) { + resetHideCallbacks() + if (v.id == androidx.media3.ui.R.id.exo_overflow_show) { + overflowShowAnimator.start() + } else if (v.id == androidx.media3.ui.R.id.exo_overflow_hide) { + overflowHideAnimator.start() + } + } + + private fun showAllBars() { + if (!isAnimationEnabled) { + setUxState(UX_STATE_ALL_VISIBLE) + resetHideCallbacks() + return + } + when (uxState) { + UX_STATE_NONE_VISIBLE -> showAllBarsAnimator.start() + UX_STATE_ONLY_PROGRESS_VISIBLE -> showMainBarAnimator.start() + UX_STATE_ANIMATING_HIDE -> needToShowBars = true + UX_STATE_ANIMATING_SHOW -> return + else -> Unit + } + resetHideCallbacks() + } + + private fun hideAllBars() { + hideAllBarsAnimator.start() + } + + private fun hideProgressBar() { + hideProgressBarAnimator.start() + } + + private fun hideMainBar() { + hideMainBarAnimator.start() + postDelayedRunnable(hideProgressBarRunnable, ANIMATION_INTERVAL_MS) + } + + private fun hideController() { + setUxState(UX_STATE_NONE_VISIBLE) + } + + private fun postDelayedRunnable(runnable: Runnable, interval: Long) { + if (interval >= 0) { + playerControlView.postDelayed(runnable, interval) + } + } + + private fun animateOverflow(animatedValue: Float) { + extraControlsScrollView?.apply { + translationX = width * (1 - animatedValue) + } + timeView?.setAlpha(1 - animatedValue) + basicControls?.setAlpha(1 - animatedValue) + } + + private fun useMinimalMode(): Boolean { + val width = playerControlView.width - + playerControlView.getPaddingLeft() - + playerControlView.getPaddingRight() + val height = playerControlView.height - + playerControlView.paddingBottom - + playerControlView.paddingTop + val centerControlWidth = getWidthWithMargins(centerControls) - + if (centerControls != null) { + centerControls.getPaddingLeft() + centerControls.getPaddingRight() + } else 0 + val centerControlHeight = getHeightWithMargins(centerControls) - + if (centerControls != null) { + centerControls.paddingTop + centerControls.paddingBottom + } else 0 + val defaultModeMinimumWidth = max( + centerControlWidth, + getWidthWithMargins(timeView) + getWidthWithMargins(overflowShowButton) + ) + val defaultModeMinimumHeight = (centerControlHeight + getHeightWithMargins(bottomBar) + + getHeightWithMargins(topBar)) /* + (2 * getHeightWithMargins(bottomBar))*/ + return width <= defaultModeMinimumWidth || height <= defaultModeMinimumHeight + } + + private fun updateLayoutForSizeChange() { + minimalControls?.visibility = if (isMinimalMode) View.VISIBLE else View.INVISIBLE + if (timeBar != null) { + val timeBarMarginBottom = playerControlView.resources + .getDimensionPixelSize(androidx.media3.ui.R.dimen.exo_styled_progress_margin_bottom) + val timeBarParams = timeBar.layoutParams as? MarginLayoutParams + if (timeBarParams != null) { + timeBarParams.bottomMargin = if (isMinimalMode) 0 else timeBarMarginBottom + timeBar.setLayoutParams(timeBarParams) + } + if (timeBar is DefaultTimeBar) { + if (isMinimalMode) { + timeBar.hideScrubber(/* disableScrubberPadding= */ true) + } else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) { + timeBar.hideScrubber(/* disableScrubberPadding= */ false) + } else if (uxState != UX_STATE_ANIMATING_HIDE) { + timeBar.showScrubber() + } + } + } + for (v in shownButtons) { + v.visibility = + if (isMinimalMode && shouldHideInMinimalMode(v)) View.INVISIBLE else View.VISIBLE + } + } + + private fun shouldHideInMinimalMode(button: View): Boolean { + val id = button.id + return id == androidx.media3.ui.R.id.exo_bottom_bar || + id == androidx.media3.ui.R.id.exo_prev || + id == androidx.media3.ui.R.id.exo_next || + id == androidx.media3.ui.R.id.exo_rew || + id == androidx.media3.ui.R.id.exo_rew_with_amount || + id == androidx.media3.ui.R.id.exo_ffwd || + id == androidx.media3.ui.R.id.exo_ffwd_with_amount + } + + private fun onLayoutWidthChanged() { + if (basicControls == null || extraControls == null) return + val width = playerControlView.width - + playerControlView.getPaddingLeft() - + playerControlView.getPaddingRight() + + // Reset back to all controls being basic controls and the overflow not being needed. The last + // child of extraControls is the overflow hide button, which shouldn't be moved back. + while (extraControls.childCount > 1) { + val controlViewIndex = extraControls.childCount - 2 + val controlView = extraControls.getChildAt(controlViewIndex) + extraControls.removeViewAt(controlViewIndex) + basicControls.addView(controlView, /* index= */ 0) + } + overflowShowButton?.visibility = View.GONE + + // Calculate how much of the available width is occupied. The last child of basicControls is the + // overflow show button, which we're currently assuming will not be visible. + var occupiedWidth = getWidthWithMargins(timeView) + val endIndex = basicControls.childCount - 1 + for (i in 0 until endIndex) { + val controlView = basicControls.getChildAt(i) + occupiedWidth += getWidthWithMargins(controlView) + } + if (occupiedWidth > width) { + // We need to move some controls to extraControls. + overflowShowButton?.visibility = View.VISIBLE + occupiedWidth += getWidthWithMargins(overflowShowButton) + val controlsToMove = ArrayList() + // The last child of basicControls is the overflow show button, which shouldn't be moved. + for (i in 0 until endIndex) { + val control = basicControls.getChildAt(i) + occupiedWidth -= getWidthWithMargins(control) + controlsToMove.add(control) + if (occupiedWidth <= width) { + break + } + } + if (controlsToMove.isNotEmpty()) { + basicControls.removeViews(/* start= */ 0, controlsToMove.size) + for (i in controlsToMove.indices) { + // The last child of extraControls is the overflow hide button. Add controls before it. + val index = extraControls.childCount - 1 + extraControls.addView(controlsToMove[i], index) + } + } + } else { + // If extraControls are visible, hide them since they're now empty. + if (extraControlsScrollView?.visibility == View.VISIBLE && + !overflowHideAnimator.isStarted + ) { + overflowShowAnimator.cancel() + overflowHideAnimator.start() + } + } + } + + companion object { + private const val ANIMATION_INTERVAL_MS: Long = 2000 + private const val DURATION_FOR_HIDING_ANIMATION_MS: Long = 250 + private const val DURATION_FOR_SHOWING_ANIMATION_MS: Long = 250 + + // Int for defining the UX state where all the views (ProgressBar, BottomBar) are all visible. + private const val UX_STATE_ALL_VISIBLE = 0 + + // Int for defining the UX state where only the ProgressBar view is visible. + private const val UX_STATE_ONLY_PROGRESS_VISIBLE = 1 + + // Int for defining the UX state where none of the views are visible. + private const val UX_STATE_NONE_VISIBLE = 2 + + // Int for defining the UX state where the views are being animated to be hidden. + private const val UX_STATE_ANIMATING_HIDE = 3 + + // Int for defining the UX state where the views are being animated to be shown. + private const val UX_STATE_ANIMATING_SHOW = 4 + + private fun ofAlpha(startValue: Float, endValue: Float, target: View?): ObjectAnimator { + return ObjectAnimator.ofFloat(target, "alpha", startValue, endValue) + } + + private fun getWidthWithMargins(v: View?): Int { + v ?: return 0 + var width = v.width + val layoutParams = v.layoutParams + if (layoutParams is MarginLayoutParams) { + width += layoutParams.leftMargin + layoutParams.rightMargin + } + return width + } + + private fun getHeightWithMargins(v: View?): Int { + v ?: return 0 + var height = v.height + val layoutParams = v.layoutParams + if (layoutParams is MarginLayoutParams) { + height += layoutParams.topMargin + layoutParams.bottomMargin + } + return height + } + } +} diff --git a/app/src/main/java/com/skyd/anivu/ui/player/PlayerGestureDetector.kt b/app/src/main/java/com/skyd/anivu/ui/player/PlayerGestureDetector.kt index 0b4d89da..a0ee4f9d 100644 --- a/app/src/main/java/com/skyd/anivu/ui/player/PlayerGestureDetector.kt +++ b/app/src/main/java/com/skyd/anivu/ui/player/PlayerGestureDetector.kt @@ -254,8 +254,4 @@ class PlayerGestureDetector( fun onLongPressUp(): Boolean = false fun onLongPress() {} } - - companion object { - private const val INVALID_POINTER_ID = -1 - } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/player/PlayerView.java b/app/src/main/java/com/skyd/anivu/ui/player/PlayerView.java index 4f94efcf..3c33d87d 100644 --- a/app/src/main/java/com/skyd/anivu/ui/player/PlayerView.java +++ b/app/src/main/java/com/skyd/anivu/ui/player/PlayerView.java @@ -1430,6 +1430,11 @@ public void setForward85sButton(boolean visible) { controller.setForward85sButton(visible); } + public void setOnScreenshotListener(@Nullable View.OnClickListener listener) { + Assertions.checkStateNotNull(controller); + controller.setOnScreenshotListener(listener); + } + /** * Sets whether the shuffle button is shown. * diff --git a/app/src/main/res/drawable/ic_photo_camera_24.xml b/app/src/main/res/drawable/ic_photo_camera_24.xml new file mode 100644 index 00000000..8cc2ebc1 --- /dev/null +++ b/app/src/main/res/drawable/ic_photo_camera_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/exo_player_view.xml b/app/src/main/res/layout/exo_player_view.xml index 1196732b..e80ca399 100644 --- a/app/src/main/res/layout/exo_player_view.xml +++ b/app/src/main/res/layout/exo_player_view.xml @@ -19,7 +19,7 @@ android:background="@android:color/black" tools:ignore="PrivateResource"> - + - + - + - + diff --git a/app/src/main/res/layout/player_control_view.xml b/app/src/main/res/layout/player_control_view.xml index 1091ce1b..8ab75ad5 100644 --- a/app/src/main/res/layout/player_control_view.xml +++ b/app/src/main/res/layout/player_control_view.xml @@ -26,8 +26,8 @@ app:layout_constraintEnd_toEndOf="parent" app:text="+10s" /> - - + + + + center_vertical + +