kopia lustrzana https://github.com/ryukoposting/Signal-Android
Improve smoothness of segmented progress bar and respect video duration.
rodzic
63412b0153
commit
4e57432dbb
|
@ -29,14 +29,14 @@ package org.thoughtcrime.securesms.components.segmentedprogressbar
|
|||
*/
|
||||
class Segment(val animationDurationMillis: Long) {
|
||||
|
||||
private var animationProgress: Int = 0
|
||||
var animationProgressPercentage: Float = 0f
|
||||
|
||||
var animationState: AnimationState = AnimationState.IDLE
|
||||
set(value) {
|
||||
animationProgress = when (value) {
|
||||
AnimationState.ANIMATED -> 100
|
||||
AnimationState.IDLE -> 0
|
||||
else -> animationProgress
|
||||
animationProgressPercentage = when (value) {
|
||||
AnimationState.ANIMATED -> 1f
|
||||
AnimationState.IDLE -> 0f
|
||||
else -> animationProgressPercentage
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
@ -49,9 +49,4 @@ class Segment(val animationDurationMillis: Long) {
|
|||
ANIMATING,
|
||||
IDLE
|
||||
}
|
||||
|
||||
val progressPercentage: Float
|
||||
get() = animationProgress.toFloat() / 100
|
||||
|
||||
fun progress() = animationProgress++
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package org.thoughtcrime.securesms.components.segmentedprogressbar
|
||||
|
||||
data class SegmentState(
|
||||
val position: Long,
|
||||
val duration: Long
|
||||
)
|
|
@ -28,13 +28,12 @@ import android.content.Context
|
|||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Path
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
import org.thoughtcrime.securesms.R
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Created by Tiago Ornelas on 18/04/2020.
|
||||
|
@ -42,7 +41,14 @@ import org.thoughtcrime.securesms.R
|
|||
* @see Segment
|
||||
* And the progress of each segment is animated based on a set speed
|
||||
*/
|
||||
class SegmentedProgressBar : View, Runnable, ViewPager.OnPageChangeListener, View.OnTouchListener {
|
||||
class SegmentedProgressBar : View, ViewPager.OnPageChangeListener, View.OnTouchListener {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* It is common now for devices to run at 60FPS
|
||||
*/
|
||||
val MILLIS_PER_FRAME = TimeUnit.MILLISECONDS.toMillis(17)
|
||||
}
|
||||
|
||||
private val path = Path()
|
||||
private val corners = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f)
|
||||
|
@ -57,7 +63,10 @@ class SegmentedProgressBar : View, Runnable, ViewPager.OnPageChangeListener, Vie
|
|||
}
|
||||
|
||||
/**
|
||||
* Mapping of segment index -> duration in millis
|
||||
* Mapping of segment index -> duration in millis. Negative durations
|
||||
* ARE valid but they'll result in a call to SegmentedProgressBarListener#onRequestSegmentProgressPercentage
|
||||
* which should return the current % position for the currently playing item. This helps
|
||||
* to avoid synchronizing the seek bar to playback.
|
||||
*/
|
||||
var segmentDurations: Map<Int, Long> = mapOf()
|
||||
set(value) {
|
||||
|
@ -93,8 +102,6 @@ class SegmentedProgressBar : View, Runnable, ViewPager.OnPageChangeListener, Vie
|
|||
private val selectedSegmentIndex: Int
|
||||
get() = segments.indexOf(this.selectedSegment)
|
||||
|
||||
private val animationHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
// Drawing
|
||||
val strokeApplicable: Boolean
|
||||
get() = segmentStrokeWidth * 4 <= measuredHeight
|
||||
|
@ -121,6 +128,8 @@ class SegmentedProgressBar : View, Runnable, ViewPager.OnPageChangeListener, Vie
|
|||
*/
|
||||
var listener: SegmentedProgressBarListener? = null
|
||||
|
||||
private var lastFrameTimeMillis: Long = 0L
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
|
@ -225,6 +234,8 @@ class SegmentedProgressBar : View, Runnable, ViewPager.OnPageChangeListener, Vie
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
onFrame(System.currentTimeMillis())
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -233,17 +244,20 @@ class SegmentedProgressBar : View, Runnable, ViewPager.OnPageChangeListener, Vie
|
|||
fun start() {
|
||||
pause()
|
||||
val segment = selectedSegment
|
||||
if (segment == null)
|
||||
if (segment == null) {
|
||||
next()
|
||||
else
|
||||
animationHandler.postDelayed(this, segment.animationDurationMillis / 100)
|
||||
} else {
|
||||
isPaused = false
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the animation process
|
||||
*/
|
||||
fun pause() {
|
||||
animationHandler.removeCallbacks(this)
|
||||
isPaused = true
|
||||
lastFrameTimeMillis = 0L
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -327,15 +341,24 @@ class SegmentedProgressBar : View, Runnable, ViewPager.OnPageChangeListener, Vie
|
|||
if (nextSegment != null) {
|
||||
pause()
|
||||
nextSegment.animationState = Segment.AnimationState.ANIMATING
|
||||
animationHandler.postDelayed(this, nextSegment.animationDurationMillis / 100)
|
||||
isPaused = false
|
||||
invalidate()
|
||||
this.listener?.onPage(oldSegmentIndex, this.selectedSegmentIndex)
|
||||
viewPager?.currentItem = this.selectedSegmentIndex
|
||||
} else {
|
||||
animationHandler.removeCallbacks(this)
|
||||
pause()
|
||||
this.listener?.onFinished()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSegmentProgressPercentage(segment: Segment, timeSinceLastFrameMillis: Long): Float {
|
||||
return if (segment.animationDurationMillis > 0) {
|
||||
segment.animationProgressPercentage + timeSinceLastFrameMillis.toFloat() / segment.animationDurationMillis
|
||||
} else {
|
||||
listener?.onRequestSegmentProgressPercentage() ?: 0f
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSegments() {
|
||||
this.segments.clear()
|
||||
segments.addAll(
|
||||
|
@ -348,12 +371,30 @@ class SegmentedProgressBar : View, Runnable, ViewPager.OnPageChangeListener, Vie
|
|||
reset()
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
if (this.selectedSegment?.progress() ?: 0 >= 100) {
|
||||
private var isPaused = true
|
||||
|
||||
private fun onFrame(frameTimeMillis: Long) {
|
||||
if (isPaused) {
|
||||
return
|
||||
}
|
||||
|
||||
val lastFrameTimeMillis = this.lastFrameTimeMillis
|
||||
|
||||
this.lastFrameTimeMillis = frameTimeMillis
|
||||
|
||||
val selectedSegment = this.selectedSegment
|
||||
if (selectedSegment == null) {
|
||||
loadSegment(offset = 1, userAction = false)
|
||||
} else if (lastFrameTimeMillis > 0L) {
|
||||
val segmentProgressPercentage = getSegmentProgressPercentage(selectedSegment, frameTimeMillis - lastFrameTimeMillis)
|
||||
selectedSegment.animationProgressPercentage = segmentProgressPercentage
|
||||
if (selectedSegment.animationProgressPercentage >= 1f) {
|
||||
loadSegment(offset = 1, userAction = false)
|
||||
} else {
|
||||
this.invalidate()
|
||||
}
|
||||
} else {
|
||||
this.invalidate()
|
||||
animationHandler.postDelayed(this, this.selectedSegment?.animationDurationMillis?.let { it / 100 } ?: (timePerSegmentMs / 100))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,4 +37,6 @@ interface SegmentedProgressBarListener {
|
|||
* Notifies when last segment finished animating
|
||||
*/
|
||||
fun onFinished()
|
||||
|
||||
fun onRequestSegmentProgressPercentage(): Float?
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ fun SegmentedProgressBar.getDrawingComponents(
|
|||
RectF(
|
||||
startBound + stroke,
|
||||
height - stroke,
|
||||
startBound + segment.progressPercentage * segmentWidth,
|
||||
startBound + segment.animationProgressPercentage * segmentWidth,
|
||||
stroke
|
||||
)
|
||||
)
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
package org.thoughtcrime.securesms.mediapreview
|
||||
|
||||
import android.net.Uri
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.thoughtcrime.securesms.video.VideoPlayer
|
||||
|
||||
/**
|
||||
|
@ -14,12 +11,15 @@ class VideoControlsDelegate {
|
|||
|
||||
private val playWhenReady: MutableMap<Uri, Boolean> = mutableMapOf()
|
||||
private val playerSubject = BehaviorSubject.create<Player>()
|
||||
private val playerReadySignal = PublishSubject.create<Unit>()
|
||||
val playerUpdates: Observable<PlayerUpdate> = playerReadySignal
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.flatMap { playerSubject }
|
||||
.filter { it.videoPlayer != null }
|
||||
.map { PlayerUpdate(it.uri, it.videoPlayer?.duration!!) }
|
||||
|
||||
fun getPlayerState(uri: Uri): PlayerState? {
|
||||
val player = playerSubject.value
|
||||
return if (player?.uri == uri && player.videoPlayer != null) {
|
||||
PlayerState(uri, player.videoPlayer.playbackPosition, player.videoPlayer.duration)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun pause() = playerSubject.value?.videoPlayer?.pause()
|
||||
|
||||
|
@ -41,14 +41,6 @@ class VideoControlsDelegate {
|
|||
fun attachPlayer(uri: Uri, videoPlayer: VideoPlayer?) {
|
||||
playerSubject.onNext(Player(uri, videoPlayer))
|
||||
|
||||
if ((videoPlayer?.duration ?: -1L) > 0L) {
|
||||
playerReadySignal.onNext(Unit)
|
||||
} else {
|
||||
videoPlayer?.setPlayerStateCallbacks {
|
||||
playerReadySignal.onNext(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
if (playWhenReady[uri] == true) {
|
||||
playWhenReady[uri] = false
|
||||
videoPlayer?.play()
|
||||
|
@ -64,8 +56,9 @@ class VideoControlsDelegate {
|
|||
val videoPlayer: VideoPlayer? = null
|
||||
)
|
||||
|
||||
data class PlayerUpdate(
|
||||
data class PlayerState(
|
||||
val mediaUri: Uri,
|
||||
val position: Long,
|
||||
val duration: Long
|
||||
)
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ import org.thoughtcrime.securesms.util.AvatarUtil
|
|||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
@ -173,6 +174,25 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
|||
override fun onFinished() {
|
||||
viewModel.goToNextPost()
|
||||
}
|
||||
|
||||
override fun onRequestSegmentProgressPercentage(): Float? {
|
||||
val attachmentUri = if (viewModel.hasPost() && MediaUtil.isVideo(viewModel.getPost().attachment)) {
|
||||
viewModel.getPost().attachment.uri
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return if (attachmentUri != null) {
|
||||
val playerState = videoControlsDelegate.getPlayerState(attachmentUri)
|
||||
if (playerState != null) {
|
||||
playerState.position.toFloat() / playerState.duration
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sharedViewModel.isScrolling.observe(viewLifecycleOwner) { isScrolling ->
|
||||
|
@ -208,7 +228,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
|||
|
||||
val durations: Map<Int, Long> = state.posts
|
||||
.mapIndexed { index, storyPost ->
|
||||
index to (storyPost.attachment.uri?.let { state.durations[it] } ?: TimeUnit.SECONDS.toMillis(5))
|
||||
index to if (MediaUtil.isVideo(storyPost.attachment)) -1L else TimeUnit.SECONDS.toMillis(5)
|
||||
}
|
||||
.toMap()
|
||||
|
||||
|
@ -246,18 +266,17 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
|||
}
|
||||
}
|
||||
|
||||
lifecycleDisposable += videoControlsDelegate.playerUpdates.subscribe { update ->
|
||||
if (update.duration > 0L) {
|
||||
viewModel.setDuration(update.mediaUri, update.duration)
|
||||
}
|
||||
}
|
||||
|
||||
adjustConstraintsForScreenDimensions(viewsAndReplies, cardWrapper, card)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.setIsFragmentResumed(true)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
pauseProgress()
|
||||
viewModel.setIsFragmentResumed(false)
|
||||
}
|
||||
|
||||
override fun onFinishForwardAction() = Unit
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.page
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
data class StoryViewerPageState(
|
||||
val posts: List<StoryPost> = emptyList(),
|
||||
val durations: Map<Uri, Long> = emptyMap(),
|
||||
val selectedPostIndex: Int = 0,
|
||||
val replyState: ReplyState = ReplyState.NONE
|
||||
) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.page
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
|
@ -42,12 +41,6 @@ class StoryViewerPageViewModel(
|
|||
refresh()
|
||||
}
|
||||
|
||||
fun setDuration(uri: Uri, duration: Long) {
|
||||
store.update {
|
||||
it.copy(durations = it.durations + (uri to duration))
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
disposables.clear()
|
||||
disposables += repository.getStoryPostsFor(recipientId).subscribe { posts ->
|
||||
|
@ -117,6 +110,10 @@ class StoryViewerPageViewModel(
|
|||
storyViewerDialogSubject.onNext(Optional.of(StoryViewerDialog.GroupDirectReply(recipientId, storyId)))
|
||||
}
|
||||
|
||||
fun setIsFragmentResumed(isFragmentResumed: Boolean) {
|
||||
storyViewerPlaybackStore.update { it.copy(isFragmentResumed = isFragmentResumed) }
|
||||
}
|
||||
|
||||
fun setIsUserScrollingParent(isUserScrollingParent: Boolean) {
|
||||
storyViewerPlaybackStore.update { it.copy(isUserScrollingParent = isUserScrollingParent) }
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@ data class StoryViewerPlaybackState(
|
|||
val isDisplayingCaptionOverlay: Boolean = false,
|
||||
val isUserScrollingParent: Boolean = false,
|
||||
val isSelectedPage: Boolean = false,
|
||||
val isDisplayingSlate: Boolean = false
|
||||
val isDisplayingSlate: Boolean = false,
|
||||
val isFragmentResumed: Boolean = false
|
||||
) {
|
||||
val isPaused: Boolean = !areSegmentsInitialized ||
|
||||
isUserTouching ||
|
||||
|
@ -24,5 +25,6 @@ data class StoryViewerPlaybackState(
|
|||
isDisplayingCaptionOverlay ||
|
||||
isUserScrollingParent ||
|
||||
!isSelectedPage ||
|
||||
isDisplayingSlate
|
||||
isDisplayingSlate ||
|
||||
!isFragmentResumed
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue