Update first time navigation screen.

This commit is contained in:
Alex Hart
2022-10-27 13:43:52 -03:00
committed by GitHub
parent d003dc435a
commit 3600a4818c
18 changed files with 303 additions and 229 deletions

View File

@@ -516,6 +516,14 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
}
)
clickPref(
title = DSLSettingsText.from("Clear first time navigation state"),
isEnabled = true,
onClick = {
SignalStore.storyValues().userHasSeenFirstNavView = false
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_stories_dialog_launcher),
onClick = {

View File

@@ -1,7 +1,8 @@
package org.thoughtcrime.securesms.stories
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
@@ -10,14 +11,13 @@ import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintLayout
import com.airbnb.lottie.LottieAnimationView
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.components.CornerMask
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.util.visible
@@ -28,20 +28,22 @@ class StoryFirstTimeNavigationView @JvmOverloads constructor(
companion object {
private const val BLUR_ALPHA = 0x3D
private const val NO_BLUR_ALPHA = 0xB3
private const val NO_BLUR_ALPHA = 0xCC
}
init {
inflate(context, R.layout.story_first_time_navigation_view, this)
}
private val tapToAdvance: LottieAnimationView = findViewById(R.id.edu_tap_icon)
private val swipeUp: LottieAnimationView = findViewById(R.id.edu_swipe_up_icon)
private val swipeRight: LottieAnimationView = findViewById(R.id.edu_swipe_right_icon)
private val blurHashView: ImageView = findViewById(R.id.edu_blur_hash)
private val overlayView: ImageView = findViewById(R.id.edu_overlay)
private val gotIt: View = findViewById(R.id.edu_got_it)
private val close: View = findViewById(R.id.edu_close)
private val cornerMask = CornerMask(this).apply {
setRadius(DimensionUnit.DP.toPixels(18f).toInt())
}
private var isPlayingAnimations = false
var callback: Callback? = null
@@ -59,12 +61,14 @@ class StoryFirstTimeNavigationView @JvmOverloads constructor(
hide()
}
setOnClickListener { }
}
close.setOnClickListener {
callback?.onCloseClicked()
GlideApp.with(this).clear(blurHashView)
blurHashView.setImageDrawable(null)
hide()
}
override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas)
cornerMask.mask(canvas)
setOnClickListener { }
}
fun setBlurHash(blurHash: BlurHash?) {
@@ -105,10 +109,47 @@ class StoryFirstTimeNavigationView @JvmOverloads constructor(
}
visible = true
startLottieAnimations()
}
fun hide() {
visible = false
endLottieAnimations()
}
private fun startLottieAnimations() {
isPlayingAnimations = true
tapToAdvance.addAnimatorListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
if (isPlayingAnimations) {
swipeUp.playAnimation()
}
}
})
swipeUp.addAnimatorListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
if (isPlayingAnimations) {
swipeRight.playAnimation()
}
}
})
swipeRight.addAnimatorListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
postDelayed({
if (isPlayingAnimations) {
startLottieAnimations()
}
}, 300)
}
})
tapToAdvance.playAnimation()
}
private fun endLottieAnimations() {
isPlayingAnimations = false
}
private fun isRenderEffectSupported(): Boolean {
@@ -118,5 +159,6 @@ class StoryFirstTimeNavigationView @JvmOverloads constructor(
interface Callback {
fun userHasSeenFirstNavigationView(): Boolean
fun onGotItClicked()
fun onCloseClicked()
}
}

View File

@@ -6,6 +6,7 @@ import android.media.AudioManager
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatDelegate
import androidx.media.AudioManagerCompat
@@ -16,7 +17,9 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
import org.thoughtcrime.securesms.stories.StoryViewerArgs
import org.thoughtcrime.securesms.util.FullscreenHelper
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.ViewUtil
import kotlin.math.max
import kotlin.math.min
@@ -34,9 +37,18 @@ class StoryViewerActivity : PassphraseRequiredActivity(), VoiceNoteMediaControll
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
StoryMutePolicy.initialize()
Glide.get(this).setMemoryCategory(MemoryCategory.HIGH)
FullscreenHelper.showSystemUI(window)
supportPostponeEnterTransition()
val root = findViewById<View>(android.R.id.content)
root.setPadding(
0,
ViewUtil.getStatusBarHeight(root),
0,
ViewUtil.getNavigationBarHeight(root)
)
super.onCreate(savedInstanceState, ready)
setContentView(R.layout.fragment_container)

View File

@@ -1,5 +1,8 @@
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
@@ -8,9 +11,11 @@ import androidx.fragment.app.viewModels
import androidx.viewpager2.widget.ViewPager2
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
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
@@ -116,6 +121,20 @@ class StoryViewerFragment :
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)
}
}
}
}

