Signal-Android/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerFragment.kt

218 wiersze
7.3 KiB
Kotlin

package org.thoughtcrime.securesms.stories.viewer
import android.graphics.RenderEffect
import android.graphics.Shader
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.core.app.ActivityCompat
import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.viewpager2.widget.ViewPager2
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import org.signal.core.util.getParcelableArrayListCompat
import org.signal.core.util.getParcelableCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stories.StoryViewerArgs
import org.thoughtcrime.securesms.stories.viewer.first.StoryFirstTimeNavigationFragment
import org.thoughtcrime.securesms.stories.viewer.page.StoryViewerPageArgs
import org.thoughtcrime.securesms.stories.viewer.page.StoryViewerPageFragment
import org.thoughtcrime.securesms.stories.viewer.reply.StoriesSharedElementCrossFaderView
import org.thoughtcrime.securesms.util.LifecycleDisposable
/**
* Fragment which manages a vertical pager fragment of stories.
*/
class StoryViewerFragment :
Fragment(R.layout.stories_viewer_fragment),
StoryViewerPageFragment.Callback,
StoriesSharedElementCrossFaderView.Callback {
private val onPageChanged = OnPageChanged()
private lateinit var storyPager: ViewPager2
private val viewModel: StoryViewerViewModel by viewModels(
factoryProducer = {
StoryViewerViewModel.Factory(storyViewerArgs, StoryViewerRepository())
}
)
private val lifecycleDisposable = LifecycleDisposable()
private val storyViewerArgs: StoryViewerArgs by lazy { requireArguments().getParcelableCompat(ARGS, StoryViewerArgs::class.java)!! }
private lateinit var storyCrossfader: StoriesSharedElementCrossFaderView
private var pagerOnPageSelectedLock: Boolean = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
storyCrossfader = view.findViewById(R.id.story_content_crossfader)
storyPager = view.findViewById(R.id.story_item_pager)
ViewCompat.setTransitionName(storyCrossfader, "story")
storyCrossfader.callback = this
val adapter = StoryViewerPagerAdapter(
this,
StoryViewerPageArgs(
recipientId = Recipient.UNKNOWN.id,
initialStoryId = storyViewerArgs.storyId,
isJumpForwardToUnviewed = storyViewerArgs.isJumpToUnviewed,
isOutgoingOnly = storyViewerArgs.isFromMyStories,
source = when {
storyViewerArgs.isFromInfoContextMenuAction -> StoryViewerPageArgs.Source.INFO_CONTEXT
storyViewerArgs.isFromNotification -> StoryViewerPageArgs.Source.NOTIFICATION
else -> StoryViewerPageArgs.Source.UNKNOWN
},
groupReplyStartPosition = storyViewerArgs.groupReplyStartPosition
)
)
storyPager.adapter = adapter
storyPager.overScrollMode = ViewPager2.OVER_SCROLL_NEVER
lifecycleDisposable += viewModel.allowParentScrolling.observeOn(AndroidSchedulers.mainThread()).subscribe {
storyPager.isUserInputEnabled = it
}
storyPager.offscreenPageLimit = 1
lifecycleDisposable.bindTo(viewLifecycleOwner)
lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state ->
if (state.noPosts) {
ActivityCompat.finishAfterTransition(requireActivity())
}
adapter.setPages(state.pages)
if (state.pages.isNotEmpty() && storyPager.currentItem != state.page) {
pagerOnPageSelectedLock = true
storyPager.isUserInputEnabled = false
storyPager.setCurrentItem(state.page, state.previousPage > -1)
pagerOnPageSelectedLock = false
if (state.page >= state.pages.size) {
ActivityCompat.finishAfterTransition(requireActivity())
lifecycleDisposable.clear()
}
}
when (state.crossfadeSource) {
is StoryViewerState.CrossfadeSource.TextModel -> storyCrossfader.setSourceView(state.crossfadeSource.storyTextPostModel)
is StoryViewerState.CrossfadeSource.ImageUri -> storyCrossfader.setSourceView(state.crossfadeSource.imageUri, state.crossfadeSource.imageBlur)
StoryViewerState.CrossfadeSource.None -> Unit
}
if (state.crossfadeTarget is StoryViewerState.CrossfadeTarget.Record) {
storyCrossfader.setTargetView(state.crossfadeTarget.messageRecord)
requireActivity().supportStartPostponedEnterTransition()
}
if (state.skipCrossfade) {
viewModel.setCrossfaderIsReady(true)
}
if (state.loadState.isReady()) {
storyCrossfader.alpha = 0f
}
}
if (savedInstanceState != null && savedInstanceState.containsKey(HIDDEN)) {
val ids: List<RecipientId> = savedInstanceState.getParcelableArrayListCompat(HIDDEN, RecipientId::class.java)!!
viewModel.addHiddenAndRefresh(ids.toSet())
} else {
viewModel.refresh()
if (!SignalStore.storyValues().userHasSeenFirstNavView) {
StoryFirstTimeNavigationFragment().show(childFragmentManager, null)
}
}
if (Build.VERSION.SDK_INT >= 31) {
lifecycleDisposable += viewModel.isFirstTimeNavigationShowing.subscribe {
if (it) {
requireView().rootView.setRenderEffect(RenderEffect.createBlurEffect(100f, 100f, Shader.TileMode.CLAMP))
} else {
requireView().rootView.setRenderEffect(null)
}
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putParcelableArrayList(HIDDEN, ArrayList(viewModel.getHidden()))
}
override fun onResume() {
super.onResume()
viewModel.setIsScrolling(false)
storyPager.registerOnPageChangeCallback(onPageChanged)
}
override fun onPause() {
super.onPause()
viewModel.setIsScrolling(false)
storyPager.unregisterOnPageChangeCallback(onPageChanged)
}
override fun onGoToPreviousStory(recipientId: RecipientId) {
viewModel.onGoToPrevious(recipientId)
}
override fun onFinishedPosts(recipientId: RecipientId) {
viewModel.onGoToNext(recipientId)
}
override fun onStoryHidden(recipientId: RecipientId) {
viewModel.addHiddenAndRefresh(setOf(recipientId))
}
override fun onContentTranslation(x: Float, y: Float) {
storyCrossfader.translationX = x
storyCrossfader.translationY = y
}
override fun onReadyToAnimate() {
}
override fun onAnimationStarted() {
viewModel.setCrossfaderIsReady(false)
}
override fun onAnimationFinished() {
viewModel.setCrossfaderIsReady(true)
}
inner class OnPageChanged : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
if (!pagerOnPageSelectedLock) {
viewModel.setSelectedPage(position)
}
}
override fun onPageScrollStateChanged(state: Int) {
viewModel.setIsScrolling(state == ViewPager2.SCROLL_STATE_DRAGGING)
if (state == ViewPager2.SCROLL_STATE_IDLE) {
storyPager.isUserInputEnabled = true
}
}
}
companion object {
private const val ARGS = "args"
private const val HIDDEN = "hidden"
fun create(storyViewerArgs: StoryViewerArgs): Fragment {
return StoryViewerFragment().apply {
arguments = Bundle().apply {
putParcelable(ARGS, storyViewerArgs)
}
}
}
}
}