Add thumbnail shared element animation.

main
Alex Hart 2023-02-09 14:38:48 -04:00 zatwierdzone przez Greyson Parrelli
rodzic 2c48d40375
commit d0de43a6b2
25 zmienionych plików z 484 dodań i 252 usunięć

Wyświetl plik

@ -10,10 +10,11 @@ import androidx.lifecycle.Observer;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.conversation.ConversationItem;
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode;
import org.thoughtcrime.securesms.conversation.ConversationMessage;
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode;
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselectable;
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
@ -23,6 +24,7 @@ import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@ -113,5 +115,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onViewGiftBadgeClicked(@NonNull MessageRecord messageRecord);
void onGiftBadgeRevealed(@NonNull MessageRecord messageRecord);
void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args);
}
}

Wyświetl plik

@ -97,7 +97,7 @@ public class ThreadPhotoRailView extends FrameLayout {
}
imageView.setOnClickListener(v -> {
if (clickedListener != null) clickedListener.onItemClicked(mediaRecord);
if (clickedListener != null) clickedListener.onItemClicked(imageView, mediaRecord);
});
}
@ -118,6 +118,6 @@ public class ThreadPhotoRailView extends FrameLayout {
}
public interface OnItemClickedListener {
void onItemClicked(MediaTable.MediaRecord mediaRecord);
void onItemClicked(View itemView, MediaTable.MediaRecord mediaRecord);
}
}

Wyświetl plik

@ -8,6 +8,7 @@ import android.view.View
import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityOptionsCompat
import androidx.core.util.Pair
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
import org.thoughtcrime.securesms.groups.GroupId
@ -22,6 +23,7 @@ class ConversationSettingsActivity : DSLSettingsActivity(), ConversationSettings
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
ActivityCompat.postponeEnterTransition(this)
setExitSharedElementCallback(MaterialContainerTransformSharedElementCallback())
super.onCreate(savedInstanceState, ready)
}