View File

@@ -53,13 +53,20 @@ class StoryViewerViewModel(
var hasConsumedInitialState = false
private set
private val firstTimeNavigationPublisher: BehaviorSubject<Boolean> = BehaviorSubject.createDefault(false)
val isChildScrolling: Observable<Boolean> = childScrollStatePublisher.distinctUntilChanged()
val isFirstTimeNavigationShowing: Observable<Boolean> = firstTimeNavigationPublisher.distinctUntilChanged()
fun addHiddenAndRefresh(hidden: Set<RecipientId>) {
this.hidden.addAll(hidden)
refresh()
}
fun setIsDisplayingFirstTimeNavigation(isDisplayingFirstTimeNavigation: Boolean) {
firstTimeNavigationPublisher.onNext(isDisplayingFirstTimeNavigation)
}
fun getHidden(): Set<RecipientId> = hidden
fun setCrossfadeTarget(messageRecord: MmsMessageRecord) {

View File

@@ -0,0 +1,86 @@
package org.thoughtcrime.securesms.stories.viewer.first
import android.app.Dialog
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import androidx.core.app.ActivityCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.stories.StoryFirstTimeNavigationView
import org.thoughtcrime.securesms.stories.viewer.StoryViewerState
import org.thoughtcrime.securesms.stories.viewer.StoryViewerViewModel
import org.thoughtcrime.securesms.util.LifecycleDisposable
class StoryFirstTimeNavigationFragment : DialogFragment(R.layout.story_viewer_first_time_navigation_stub), StoryFirstTimeNavigationView.Callback {
private val viewModel: StoryViewerViewModel by viewModels(ownerProducer = {
requireParentFragment()
})
private val disposables = LifecycleDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen)
isCancelable = false
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
if (Build.VERSION.SDK_INT >= 21) {
dialog.window!!.addFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION or
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS or
WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS or
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
)
}
return dialog
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
(view as StoryFirstTimeNavigationView).show()
view.callback = this
viewModel.setIsDisplayingFirstTimeNavigation(true)
disposables += viewModel.state.subscribe { state ->
when (state.crossfadeSource) {
is StoryViewerState.CrossfadeSource.ImageUri -> {
view.setBlurHash(state.crossfadeSource.imageBlur)
}
else -> {
view.setBlurHash(null)
}
}
}
}
override fun userHasSeenFirstNavigationView(): Boolean {
return SignalStore.storyValues().userHasSeenFirstNavView
}
override fun onGotItClicked() {
dismissAllowingStateLoss()
SignalStore.storyValues().userHasSeenFirstNavView = true
viewModel.setIsDisplayingFirstTimeNavigation(false)
}
override fun onCloseClicked() {
dismissAllowingStateLoss()
if (viewModel.stateSnapshot.skipCrossfade) {
requireActivity().finish()
} else {
ActivityCompat.finishAfterTransition(requireActivity())
}
}
}

View File

@@ -1,61 +0,0 @@
package org.thoughtcrime.securesms.stories.viewer.page
import android.view.ViewStub
import androidx.core.view.isVisible
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.stories.StoryFirstTimeNavigationView
import org.thoughtcrime.securesms.util.views.Stub
/**
* Specialized stub that allows for early arrival of the blurhash and callback.
*/
class StoryFirstNavigationStub(viewStub: ViewStub) : Stub<StoryFirstTimeNavigationView>(viewStub) {
private var callback: StoryFirstTimeNavigationView.Callback? = null
private var blurHash: BlurHash? = null
fun setCallback(callback: StoryFirstTimeNavigationView.Callback) {
if (resolved()) {
get().callback = callback
} else {
this.callback = callback
}
}
fun setBlurHash(blurHash: BlurHash?) {
if (resolved()) {
get().setBlurHash(blurHash)
} else {
this.blurHash = blurHash
}
}
fun showIfAble(ableToShow: Boolean) {
if (ableToShow) {
get().show()
}
}
fun isVisible(): Boolean {
return resolved() && get().isVisible
}
fun hide() {
if (resolved()) {
get().hide()
}
}
override fun get(): StoryFirstTimeNavigationView {
val needsResolve = !resolved()
val view = super.get()
if (needsResolve) {
view.setBlurHash(blurHash)
view.callback = callback
blurHash = null
callback = null
}
return view
}
}

View File

@@ -6,11 +6,8 @@ import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.RenderEffect
import android.graphics.Shader
import android.graphics.drawable.Drawable
import android.media.AudioManager
import android.os.Build
import android.os.Bundle
import android.text.method.ScrollingMovementMethod
import android.view.GestureDetector
@@ -55,14 +52,12 @@ import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectFor
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment
import org.thoughtcrime.securesms.mediapreview.VideoControlsDelegate
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
import org.thoughtcrime.securesms.stories.StoryFirstTimeNavigationView
import org.thoughtcrime.securesms.stories.StorySlateView
import org.thoughtcrime.securesms.stories.StoryVolumeOverlayView
import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu
@@ -97,7 +92,6 @@ class StoryViewerPageFragment :
StoryPostFragment.Callback,
MultiselectForwardBottomSheet.Callback,
StorySlateView.Callback,
StoryFirstTimeNavigationView.Callback,
StoryInfoBottomSheetDialogFragment.OnInfoSheetDismissedListener,
SafetyNumberBottomSheet.Callbacks {
@@ -108,7 +102,7 @@ class StoryViewerPageFragment :
private lateinit var viewsAndReplies: MaterialButton
private lateinit var storyCaptionContainer: FrameLayout
private lateinit var storyContentContainer: FrameLayout
private lateinit var storyFirstTimeNavigationViewStub: StoryFirstNavigationStub
private lateinit var storyPageContainer: ConstraintLayout
private lateinit var sendingBarTextView: TextView
private lateinit var sendingBar: View
@@ -176,17 +170,16 @@ class StoryViewerPageFragment :
val storyGradientBottom: View = view.findViewById(R.id.story_gradient_bottom)
val storyVolumeOverlayView: StoryVolumeOverlayView = view.findViewById(R.id.story_volume_overlay)
storyPageContainer = view.findViewById(R.id.story_page_container)
storyContentContainer = view.findViewById(R.id.story_content_container)
storyCaptionContainer = view.findViewById(R.id.story_caption_container)
storySlate = view.findViewById(R.id.story_slate)
progressBar = view.findViewById(R.id.progress)
viewsAndReplies = view.findViewById(R.id.views_and_replies_bar)
storyFirstTimeNavigationViewStub = StoryFirstNavigationStub(view.findViewById(R.id.story_first_time_nav_stub))
sendingBarTextView = view.findViewById(R.id.sending_text_view)
sendingBar = view.findViewById(R.id.sending_bar)
storySlate.callback = this
storyFirstTimeNavigationViewStub.setCallback(this)
chrome = listOf(
closeView,
@@ -322,6 +315,10 @@ class StoryViewerPageFragment :
viewModel.setIsUserScrollingChild(it)
}
lifecycleDisposable += sharedViewModel.isFirstTimeNavigationShowing.subscribe {
viewModel.setIsDisplayingFirstTimeNavigation(it)
}
lifecycleDisposable += storyVolumeViewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe { volumeState ->
if (volumeState.isMuted) {
videoControlsDelegate.mute()
@@ -384,7 +381,6 @@ class StoryViewerPageFragment :
presentDate(date, post)
presentDistributionList(distributionList, post)
presentCaption(caption, largeCaption, largeCaptionOverlay, post)
presentBlur(post)
val durations: Map<Int, Long> = state.posts
.mapIndexed { index, storyPost ->
@@ -428,37 +424,20 @@ class StoryViewerPageFragment :
resumeProgress()
}
val wasDisplayingNavigationView = storyFirstTimeNavigationViewStub.isVisible()
when {
state.hideChromeImmediate -> {
hideChromeImmediate()
storyCaptionContainer.visible = false
storyFirstTimeNavigationViewStub.hide()
}
state.hideChrome -> {
hideChrome()
storyCaptionContainer.visible = true
storyFirstTimeNavigationViewStub.showIfAble(!SignalStore.storyValues().userHasSeenFirstNavView)
}
else -> {
showChrome()
storyCaptionContainer.visible = true
storyFirstTimeNavigationViewStub.showIfAble(!SignalStore.storyValues().userHasSeenFirstNavView)
}
}
val isDisplayingNavigationView = storyFirstTimeNavigationViewStub.isVisible()
if (isDisplayingNavigationView && Build.VERSION.SDK_INT >= 31) {
hideChromeImmediate()
storyContentContainer.setRenderEffect(RenderEffect.createBlurEffect(100f, 100f, Shader.TileMode.CLAMP))
} else if (Build.VERSION.SDK_INT >= 31) {
storyContentContainer.setRenderEffect(null)
}
if (wasDisplayingNavigationView xor isDisplayingNavigationView) {
viewModel.setIsDisplayingFirstTimeNavigation(storyFirstTimeNavigationViewStub.isVisible())
}
}
timeoutDisposable.bindTo(viewLifecycleOwner)
@@ -590,7 +569,7 @@ class StoryViewerPageFragment :
card: CardView
) {
val constraintSet = ConstraintSet()
constraintSet.clone(requireView() as ConstraintLayout)
constraintSet.clone(storyPageContainer)
when (StoryDisplay.getStoryDisplay(resources.displayMetrics.widthPixels.toFloat(), resources.displayMetrics.heightPixels.toFloat())) {
StoryDisplay.LARGE -> {
@@ -613,7 +592,7 @@ class StoryViewerPageFragment :
}
}
constraintSet.applyTo(requireView() as ConstraintLayout)
constraintSet.applyTo(storyPageContainer)
}
private fun resumeProgress() {
@@ -781,13 +760,6 @@ class StoryViewerPageFragment :
distributionList.visible = storyPost.distributionList != null && !storyPost.distributionList.isMyStory
}
private fun presentBlur(storyPost: StoryPost) {
val record = storyPost.conversationMessage.messageRecord as? MediaMmsMessageRecord
val blurHash = record?.slideDeck?.thumbnailSlide?.placeholderBlur
storyFirstTimeNavigationViewStub.setBlurHash(blurHash)
}
@SuppressLint("SetTextI18n")
private fun presentCaption(caption: TextView, largeCaption: TextView, largeCaptionOverlay: View, storyPost: StoryPost) {
val displayBody: String = if (storyPost.content is StoryPost.Content.AttachmentContent) {
@@ -1284,15 +1256,6 @@ class StoryViewerPageFragment :
sharedViewModel.setContentIsReady()
}
override fun userHasSeenFirstNavigationView(): Boolean {
return SignalStore.storyValues().userHasSeenFirstNavView
}
override fun onGotItClicked() {
SignalStore.storyValues().userHasSeenFirstNavView = true
viewModel.setIsDisplayingFirstTimeNavigation(false)
}
override fun onInfoSheetDismissed() {
viewModel.setIsDisplayingInfoDialog(false)
}

View File

@@ -24,7 +24,7 @@ data class StoryViewerPlaybackState(
val isUserScaling: Boolean = false,
val isDisplayingPartialSendDialog: Boolean = false
) {
val hideChromeImmediate: Boolean = isRunningSharedElementAnimation
val hideChromeImmediate: Boolean = isRunningSharedElementAnimation || isDisplayingFirstTimeNavigation
val hideChrome: Boolean = isRunningSharedElementAnimation ||
isUserLongTouching ||