diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5c897d688..968c0c978 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -403,7 +403,7 @@ { + return PROPERTIES + } + + override fun captureStartValues(transitionValues: TransitionValues) { + if (ViewCompat.getTransitionName(transitionValues.view) == "story") { + captureValues(transitionValues) + } + } + + override fun captureEndValues(transitionValues: TransitionValues) { + if (ViewCompat.getTransitionName(transitionValues.view) == "story") { + resetValues(transitionValues.view) + captureValues(transitionValues) + } + } + + private fun captureValues(transitionValues: TransitionValues) = with(transitionValues.view) { + transitionValues.values[LAYOUT_WIDTH] = width.toFloat() + transitionValues.values[LAYOUT_HEIGHT] = height.toFloat() + transitionValues.values[POSITION_X] = x + transitionValues.values[POSITION_Y] = y + transitionValues.values[SCALE_X] = scaleX + transitionValues.values[SCALE_Y] = scaleY + } + + private fun resetValues(view: View) = with(view) { + translationX = 0f + translationY = 0f + scaleX = 1f + scaleY = 1f + } + + override fun createAnimator( + sceneRoot: ViewGroup, + start: TransitionValues?, + end: TransitionValues? + ): Animator? { + if (start == null || end == null) { + return null + } + + val startWidth = start.values[LAYOUT_WIDTH] as Float + val endWidth = end.values[LAYOUT_WIDTH] as Float + val startHeight = start.values[LAYOUT_HEIGHT] as Float + val endHeight = end.values[LAYOUT_HEIGHT] as Float + + val startX = start.values[POSITION_X] as Float + val endX = end.values[POSITION_X] as Float + val startY = start.values[POSITION_Y] as Float + val endY = end.values[POSITION_Y] as Float + + val startScaleX = start.values[SCALE_X] as Float + val startScaleY = start.values[SCALE_Y] as Float + + end.view.translationX = (startX - endX) - (endWidth - startWidth) / 2 + end.view.translationY = (startY - endY) - (endHeight - startHeight) / 2 + + end.view.scaleX = (startWidth / endWidth) * startScaleX + end.view.scaleY = (startHeight / endHeight) * startScaleY + + return ObjectAnimator.ofPropertyValuesHolder(end.view, + ofFloat(View.TRANSLATION_X, 0f), + ofFloat(View.TRANSLATION_Y, 0f), + ofFloat(View.SCALE_X, 1f), + ofFloat(View.SCALE_Y, 1f)).apply { + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + resetValues(start.view) + resetValues(end.view) + } + }) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostView.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostView.kt index d7607ab60..20bda20ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostView.kt @@ -7,17 +7,15 @@ import android.graphics.drawable.Drawable import android.util.AttributeSet import android.util.TypedValue import android.view.View -import android.widget.TextView import androidx.annotation.ColorInt import androidx.annotation.Px +import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.doOnNextLayout import androidx.core.view.isVisible -import androidx.core.widget.doAfterTextChanged import com.airbnb.lottie.SimpleColorFilter import com.google.android.material.imageview.ShapeableImageView -import org.signal.core.util.DimensionUnit import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.conversation.colors.ChatColors import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost @@ -30,7 +28,6 @@ import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationState import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryScale import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryTextWatcher import org.thoughtcrime.securesms.stories.viewer.page.StoryDisplay -import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.concurrent.ListenableFuture import org.thoughtcrime.securesms.util.visible import java.util.Locale @@ -47,26 +44,13 @@ class StoryTextPostView @JvmOverloads constructor( private var textAlignment: TextAlignment? = null private val backgroundView: ShapeableImageView = findViewById(R.id.text_story_post_background) - private val textView: TextView = findViewById(R.id.text_story_post_text) + private val textView: StoryTextView = findViewById(R.id.text_story_post_text) private val linkPreviewView: StoryLinkPreviewView = findViewById(R.id.text_story_post_link_preview) private var isPlaceholder: Boolean = true init { - backgroundView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> - textView.maxWidth = backgroundView.measuredWidth - DimensionUnit.DP.toPixels(40f).toInt() - - textAlignment?.apply { - adjustTextTranslationX(this) - } - } - TextStoryTextWatcher.install(textView) - textView.doAfterTextChanged { - textAlignment?.apply { - adjustTextTranslationX(this) - } - } val displaySize = StoryDisplay.getStoryDisplay( resources.displayMetrics.widthPixels.toFloat(), @@ -126,13 +110,7 @@ class StoryTextPostView @JvmOverloads constructor( } fun setTextBackgroundColor(@ColorInt color: Int) { - if (color == Color.TRANSPARENT) { - textView.background = null - } else { - textView.background = AppCompatResources.getDrawable(context, R.drawable.rounded_rectangle_secondary_18)?.apply { - colorFilter = SimpleColorFilter(color) - } - } + textView.setWrappedBackgroundColor(color) } fun bindFromCreationState(state: TextStoryPostCreationState) { @@ -151,7 +129,6 @@ class StoryTextPostView @JvmOverloads constructor( setTextGravity(state.textAlignment) setTextScale(state.textScale) - postAdjustTextTranslationX(state.textAlignment) postAdjustLinkPreviewTranslationY() } @@ -173,8 +150,11 @@ class StoryTextPostView @JvmOverloads constructor( hideCloseButton() - postAdjustTextTranslationX(TextAlignment.CENTER) postAdjustLinkPreviewTranslationY() + + doOnNextLayout { + (context as? AppCompatActivity)?.supportStartPostponedEnterTransition() + } } fun bindLinkPreview(linkPreview: LinkPreview?): ListenableFuture { @@ -192,12 +172,6 @@ class StoryTextPostView @JvmOverloads constructor( } } - private fun postAdjustTextTranslationX(textAlignment: TextAlignment) { - doOnNextLayout { - adjustTextTranslationX(textAlignment) - } - } - fun setTextViewClickListener(onClickListener: OnClickListener) { setOnClickListener(onClickListener) } @@ -250,34 +224,4 @@ class StoryTextPostView @JvmOverloads constructor( textView.translationY = desiredPoint - originPoint } } - - private fun alignTextLeft() { - textView.translationX = DimensionUnit.DP.toPixels(20f) - } - - private fun alignTextRight() { - textView.translationX = backgroundView.measuredWidth - textView.measuredWidth - DimensionUnit.DP.toPixels(20f) - } - - private fun adjustTextTranslationX(textAlignment: TextAlignment) { - when (textAlignment) { - TextAlignment.CENTER -> { - textView.translationX = backgroundView.measuredWidth / 2f - textView.measuredWidth / 2f - } - TextAlignment.START -> { - if (ViewUtil.isLtr(textView)) { - alignTextLeft() - } else { - alignTextRight() - } - } - TextAlignment.END -> { - if (ViewUtil.isRtl(textView)) { - alignTextLeft() - } else { - alignTextRight() - } - } - } - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextView.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextView.kt new file mode 100644 index 000000000..9419ca255 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextView.kt @@ -0,0 +1,58 @@ +package org.thoughtcrime.securesms.stories + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.RectF +import android.util.AttributeSet +import androidx.annotation.ColorInt +import org.signal.core.util.DimensionUnit +import org.thoughtcrime.securesms.components.emoji.EmojiTextView + +class StoryTextView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : EmojiTextView(context, attrs) { + + private val textBounds: RectF = RectF() + private val wrappedBackgroundPaint = Paint().apply { + style = Paint.Style.FILL + isAntiAlias = true + color = Color.TRANSPARENT + } + + init { + if (isInEditMode) { + wrappedBackgroundPaint.color = Color.RED + } + } + + fun setWrappedBackgroundColor(@ColorInt color: Int) { + wrappedBackgroundPaint.color = color + invalidate() + } + + override fun onDraw(canvas: Canvas) { + if (wrappedBackgroundPaint.color != Color.TRANSPARENT) { + textBounds.set(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat()) + + val maxWidth = (0 until layout.lineCount).map { + layout.getLineWidth(it) + }.maxOrNull() + + if (maxWidth != null) { + textBounds.inset((width - maxWidth - paddingStart - paddingEnd) / 2f, 0f) + + canvas.drawRoundRect( + textBounds, + DimensionUnit.DP.toPixels(18f), + DimensionUnit.DP.toPixels(18f), + wrappedBackgroundPaint + ) + } + } + + super.onDraw(canvas) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt index b5398f03e..ec59d5e70 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt @@ -10,6 +10,8 @@ import android.view.MenuItem import android.view.View import android.widget.Toast import androidx.activity.OnBackPressedCallback +import androidx.core.app.ActivityOptionsCompat +import androidx.core.view.ViewCompat import androidx.fragment.app.viewModels import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.BaseTransientBottomBar @@ -140,14 +142,15 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l private fun createStoryLandingItem(data: StoriesLandingItemData): StoriesLandingItem.Model { return StoriesLandingItem.Model( data = data, - onRowClick = { - if (it.data.storyRecipient.isMyStory) { + onRowClick = { model, preview -> + if (model.data.storyRecipient.isMyStory) { startActivity(Intent(requireContext(), MyStoriesActivity::class.java)) - } else if (it.data.primaryStory.messageRecord.isOutgoing && it.data.primaryStory.messageRecord.isFailed) { - lifecycleDisposable += viewModel.resend(it.data.primaryStory.messageRecord).subscribe() + } else if (model.data.primaryStory.messageRecord.isOutgoing && model.data.primaryStory.messageRecord.isFailed) { + lifecycleDisposable += viewModel.resend(model.data.primaryStory.messageRecord).subscribe() Toast.makeText(requireContext(), R.string.message_recipients_list_item__resend, Toast.LENGTH_SHORT).show() } else { - startActivity(StoryViewerActivity.createIntent(requireContext(), it.data.storyRecipient.id)) + val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), preview, ViewCompat.getTransitionName(preview) ?: "") + startActivity(StoryViewerActivity.createIntent(requireContext(), model.data.storyRecipient.id), options.toBundle()) } }, onForwardStory = { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingItem.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingItem.kt index 8783f984c..aad593329 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingItem.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingItem.kt @@ -35,7 +35,7 @@ object StoriesLandingItem { class Model( val data: StoriesLandingItemData, - val onRowClick: (Model) -> Unit, + val onRowClick: (Model, View) -> Unit, val onHideStory: (Model) -> Unit, val onForwardStory: (Model) -> Unit, val onShareStory: (Model) -> Unit, @@ -166,7 +166,7 @@ object StoriesLandingItem { } private fun setUpClickListeners(model: Model) { - itemView.setOnClickListener { model.onRowClick(model) } + itemView.setOnClickListener { model.onRowClick(model, storyPreview) } if (model.data.storyRecipient.isMyStory) { itemView.setOnLongClickListener(null) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt index 556c8dab5..8360faf7b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt @@ -1,6 +1,9 @@ package org.thoughtcrime.securesms.stories.my import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.core.app.ActivityOptionsCompat +import androidx.core.view.ViewCompat import androidx.fragment.app.viewModels import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.DSLConfiguration @@ -31,6 +34,12 @@ class MyStoriesFragment : DSLSettingsFragment( override fun bindAdapter(adapter: DSLSettingsAdapter) { MyStoriesItem.register(adapter) + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + requireActivity().finish() + } + }) + lifecycleDisposable.bindTo(viewLifecycleOwner) viewModel.state.observe(viewLifecycleOwner) { adapter.submitList(getConfiguration(it).toMappingModelList()) @@ -56,7 +65,7 @@ class MyStoriesFragment : DSLSettingsFragment( customPref( MyStoriesItem.Model( distributionStory = conversationMessage, - onClick = { + onClick = { it, preview -> if (it.distributionStory.messageRecord.isOutgoing && it.distributionStory.messageRecord.isFailed) { lifecycleDisposable += viewModel.resend(it.distributionStory.messageRecord).subscribe() Toast.makeText(requireContext(), R.string.message_recipients_list_item__resend, Toast.LENGTH_SHORT).show() @@ -67,7 +76,8 @@ class MyStoriesFragment : DSLSettingsFragment( Recipient.self().id } - startActivity(StoryViewerActivity.createIntent(requireContext(), recipientId, conversationMessage.messageRecord.id)) + val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), preview, ViewCompat.getTransitionName(preview) ?: "") + startActivity(StoryViewerActivity.createIntent(requireContext(), recipientId, conversationMessage.messageRecord.id), options.toBundle()) } }, onSaveClick = { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesItem.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesItem.kt index 3a08af107..a778c186c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesItem.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesItem.kt @@ -32,7 +32,7 @@ object MyStoriesItem { class Model( val distributionStory: ConversationMessage, - val onClick: (Model) -> Unit, + val onClick: (Model, View) -> Unit, val onSaveClick: (Model) -> Unit, val onDeleteClick: (Model) -> Unit, val onForwardClick: (Model) -> Unit, @@ -81,7 +81,7 @@ object MyStoriesItem { override fun bind(model: Model) { storyPreview.isClickable = false - itemView.setOnClickListener { model.onClick(model) } + itemView.setOnClickListener { model.onClick(model, storyPreview) } downloadTarget.setOnClickListener { model.onSaveClick(model) } moreTarget.setOnClickListener { showContextMenu(model) } presentDateOrStatus(model) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerActivity.kt index f9bc8f841..9c6652ab5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerActivity.kt @@ -16,6 +16,8 @@ class StoryViewerActivity : PassphraseRequiredActivity() { } override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { + supportPostponeEnterTransition() + super.onCreate(savedInstanceState, ready) setContentView(R.layout.fragment_container) diff --git a/app/src/main/res/layout/stories_landing_item.xml b/app/src/main/res/layout/stories_landing_item.xml index 6bd0ca989..cc3f411fa 100644 --- a/app/src/main/res/layout/stories_landing_item.xml +++ b/app/src/main/res/layout/stories_landing_item.xml @@ -102,6 +102,7 @@ android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:padding="2dp" + android:transitionName="story" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/stories_my_stories_item.xml b/app/src/main/res/layout/stories_my_stories_item.xml index 119656f91..5e4080b63 100644 --- a/app/src/main/res/layout/stories_my_stories_item.xml +++ b/app/src/main/res/layout/stories_my_stories_item.xml @@ -14,6 +14,7 @@ android:layout_height="84dp" android:layout_marginTop="12dp" android:layout_marginBottom="12dp" + android:transitionName="story" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/stories_text_post_view.xml b/app/src/main/res/layout/stories_text_post_view.xml index 7761b1386..b473af33e 100644 --- a/app/src/main/res/layout/stories_text_post_view.xml +++ b/app/src/main/res/layout/stories_text_post_view.xml @@ -12,12 +12,11 @@ android:layout_height="match_parent" app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.Story.Text" /> - + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0"> diff --git a/app/src/main/res/transition/change_transform.xml b/app/src/main/res/transition/change_transform.xml new file mode 100644 index 000000000..d1d906fda --- /dev/null +++ b/app/src/main/res/transition/change_transform.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values-v21/themes.xml b/app/src/main/res/values-v21/themes.xml index 8cc467c02..ddbf7c253 100644 --- a/app/src/main/res/values-v21/themes.xml +++ b/app/src/main/res/values-v21/themes.xml @@ -16,6 +16,12 @@ @color/core_ultramarine_light + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 89d0a2ea2..3aa760e2c 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -35,6 +35,9 @@ + +