列表支持前后视频预加载

This commit is contained in:
Lindong 2026-01-27 15:36:24 +08:00
parent 45b63e870e
commit da258f69f6
4 changed files with 92 additions and 59 deletions

View File

@ -241,9 +241,10 @@ class HomeFragment : AppViewsFragment<ViewBinding, UiState, ViewModel>(), OnSwit
handleEventOneVideoSwiped() handleEventOneVideoSwiped()
} }
// Preload next N videos // Preload next N videos and keep previous N videos
val preloadCount = 3 val preloadCount = 3
for (i in 1..preloadCount) { for (i in 1..preloadCount) {
// Next N
val nextPos = position + i val nextPos = position + i
if (nextPos < mViewPagerAdapter.itemCount) { if (nextPos < mViewPagerAdapter.itemCount) {
val nextFragment = mViewPagerAdapter.getFragmentByIndex(nextPos) val nextFragment = mViewPagerAdapter.getFragmentByIndex(nextPos)
@ -251,22 +252,35 @@ class HomeFragment : AppViewsFragment<ViewBinding, UiState, ViewModel>(), OnSwit
nextFragment.preloadVideo() nextFragment.preloadVideo()
} }
} }
}
// Prev N (Keep them ready for playback)
// Release players for previous fragments to save memory
// We keep 'preloadCount' fragments after current, and maybe 0 before?
// But offscreenPageLimit keeps them in memory. We should release their heavy resources.
val offscreenLimit = 3
for (i in 1..offscreenLimit) {
val prevPos = position - i val prevPos = position - i
if (prevPos >= 0) { if (prevPos >= 0) {
val prevFragment = mViewPagerAdapter.getFragmentByIndex(prevPos) val prevFragment = mViewPagerAdapter.getFragmentByIndex(prevPos)
if (prevFragment != null && prevFragment is HomeItemFragment) { if (prevFragment != null && prevFragment is HomeItemFragment) {
prevFragment.releasePlayer() prevFragment.preloadVideo()
} }
} }
} }
// Release players outside of the window (position - preloadCount - 1)
val releasePosBefore = position - preloadCount - 1
if (releasePosBefore >= 0) {
val prevFragment = mViewPagerAdapter.getFragmentByIndex(releasePosBefore)
if (prevFragment != null && prevFragment is HomeItemFragment) {
prevFragment.releasePlayer()
}
}
// Release players outside of the window (position + preloadCount + 1)
val releasePosAfter = position + preloadCount + 1
if (releasePosAfter < mViewPagerAdapter.itemCount) {
val nextFragment = mViewPagerAdapter.getFragmentByIndex(releasePosAfter)
if (nextFragment != null && nextFragment is HomeItemFragment) {
nextFragment.releasePlayer()
}
}
// load more // load more
if (mViewPagerAdapter.itemCount > 0 && position >= mViewPagerAdapter.itemCount - 3) { if (mViewPagerAdapter.itemCount > 0 && position >= mViewPagerAdapter.itemCount - 3) {
lifecycleScope.launch { lifecycleScope.launch {

View File

@ -93,17 +93,23 @@ class HomeItemFragment : AppViewsEmptyViewModelFragment<ViewBinding>() {
} }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putFloat("curPlayedSecond", mCurPlayedSecond)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
mCurPlayedSecond = savedInstanceState.getFloat("curPlayedSecond", 0f)
}
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
mMaskBitmap?.recycle() mMaskBitmap?.recycle()
} }
fun playVideo() { fun playVideo() {
mIsPreloading = false mIsPreloading = false
binding?.playerContainer?.isVisible = true binding?.playerContainer?.isVisible = true
@ -174,19 +180,21 @@ class HomeItemFragment : AppViewsEmptyViewModelFragment<ViewBinding>() {
mPlayerView?.enableAutomaticInitialization = true mPlayerView?.enableAutomaticInitialization = true
} }
// val playerUiController = MyPlayerControlView(mPlayerView!!)
// mPlayerView!!.setCustomPlayerUi(playerUiController.rootView)
mPlayerView!!.removeViews(1, mPlayerView!!.childCount - 1)
mPlayerView!!.addYouTubePlayerListener(object : AbstractYouTubePlayerListener() { mPlayerView!!.addYouTubePlayerListener(object : AbstractYouTubePlayerListener() {
override fun onReady(@NonNull youTubePlayer: YouTubePlayer) { override fun onReady(youTubePlayer: YouTubePlayer) {
mPlayer = youTubePlayer mPlayer = youTubePlayer
val playerUiController = MyPlayerControlView(mPlayerView!!, youTubePlayer)
mPlayerView!!.setCustomPlayerUi(playerUiController.rootView)
if (mPendingPlay) { if (mPendingPlay) {
mPlayer?.loadVideo(mVideoData!!.id, 0f) mPlayer?.loadVideo(mVideoData!!.id, mCurPlayedSecond)
} else if (mIsPreloading) { } else if (mIsPreloading) {
mPlayer?.mute() mPlayer?.mute()
mPlayer?.loadVideo(mVideoData!!.id, 0f) mPlayer?.loadVideo(mVideoData!!.id, mCurPlayedSecond)
} else { } else {
mPlayer?.cueVideo(mVideoData!!.id, 0f) mPlayer?.cueVideo(mVideoData!!.id, mCurPlayedSecond)
} }
} }
@ -232,6 +240,7 @@ class HomeItemFragment : AppViewsEmptyViewModelFragment<ViewBinding>() {
PlayerConstants.PlayerState.ENDED -> { PlayerConstants.PlayerState.ENDED -> {
togglePlayingState(false) togglePlayingState(false)
if (!mIsPreloading) { if (!mIsPreloading) {
mCurPlayedSecond = 0f
mPlayer?.loadVideo(mVideoData!!.id, 0f) mPlayer?.loadVideo(mVideoData!!.id, 0f)
} }
} }
@ -272,9 +281,9 @@ class HomeItemFragment : AppViewsEmptyViewModelFragment<ViewBinding>() {
private fun togglePlayingState1(isPlaying: Boolean) { private fun togglePlayingState1(isPlaying: Boolean) {
if (mIsPlaying != isPlaying) { if (mIsPlaying != isPlaying) {
mIsPlaying = isPlaying mIsPlaying = isPlaying
if (mIsPlaying) { // if (mIsPlaying) {
binding?.circlePb?.isVisible = false // binding?.circlePb?.isVisible = false
} // }
binding?.ivMask?.isVisible = !mIsPlaying binding?.ivMask?.isVisible = !mIsPlaying
// Ensure playerContainer is visible when playing OR preloading (masked) // Ensure playerContainer is visible when playing OR preloading (masked)
@ -295,7 +304,7 @@ class HomeItemFragment : AppViewsEmptyViewModelFragment<ViewBinding>() {
private fun switchState2Play() { private fun switchState2Play() {
mThumbHandler.removeCallbacksAndMessages(null) mThumbHandler.removeCallbacksAndMessages(null)
binding?.circlePb?.isVisible = false // binding?.circlePb?.isVisible = false
binding?.ivMask?.isVisible = false binding?.ivMask?.isVisible = false
binding?.playerContainer?.isVisible = true binding?.playerContainer?.isVisible = true
hidePlayIconAnim() hidePlayIconAnim()
@ -305,7 +314,7 @@ class HomeItemFragment : AppViewsEmptyViewModelFragment<ViewBinding>() {
private fun switchState2Pause() { private fun switchState2Pause() {
binding?.ivMask?.isVisible = true binding?.ivMask?.isVisible = true
binding?.playerContainer?.isVisible = false binding?.playerContainer?.isVisible = false
showPlayIconAnim() // showPlayIconAnim()
mTickerTimer.pause() mTickerTimer.pause()
} }
@ -369,6 +378,7 @@ class HomeItemFragment : AppViewsEmptyViewModelFragment<ViewBinding>() {
private fun hidePlayIconAnim() { private fun hidePlayIconAnim() {
return
if (!binding?.playIcon!!.isVisible) { if (!binding?.playIcon!!.isVisible) {
return return
} }
@ -404,6 +414,7 @@ class HomeItemFragment : AppViewsEmptyViewModelFragment<ViewBinding>() {
} }
private fun showPlayIconAnim() { private fun showPlayIconAnim() {
return
with (binding?.playIcon!!) { with (binding?.playIcon!!) {
visibility = View.VISIBLE visibility = View.VISIBLE

View File

@ -21,13 +21,14 @@ import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.menu.YouTub
import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.utils.FadeViewHelper import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.utils.FadeViewHelper
import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.views.YouTubePlayerSeekBar import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.views.YouTubePlayerSeekBar
import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.views.YouTubePlayerSeekBarListener import com.pierfrancescosoffritti.androidyoutubeplayer.core.customui.views.YouTubePlayerSeekBarListener
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.YouTubePlayerCallback
import kotlin.jvm.javaClass import kotlin.jvm.javaClass
class MyPlayerControlView( class MyPlayerControlView(
private val youTubePlayerView: YouTubePlayerView, private val youTubePlayerView: YouTubePlayerView,
private val youTubePlayer: YouTubePlayer
) : PlayerUiController { ) : PlayerUiController {
var youTubePlayer: YouTubePlayer? = null
val rootView: View = View.inflate(youTubePlayerView.context, R.layout.layout_player_controller, null) val rootView: View = View.inflate(youTubePlayerView.context, R.layout.layout_player_controller, null)
@ -133,16 +134,25 @@ class MyPlayerControlView(
onMenuButtonClickListener = View.OnClickListener { youTubePlayerMenu.show(menuButton) } onMenuButtonClickListener = View.OnClickListener { youTubePlayerMenu.show(menuButton) }
initClickListeners() youTubePlayerView.getYouTubePlayerWhenReady(object : YouTubePlayerCallback{
override fun onYouTubePlayer(youTubePlayer: YouTubePlayer) {
this@MyPlayerControlView.youTubePlayer = youTubePlayer
initClickListeners()
}
})
} }
private fun initClickListeners() {
youTubePlayer.addListener(youtubePlayerSeekBar)
youTubePlayer.addListener(fadeControlsContainer)
youTubePlayer.addListener(youTubePlayerStateListener)
youtubePlayerSeekBar.youtubePlayerSeekBarListener = object : YouTubePlayerSeekBarListener { private fun initClickListeners() {
override fun seekTo(time: Float) = youTubePlayer.seekTo(time) youTubePlayer?.addListener(youtubePlayerSeekBar)
youTubePlayer?.addListener(fadeControlsContainer)
youTubePlayer?.addListener(youTubePlayerStateListener)
youTubePlayer?.let {
youtubePlayerSeekBar.youtubePlayerSeekBarListener = object : YouTubePlayerSeekBarListener {
override fun seekTo(time: Float) = it.seekTo(time)
}
} }
panel.setOnClickListener { fadeControlsContainer.toggleVisibility() } panel.setOnClickListener { fadeControlsContainer.toggleVisibility() }
playPauseButton.setOnClickListener { onPlayButtonPressed() } playPauseButton.setOnClickListener { onPlayButtonPressed() }
@ -270,9 +280,9 @@ class MyPlayerControlView(
private fun onPlayButtonPressed() { private fun onPlayButtonPressed() {
if (isPlaying) if (isPlaying)
youTubePlayer.pause() youTubePlayer?.pause()
else else
youTubePlayer.play() youTubePlayer?.play()
} }
private fun updateState(state: PlayerConstants.PlayerState) { private fun updateState(state: PlayerConstants.PlayerState) {

View File

@ -8,44 +8,43 @@
android:id="@+id/player_container" android:id="@+id/player_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center"/> android:layout_gravity="center" />
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_mask" android:id="@+id/iv_mask"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center"
android:background="#00000000" android:background="#00000000"
android:clickable="false" android:clickable="false" />
android:layout_gravity="center"/>
<View <View
android:id="@+id/click_mask_view" android:id="@+id/click_mask_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clickable="true"
android:layout_marginBottom="30dp" android:layout_marginBottom="30dp"
/> android:clickable="true" />
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/play_icon" android:id="@+id/play_icon"
android:layout_width="100dp" android:layout_width="100dp"
android:layout_height="100dp" android:layout_height="100dp"
android:layout_gravity="center"
android:padding="20dp" android:padding="20dp"
android:src="@mipmap/icon_play" android:src="@mipmap/icon_play"
android:visibility="gone" android:visibility="gone" />
android:layout_gravity="center"/>
<LinearLayout <LinearLayout
android:id="@+id/bottom_container" android:id="@+id/bottom_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:layout_gravity="bottom"
android:layout_marginVertical="25dp" android:layout_marginVertical="25dp"
android:layout_marginLeft="15dp" android:layout_marginLeft="15dp"
android:layout_marginRight="80dp" android:layout_marginRight="80dp"
android:layout_gravity="bottom"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/tv_video_from" android:id="@+id/tv_video_from"
@ -53,34 +52,34 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" android:singleLine="true"
android:text="From" android:text="From"
android:textSize="15sp" android:textColor="@color/white"
android:textColor="@color/white" /> android:textSize="15sp" />
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<TextView <TextView
android:id="@+id/tv_video_intro" android:id="@+id/tv_video_intro"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:text="introduce" android:layout_marginEnd="45dp"
android:clickable="false" android:clickable="false"
android:maxLines="2" android:maxLines="2"
android:layout_marginEnd="45dp" android:text="introduce"
android:textSize="15sp" android:textColor="@color/white_al80"
android:textColor="@color/white_al80" /> android:textSize="15sp" />
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_intro_expand" android:id="@+id/iv_intro_expand"
android:layout_width="40dp" android:layout_width="40dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_gravity="right|bottom"
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:paddingVertical="5dp" android:paddingVertical="5dp"
android:layout_gravity="right|bottom"
android:src="@mipmap/arrow_up" android:src="@mipmap/arrow_up"
android:visibility="gone" android:visibility="gone" />
/>
</FrameLayout> </FrameLayout>
</LinearLayout> </LinearLayout>
@ -89,18 +88,17 @@
android:id="@+id/progress_bar_player" android:id="@+id/progress_bar_player"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="3dp" android:layout_height="3dp"
android:layout_marginBottom="16dp"
android:layout_marginHorizontal="15dp"
android:layout_gravity="bottom" android:layout_gravity="bottom"
/> android:layout_marginHorizontal="15dp"
android:layout_marginBottom="16dp" />
<ProgressBar <ProgressBar
android:id="@+id/circle_pb" android:id="@+id/circle_pb"
style="?android:attr/progressBarStyleLarge"
android:layout_width="35dp" android:layout_width="35dp"
android:layout_height="35dp" android:layout_height="35dp"
android:layout_gravity="center" android:layout_gravity="center"
android:indeterminateTint="@android:color/white" android:indeterminateTint="@android:color/white"
style="?android:attr/progressBarStyleLarge" android:visibility="gone" />
/>
</FrameLayout> </FrameLayout>