Wyświetl plik

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.components.settings.conversation
import android.app.ActivityOptions
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
@ -528,10 +529,13 @@ class ConversationSettingsFragment : DSLSettingsFragment(
SharedMediaPreference.Model(
mediaCursor = state.sharedMedia,
mediaIds = state.sharedMediaIds,
onMediaRecordClick = { mediaRecord, isLtr ->
onMediaRecordClick = { view, mediaRecord, isLtr ->
view.transitionName = "thumb"
val options = ActivityOptions.makeSceneTransitionAnimation(requireActivity(), view, "thumb")
startActivityForResult(
MediaIntentFactory.intentFromMediaRecord(requireContext(), mediaRecord, isLtr, allMediaInRail = true),
REQUEST_CODE_RETURN_FROM_MEDIA
REQUEST_CODE_RETURN_FROM_MEDIA,
options.toBundle()
)
}
)

Wyświetl plik

@ -24,7 +24,7 @@ object SharedMediaPreference {
class Model(
val mediaCursor: Cursor,
val mediaIds: List<Long>,
val onMediaRecordClick: (MediaTable.MediaRecord, Boolean) -> Unit
val onMediaRecordClick: (View, MediaTable.MediaRecord, Boolean) -> Unit
) : PreferenceModel<Model>() {
override fun areItemsTheSame(newItem: Model): Boolean {
return true
@ -42,8 +42,8 @@ object SharedMediaPreference {
override fun bind(model: Model) {
rail.setCursor(GlideApp.with(rail), model.mediaCursor)
rail.setListener {
model.onMediaRecordClick(it, ViewUtil.isLtr(rail))
rail.setListener { v, m ->
model.onMediaRecordClick(v, m, ViewUtil.isLtr(rail))
}
}
}

Wyświetl plik

@ -4,6 +4,7 @@ import android.content.Intent
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.view.Window
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.Toolbar
import io.reactivex.rxjava3.subjects.PublishSubject
@ -34,6 +35,8 @@ open class ConversationActivity : PassphraseRequiredActivity(), ConversationPare
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
if (savedInstanceState != null) {
shareDataTimestamp = savedInstanceState.getLong(STATE_WATERMARK, -1L)
} else if (intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) {

Wyświetl plik

@ -21,6 +21,7 @@ import android.animation.Animator;
import android.animation.LayoutTransition;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.app.ActivityOptions;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
@ -38,6 +39,7 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
@ -68,6 +70,7 @@ import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback;
import org.jetbrains.annotations.NotNull;
import org.signal.core.util.DimensionUnit;
@ -138,6 +141,8 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.longmessage.LongMessageFragment;
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder;
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
import org.thoughtcrime.securesms.mediapreview.MediaPreviewV2Activity;
import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment;
import org.thoughtcrime.securesms.messagerequests.MessageRequestState;
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
@ -2100,6 +2105,24 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
}
}
@Override
public void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args) {
if (args.isVideoGif()) {
int adapterPosition = list.getChildAdapterPosition(parent);
GiphyMp4ProjectionPlayerHolder holder = giphyMp4ProjectionRecycler.getCurrentHolder(adapterPosition);
if (holder != null) {
parent.showProjectionArea();
holder.hide();
}
}
sharedElement.setTransitionName(MediaPreviewV2Activity.SHARED_ELEMENT_TRANSITION_NAME);
requireActivity().setExitSharedElementCallback(new MaterialContainerTransformSharedElementCallback());
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(requireActivity(), sharedElement, MediaPreviewV2Activity.SHARED_ELEMENT_TRANSITION_NAME);
requireActivity().startActivity(MediaIntentFactory.create(requireActivity(), args), options.toBundle());
}
@Override
public void onActivatePaymentsClicked() {
Intent intent = new Intent(requireContext(), PaymentsActivity.class);

Wyświetl plik

@ -163,7 +163,6 @@ import kotlin.jvm.functions.Function1;
* thread. Used by ComposeMessageActivity's ListActivity via a ConversationAdapter.
*
* @author Moxie Marlinspike
*
*/
public final class ConversationItem extends RelativeLayout implements BindableConversationItem,
@ -1659,8 +1658,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
if (sharedContactStub.resolved()) sharedContactStub.get().getFooter().setVisibility(GONE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.require().getFooter().setVisibility(GONE);
if (isFooterVisible(current, next, isGroupThread))
{
if (isFooterVisible(current, next, isGroupThread)) {
ConversationItemFooter activeFooter = getActiveFooter(current);
activeFooter.setVisibility(VISIBLE);
activeFooter.setMessageRecord(current, locale);
@ -2379,8 +2377,16 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
false,
false,
MediaTable.Sorting.Newest,
slide.isVideoGif());
context.startActivity(MediaIntentFactory.create(context, args));
slide.isVideoGif(),
new MediaIntentFactory.SharedElementArgs(
slide.asAttachment().getWidth(),
slide.asAttachment().getHeight(),
mediaThumbnailStub.require().getCorners().getTopLeft(),
mediaThumbnailStub.require().getCorners().getTopRight(),
mediaThumbnailStub.require().getCorners().getBottomRight(),
mediaThumbnailStub.require().getCorners().getBottomLeft()
));
eventListener.goToMediaPreview(ConversationItem.this, mediaThumbnailStub.require(), args);
} else if (slide.getUri() != null) {
Log.i(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType());
Uri publicUri = PartAuthority.getAttachmentPublicUri(slide.getUri());

Wyświetl plik

@ -114,7 +114,10 @@ public final class GiphyMp4ProjectionRecycler implements GiphyMp4PlaybackControl
});
holder.playContent(giphyMp4Playable.getMediaItem(), giphyMp4Playable.getPlaybackPolicyEnforcer());
} else {
giphyMp4Playable.showProjectionArea();
holder.setOnPlaybackReady(() -> {
holder.show();
giphyMp4Playable.hideProjectionArea();
parent.invalidate();
});

Wyświetl plik

@ -345,7 +345,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
}
thumbnailView.setImageResource(glideRequests, slide, false, false);
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(thumbnailView, mediaRecord));
thumbnailView.setOnLongClickListener(view -> onLongClick());
}
@ -411,7 +411,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
line1.setText(fileName.orElse(fileTypeDescription));
line2.setText(getLine2(context, mediaRecord, slide));
itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(getTransitionAnchor(), mediaRecord));
itemView.setOnLongClickListener(view -> onLongClick());
selectForMarque = () -> line1.setSelected(true);
handler = new Handler(Looper.getMainLooper());
@ -459,6 +459,10 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
return fileName.orElse(null);
}
protected @NonNull View getTransitionAnchor() {
return itemView;
}
private @NonNull String describe(@NonNull Recipient from, @NonNull Recipient thread) {
if (from == Recipient.UNKNOWN && thread == Recipient.UNKNOWN) {
return fileName.orElse(fileTypeDescription);
@ -541,8 +545,8 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
audioView.setAudio((AudioSlide) slide, new AudioViewCallbacksAdapter(audioItemListener, mmsId), true, true);
audioItemListener.registerPlaybackStateObserver(audioView.getPlaybackStateObserver());
audioView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
audioView.setOnClickListener(view -> itemClickListener.onMediaClicked(audioView, mediaRecord));
itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(audioView, mediaRecord));
}
@Override
@ -584,10 +588,15 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
super.bind(context, mediaRecord, slide);
this.slide = slide;
thumbnailView.setImageResource(glideRequests, slide, false, false);
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(thumbnailView, mediaRecord));
thumbnailView.setOnLongClickListener(view -> onLongClick());
}
@Override
protected @NonNull View getTransitionAnchor() {
return thumbnailView;
}
@Override
protected String getFileTypeDescription(@NonNull Context context, @NonNull Slide slide) {
if (slide.hasVideo()) return context.getString(R.string.MediaOverviewActivity_video);
@ -648,7 +657,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
}
interface ItemClickListener {
void onMediaClicked(@NonNull MediaTable.MediaRecord mediaRecord);
void onMediaClicked(@NonNull View view, @NonNull MediaTable.MediaRecord mediaRecord);
void onMediaLongClicked(MediaTable.MediaRecord mediaRecord);
}

Wyświetl plik

@ -34,6 +34,7 @@ import androidx.viewpager.widget.ViewPager;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback;
import org.signal.libsignal.protocol.util.Pair;
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
@ -91,6 +92,7 @@ public final class MediaOverviewActivity extends PassphraseRequiredActivity {
@Override
protected void onCreate(Bundle bundle, boolean ready) {
setExitSharedElementCallback(new MaterialContainerTransformSharedElementCallback());
setContentView(R.layout.media_overview_activity);
initializeResources();

Wyświetl plik

@ -1,11 +1,13 @@
package org.thoughtcrime.securesms.mediaoverview;
import android.app.ActivityOptions;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.util.Size;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
@ -27,6 +29,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
import org.signal.core.util.DimensionUnit;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
@ -38,6 +41,7 @@ import org.thoughtcrime.securesms.database.MediaTable;
import org.thoughtcrime.securesms.database.loaders.GroupedThreadMediaLoader;
import org.thoughtcrime.securesms.database.loaders.MediaLoader;
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
import org.thoughtcrime.securesms.mediapreview.MediaPreviewV2Activity;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.BottomOffsetDecoration;
@ -196,11 +200,11 @@ public final class MediaOverviewPageFragment extends Fragment
}
@Override
public void onMediaClicked(@NonNull MediaTable.MediaRecord mediaRecord) {
public void onMediaClicked(@NonNull View view, @NonNull MediaTable.MediaRecord mediaRecord) {
if (actionMode != null) {
handleMediaMultiSelectClick(mediaRecord);
} else {
handleMediaPreviewClick(mediaRecord);
handleMediaPreviewClick(view, mediaRecord);
}
}
@ -224,7 +228,7 @@ public final class MediaOverviewPageFragment extends Fragment
}
}
private void handleMediaPreviewClick(@NonNull MediaTable.MediaRecord mediaRecord) {
private void handleMediaPreviewClick(@NonNull View view, @NonNull MediaTable.MediaRecord mediaRecord) {
if (mediaRecord.getAttachment().getUri() == null) {
return;
}
@ -249,8 +253,18 @@ public final class MediaOverviewPageFragment extends Fragment
threadId == MediaTable.ALL_THREADS,
true,
sorting,
attachment.isVideoGif());
context.startActivity(MediaIntentFactory.create(context, args));
attachment.isVideoGif(),
new MediaIntentFactory.SharedElementArgs(
attachment.getWidth(),
attachment.getHeight(),
DimensionUnit.DP.toDp(12),
DimensionUnit.DP.toDp(12),
DimensionUnit.DP.toDp(12),
DimensionUnit.DP.toDp(12)
));
view.setTransitionName(MediaPreviewV2Activity.SHARED_ELEMENT_TRANSITION_NAME);
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(requireActivity(), view, MediaPreviewV2Activity.SHARED_ELEMENT_TRANSITION_NAME);
context.startActivity(MediaIntentFactory.create(context, args), options.toBundle());
} else {
if (!MediaUtil.isAudio(attachment)) {
showFileExternally(context, mediaRecord);

Wyświetl plik

@ -7,16 +7,22 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.ZoomingImageView;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.MediaUtil;
public final class ImageMediaPreviewFragment extends MediaPreviewFragment {
private MediaPreviewPlayerControlView bottomBarControlView;
private MediaPreviewV2ViewModel viewModel;
private LifecycleDisposable lifecycleDisposable;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -24,12 +30,19 @@ public final class ImageMediaPreviewFragment extends MediaPreviewFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
ZoomingImageView zoomingImageView = (ZoomingImageView) inflater.inflate(R.layout.media_preview_image_fragment, container, false);
Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.media_preview_image_fragment, container, false);
GlideRequests glideRequests = GlideApp.with(requireActivity());
Bundle arguments = requireArguments();
Uri uri = arguments.getParcelable(DATA_URI);
String contentType = arguments.getString(DATA_CONTENT_TYPE);
ZoomingImageView zoomingImageView = view.findViewById(R.id.zooming_image_view);
viewModel = new ViewModelProvider(requireActivity()).get(MediaPreviewV2ViewModel.class);
lifecycleDisposable = new LifecycleDisposable();
lifecycleDisposable.bindTo(getViewLifecycleOwner());
if (!MediaUtil.isImageType(contentType)) {
throw new AssertionError("This fragment can only display images");
@ -40,7 +53,11 @@ public final class ImageMediaPreviewFragment extends MediaPreviewFragment {
zoomingImageView.setOnClickListener(v -> events.singleTapOnMedia());
return zoomingImageView;
lifecycleDisposable.add(viewModel.getState().distinctUntilChanged().subscribe(state -> {
zoomingImageView.setVisibility(state.isInSharedAnimation() ? View.INVISIBLE : View.VISIBLE);
}));
return view;
}
@Override

Wyświetl plik

@ -6,6 +6,7 @@ import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.signal.core.util.dp
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.database.MediaTable
import org.thoughtcrime.securesms.database.MediaTable.MediaRecord
@ -15,15 +16,16 @@ object MediaIntentFactory {
const val NOT_IN_A_THREAD = -2
const val UNKNOWN_TIMESTAMP = -2
const val THREAD_ID_EXTRA = "thread_id"
const val DATE_EXTRA = "date"
const val SIZE_EXTRA = "size"
const val CAPTION_EXTRA = "caption"
const val LEFT_IS_RECENT_EXTRA = "left_is_recent"
const val HIDE_ALL_MEDIA_EXTRA = "came_from_all_media"
const val SHOW_THREAD_EXTRA = "show_thread"
const val SORTING_EXTRA = "sorting"
const val IS_VIDEO_GIF = "is_video_gif"
@Parcelize
data class SharedElementArgs(
val width: Int = 1,
val height: Int = 1,
val topLeft: Float = 0f,
val topRight: Float = 0f,
val bottomRight: Float = 0f,
val bottomLeft: Float = 0f
) : Parcelable
@Parcelize
data class MediaPreviewArgs(
@ -38,7 +40,8 @@ object MediaIntentFactory {
val showThread: Boolean = false,
val allMediaInRail: Boolean = false,
val sorting: MediaTable.Sorting,
val isVideoGif: Boolean
val isVideoGif: Boolean,
val sharedElementArgs: SharedElementArgs = SharedElementArgs()
) : Parcelable
@JvmStatic
@ -68,7 +71,15 @@ object MediaIntentFactory {
leftIsRecent,
allMediaInRail = allMediaInRail,
sorting = MediaTable.Sorting.Newest,
isVideoGif = attachment.isVideoGif
isVideoGif = attachment.isVideoGif,
sharedElementArgs = SharedElementArgs(
attachment.width,
attachment.height,
12.dp.toFloat(),
12.dp.toFloat(),
12.dp.toFloat(),
12.dp.toFloat()
)
)
)
}

Wyświetl plik

@ -1,28 +1,104 @@
package org.thoughtcrime.securesms.mediapreview
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import androidx.core.transition.addListener
import androidx.core.view.animation.PathInterpolatorCompat
import androidx.fragment.app.commit
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import com.google.android.material.shape.ShapeAppearanceModel
import com.google.android.material.transition.platform.MaterialContainerTransform
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.util.ActionRequestListener
import org.thoughtcrime.securesms.util.LifecycleDisposable
class MediaPreviewV2Activity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner {
override lateinit var voiceNoteMediaController: VoiceNoteMediaController
private val viewModel: MediaPreviewV2ViewModel by viewModels()
private val lifecycleDisposable = LifecycleDisposable()
private lateinit var transitionImageView: ImageView
override fun attachBaseContext(newBase: Context) {
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES
super.attachBaseContext(newBase)
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
val args = MediaIntentFactory.requireArguments(intent.extras!!)
val originalCorners = ShapeAppearanceModel.Builder()
.setTopLeftCornerSize(args.sharedElementArgs.topLeft)
.setTopRightCornerSize(args.sharedElementArgs.topRight)
.setBottomRightCornerSize(args.sharedElementArgs.bottomRight)
.setBottomLeftCornerSize(args.sharedElementArgs.bottomLeft)
.build()
postponeEnterTransition()
setEnterSharedElementCallback(MaterialContainerTransformSharedElementCallback())
window.sharedElementEnterTransition = MaterialContainerTransform().apply {
addTarget(SHARED_ELEMENT_TRANSITION_NAME)
startShapeAppearanceModel = originalCorners
endShapeAppearanceModel = ShapeAppearanceModel.builder().setAllCornerSizes(0f).build()
duration = 250L
interpolator = PathInterpolatorCompat.create(0.17f, 0.17f, 0f, 1f)
addListener(
onStart = {
transitionImageView.visibility = View.VISIBLE
viewModel.setIsInSharedAnimation(true)
},
onEnd = {
transitionImageView.clearAnimation()
transitionImageView.visibility = View.INVISIBLE
viewModel.setIsInSharedAnimation(false)
}
)
}
window.sharedElementExitTransition = MaterialContainerTransform().apply {
addTarget(SHARED_ELEMENT_TRANSITION_NAME)
startShapeAppearanceModel = ShapeAppearanceModel.builder().setAllCornerSizes(0f).build()
endShapeAppearanceModel = originalCorners
duration = 250L
interpolator = PathInterpolatorCompat.create(0.17f, 0.17f, 0f, 1f)
addListener(
onStart = {
transitionImageView.visibility = View.VISIBLE
viewModel.setIsInSharedAnimation(true)
},
onEnd = {
transitionImageView.clearAnimation()
transitionImageView.visibility = View.INVISIBLE
viewModel.setIsInSharedAnimation(false)
}
)
}
super.onCreate(savedInstanceState, ready)
setTheme(R.style.TextSecure_MediaPreview)
setContentView(R.layout.activity_mediapreview_v2)
transitionImageView = findViewById(R.id.transition_image_view)
lifecycleDisposable += viewModel.state.subscribe { state ->
if (state.position in state.mediaRecords.indices) {
setTransitionImage(state.mediaRecords[state.position].attachment?.uri)
}
}
voiceNoteMediaController = VoiceNoteMediaController(this)
val systemBarColor = ContextCompat.getColor(this, R.color.signal_dark_colorSurface)
@ -31,7 +107,6 @@ class MediaPreviewV2Activity : PassphraseRequiredActivity(), VoiceNoteMediaContr
if (savedInstanceState == null) {
val bundle = Bundle()
val args = MediaIntentFactory.requireArguments(intent.extras!!)
bundle.putParcelable(MediaPreviewV2Fragment.ARGS_KEY, args)
supportFragmentManager.commit {
setReorderingAllowed(true)
@ -40,7 +115,23 @@ class MediaPreviewV2Activity : PassphraseRequiredActivity(), VoiceNoteMediaContr
}
}
private fun setTransitionImage(mediaUri: Uri?) {
if (mediaUri == null) {
GlideApp.with(this).clear(transitionImageView)
return
}
GlideApp.with(this)
.load(DecryptableStreamUriLoader.DecryptableUri(mediaUri))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.dontTransform()
.downsample(DownsampleStrategy.FIT_CENTER)
.addListener(ActionRequestListener.onEither { startPostponedEnterTransition() })
.into(transitionImageView)
}
companion object {
private const val FRAGMENT_TAG = "media_preview_fragment_v2"
const val SHARED_ELEMENT_TRANSITION_NAME = "thumb"
}
}

Wyświetl plik

@ -79,7 +79,9 @@ class MediaPreviewV2Fragment : LoggingFragment(R.layout.fragment_media_preview_v
private val lifecycleDisposable = LifecycleDisposable()
private val binding by ViewBinderDelegate(FragmentMediaPreviewV2Binding::bind)
private val viewModel: MediaPreviewV2ViewModel by viewModels()
private val viewModel: MediaPreviewV2ViewModel by viewModels(ownerProducer = {
requireActivity()
})
private val debouncer = Debouncer(2, TimeUnit.SECONDS)
private lateinit var pagerAdapter: MediaPreviewV2Adapter

Wyświetl plik

@ -12,7 +12,8 @@ data class MediaPreviewV2State(
val allMediaInAlbumRail: Boolean = false,
val leftIsRecent: Boolean = false,
val albums: Map<Long, List<Media>> = mapOf(),
val messageBodies: Map<Long, SpannableString> = mapOf()
val messageBodies: Map<Long, SpannableString> = mapOf(),
val isInSharedAnimation: Boolean = true
) {
enum class LoadState { INIT, DATA_LOADED, MEDIA_READY }
}

Wyświetl plik

@ -27,6 +27,10 @@ class MediaPreviewV2ViewModel : ViewModel() {
val currentPosition: Int
get() = store.state.position
fun setIsInSharedAnimation(isInSharedAnimation: Boolean) {
store.update { it.copy(isInSharedAnimation = isInSharedAnimation) }
}
fun fetchAttachments(context: Context, startingAttachmentId: AttachmentId, threadId: Long, sorting: MediaTable.Sorting, forceRefresh: Boolean = false) {
if (store.state.loadState == MediaPreviewV2State.LoadState.INIT || forceRefresh) {
disposables += store.update(repository.getAttachments(context, startingAttachmentId, threadId, sorting)) { result: MediaPreviewRepository.Result, oldState: MediaPreviewV2State ->

Wyświetl plik

@ -9,6 +9,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.exoplayer2.ui.PlayerControlView;
@ -16,6 +17,7 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.video.VideoPlayer;
@ -29,6 +31,9 @@ public final class VideoMediaPreviewFragment extends MediaPreviewFragment {
private VideoPlayer videoView;
private boolean isVideoGif;
private MediaPreviewV2ViewModel viewModel;
private LifecycleDisposable lifecycleDisposable;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@ -46,6 +51,13 @@ public final class VideoMediaPreviewFragment extends MediaPreviewFragment {
}
videoView = itemView.findViewById(R.id.video_player);
viewModel = new ViewModelProvider(requireActivity()).get(MediaPreviewV2ViewModel.class);
lifecycleDisposable = new LifecycleDisposable();
lifecycleDisposable.add(viewModel.getState().distinctUntilChanged().subscribe(state -> {
Log.d(TAG, "ANIM" + state.isInSharedAnimation());
itemView.setVisibility(state.isInSharedAnimation() ? View.INVISIBLE : View.VISIBLE);
}));
videoView.setWindow(requireActivity().getWindow());
videoView.setVideoSource(new VideoSlide(getContext(), uri, size, false), autoPlay, TAG);

Wyświetl plik

@ -527,7 +527,8 @@ public class AttachmentManager {
false,
false,
MediaTable.Sorting.Newest,
slide.isVideoGif());
slide.isVideoGif(),
new MediaIntentFactory.SharedElementArgs());
context.startActivity(MediaIntentFactory.create(context, args));
}
}

Wyświetl plik

@ -1,7 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/transition_image_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:importantForAccessibility="no"
android:transitionName="thumb" />
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/signal_dark_colorNeutral" />
</FrameLayout>

Wyświetl plik

@ -1,21 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.ConversationItemThumbnail
xmlns:android="http://schemas.android.com/apk/res/android"
tools:viewBindingIgnore="true"
xmlns:tools="http://schemas.android.com/tools"
<org.thoughtcrime.securesms.components.ConversationItemThumbnail xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/image_view"
android:layout_width="@dimen/media_bubble_default_dimens"
android:layout_height="@dimen/media_bubble_default_dimens"
android:scaleType="centerCrop"
android:adjustViewBounds="true"
android:contentDescription="@string/conversation_item__mms_image_description"
android:visibility="gone"
android:elevation="8dp"
app:conversationThumbnail_minWidth="@dimen/media_bubble_min_width_solo"
android:scaleType="centerCrop"
android:visibility="gone"
app:conversationThumbnail_gifWidth="@dimen/media_bubble_gif_width"
app:conversationThumbnail_maxHeight="@dimen/media_bubble_max_height"
app:conversationThumbnail_maxWidth="@dimen/media_bubble_max_width"
app:conversationThumbnail_minHeight="@dimen/media_bubble_min_height"
app:conversationThumbnail_maxHeight="@dimen/media_bubble_max_height"
app:conversationThumbnail_gifWidth="@dimen/media_bubble_gif_width"
app:conversationThumbnail_minWidth="@dimen/media_bubble_min_width_solo"
tools:src="@drawable/ic_video_light"
tools:viewBindingIgnore="true"
tools:visibility="gone" />

Wyświetl plik

@ -1,8 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.ZoomingImageView xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:viewBindingIgnore="true">
<org.thoughtcrime.securesms.components.ZoomingImageView
android:id="@+id/zooming_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:clickable="false"
android:contentDescription="@string/media_preview_activity__media_content_description" />
</FrameLayout>

Wyświetl plik

@ -1,14 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:orientation="vertical"
tools:viewBindingIgnore="true">
<org.thoughtcrime.securesms.video.VideoPlayer
android:id="@+id/video_player"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:layout_gravity="center" />
</FrameLayout>

Wyświetl plik

@ -43,6 +43,7 @@
<style name="TextSecure.MediaPreview" parent="@style/TextSecure.BaseMediaPreview">
<item name="android:navigationBarColor">@color/media_preview_bar_background</item>
<item name="android:windowContentTransitions">true</item>
</style>
<style name="Theme.Signal.Light.BottomSheetDialog" parent="Theme.Material3.Light.BottomSheetDialog">