Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import androidx.annotation.VisibleForTesting
import androidx.appcompat.widget.SearchView
import androidx.core.util.Function
import androidx.core.view.MenuItemCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
Expand Down Expand Up @@ -151,6 +152,7 @@ import com.owncloud.android.ui.helpers.FileOperationsHelper
import com.owncloud.android.ui.helpers.UriUploader
import com.owncloud.android.ui.interfaces.TransactionInterface
import com.owncloud.android.ui.navigation.NavigatorScreen
import com.owncloud.android.ui.navigation.animator.NavigationAnimator
import com.owncloud.android.ui.preview.PreviewImageActivity
import com.owncloud.android.ui.preview.PreviewImageFragment
import com.owncloud.android.ui.preview.PreviewTextFileFragment
Expand Down Expand Up @@ -280,6 +282,8 @@ class FileDisplayActivity :
*/
private var fileIDForImmediatePreview: Long = -1

private var isShowingDetailsFromPreview = false

private lateinit var folderRefreshScheduler: FolderRefreshScheduler

fun setFileIDForImmediatePreview(fileIDForImmediatePreview: Long) {
Expand Down Expand Up @@ -309,6 +313,10 @@ class FileDisplayActivity :
initUI()
initTaskRetainerFragment()

if (intent.getBooleanExtra(EXTRA_RETURN_TO_PREVIEW, false)) {
NavigationAnimator(this).prepareSlideUpEnter(savedInstanceState) { detailsHeaderImage() }
}

// Restoring after UI has been inflated.
if (savedInstanceState != null) {
showSortListGroup(savedInstanceState.getBoolean(KEY_IS_SORT_GROUP_VISIBLE))
Expand Down Expand Up @@ -446,6 +454,10 @@ class FileDisplayActivity :
createMinFragments(savedInstanceState)
}

if (savedInstanceState == null && intent.getBooleanExtra(EXTRA_RETURN_TO_PREVIEW, false)) {
handleSpecialIntents(intent)
}

upgradeNotificationForInstantUpload()
checkOutdatedServer()
checkNotifications()
Expand Down Expand Up @@ -616,6 +628,7 @@ class FileDisplayActivity :
val file = getFileFromIntent(intent)
setFile(file)
showDetails(file)
isShowingDetailsFromPreview = intent.getBooleanExtra(EXTRA_RETURN_TO_PREVIEW, false)
}

Intent.ACTION_SEARCH == action -> handleSearchIntent(intent)
Expand Down Expand Up @@ -752,6 +765,7 @@ class FileDisplayActivity :
return
}

isShowingDetailsFromPreview = false
prepareFragmentBeforeCommit(showSortListGroup)
commitFragment(fragment)
}
Expand Down Expand Up @@ -1269,6 +1283,12 @@ class FileDisplayActivity :
after()
}

isShowingDetailsFromPreview && leftFragment is FileDetailFragment -> {
before()
NavigationAnimator(this).finishWithSlideDown()
after()
}

// pop back if current fragment is AlbumItemsFragment
isAlbumItemsFragment -> {
before()
Expand Down Expand Up @@ -2062,7 +2082,12 @@ class FileDisplayActivity :
fun canPreviewInMediaPager(file: OCFile?): Boolean =
PreviewImageFragment.canBePreviewed(file) || (file != null && MimeTypeUtil.isVideo(file))

fun previewImageWithSearchContext(file: OCFile, searchFragment: Boolean, currentSearchType: SearchType?) {
fun previewImageWithSearchContext(
file: OCFile,
searchFragment: Boolean,
currentSearchType: SearchType?,
sourceView: View?
) {
val type = if (searchFragment) {
when (currentSearchType) {
SearchType.FAVORITE_SEARCH -> VirtualFolderType.FAVORITE
Expand All @@ -2080,7 +2105,7 @@ class FileDisplayActivity :
}

val showPreview = file.isDown || MimeTypeUtil.isVideo(file)
startImagePreview(file, showPreview, type, mediaState)
startImagePreview(file, showPreview, type, mediaState, sourceView)
}

fun previewFile(file: OCFile, setFabVisible: CompletionCallback?) {
Expand Down Expand Up @@ -2712,23 +2737,13 @@ class FileDisplayActivity :
file: OCFile,
showPreview: Boolean,
type: VirtualFolderType? = null,
mediaState: MediaState? = null
mediaState: MediaState? = null,
sourceView: View? = null
) {
if (user.isEmpty) {
Log_OC.e(TAG, "cannot start image preview")
return
}

val intent = Intent(this, PreviewImageActivity::class.java).apply {
putExtra(EXTRA_FILE, file)
putExtra(EXTRA_LIVE_PHOTO_FILE, file.livePhotoVideo)
putExtra(EXTRA_USER, user.get())
type?.let { putExtra(PreviewImageActivity.EXTRA_VIRTUAL_TYPE, it) }
mediaState?.let { putExtra(PreviewImageActivity.EXTRA_MEDIA_STATE, it) }
}
val intent = imagePreviewIntent(file, type, mediaState) ?: return

if (showPreview) {
startActivity(intent)
NavigationAnimator(this).scaleUp(intent, sourceView)
} else {
val helper = FileOperationsHelper(
this,
Expand All @@ -2740,6 +2755,21 @@ class FileDisplayActivity :
}
}

private fun imagePreviewIntent(file: OCFile, type: VirtualFolderType?, mediaState: MediaState?): Intent? {
if (user.isEmpty) {
Log_OC.e(TAG, "cannot start image preview")
return null
}

return Intent(this, PreviewImageActivity::class.java).apply {
putExtra(EXTRA_FILE, file)
putExtra(EXTRA_LIVE_PHOTO_FILE, file.livePhotoVideo)
putExtra(EXTRA_USER, user.get())
type?.let { putExtra(PreviewImageActivity.EXTRA_VIRTUAL_TYPE, it) }
mediaState?.let { putExtra(PreviewImageActivity.EXTRA_MEDIA_STATE, it) }
}
}

/**
* Starts the preview of a media [OCFile], synchronizing it first when it is not available yet.
*/
Expand Down Expand Up @@ -2967,13 +2997,21 @@ class FileDisplayActivity :
file,
true,
virtualType,
bundle.getSerializableArgument(PreviewImageActivity.EXTRA_MEDIA_STATE, MediaState::class.java)
bundle.getSerializableArgument(PreviewImageActivity.EXTRA_MEDIA_STATE, MediaState::class.java),
galleryThumbnailOf(file)
)
} else {
startImagePreview(file, true)
startImagePreview(file, true, sourceView = galleryThumbnailOf(file))
}
}

private fun detailsHeaderImage(): View? =
previewImageView?.takeIf { it.isShown && it.isLaidOut && it.drawable != null }

private fun galleryThumbnailOf(file: OCFile): View? = leftFragment?.view
?.findViewWithTag<View>(file.fileId)
?.takeIf { it.isShown && ViewCompat.getTransitionName(it) == NavigationAnimator.sharedElementName(file) }

@Subscribe(threadMode = ThreadMode.BACKGROUND)
fun onMessageEvent(event: TokenPushEvent?) {
if (!preferences.isKeysReInitEnabled()) {
Expand Down Expand Up @@ -3323,6 +3361,7 @@ class FileDisplayActivity :
private const val SEARCH_VIEW_FOCUS_DELAY = 100L

const val ACTION_DETAILS: String = "com.owncloud.android.ui.activity.action.DETAILS"
const val EXTRA_RETURN_TO_PREVIEW: String = "RETURN_TO_PREVIEW"

@JvmField
val REQUEST_CODE__SELECT_CONTENT_FROM_APPS: Int = REQUEST_CODE__LAST_SHARED + 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import android.content.Context
import android.view.View
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import com.elyeproj.loaderviewlibrary.LoaderImageView
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.client.account.User
Expand Down Expand Up @@ -39,6 +40,7 @@ import com.owncloud.android.ui.activity.FolderPickerActivity
import com.owncloud.android.ui.fragment.SearchType
import com.owncloud.android.ui.fragment.albums.AlbumItemsFragment
import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface
import com.owncloud.android.ui.navigation.animator.NavigationAnimator
import com.owncloud.android.utils.EncryptionUtils
import com.owncloud.android.utils.MimeTypeUtil
import com.owncloud.android.utils.theme.ViewThemeUtils
Expand Down Expand Up @@ -120,6 +122,7 @@ class OCFileListDelegate(
GalleryImageGenerationJob.cancelPreviousJob(imageView)

imageView.tag = file.fileId
ViewCompat.setTransitionName(imageView, NavigationAnimator.sharedElementName(file))

val cached = file.takeUnless { it.isUpdateThumbnailNeeded }?.getBigThumbnail()
if (cached != null) {
Expand Down Expand Up @@ -170,7 +173,7 @@ class OCFileListDelegate(
file
)
} else {
ocFileListFragmentInterface.onItemClicked(file)
ocFileListFragmentInterface.onItemClicked(file, imageView)
AlbumItemsFragment.lastMediaItemPosition = galleryRowHolder.absoluteAdapterPosition
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1184,7 +1184,7 @@ private Integer checkFileBeforeOpen(OCFile file) {
}
}

private void fileOnItemClick(OCFile file) {
private void fileOnItemClick(OCFile file, @Nullable View sourceView) {
Integer errorMessageId = checkFileBeforeOpen(file);
final var recyclerView = getRecyclerView();
if (recyclerView != null && errorMessageId != null) {
Expand All @@ -1193,7 +1193,7 @@ private void fileOnItemClick(OCFile file) {
}

if (mContainerActivity instanceof FileDisplayActivity fda && fda.canPreviewInMediaPager(file)) {
fda.previewImageWithSearchContext(file, searchFragment, currentSearchType);
fda.previewImageWithSearchContext(file, searchFragment, currentSearchType, sourceView);
} else if (file.isDown() && mContainerActivity instanceof FileDisplayActivity fda) {
fda.previewFile(file, this::setFabVisible);
} else {
Expand Down Expand Up @@ -1254,8 +1254,13 @@ private void handlePendingDownloadFile(OCFile file) {
}

@Override
@OptIn(markerClass = UnstableApi.class)
public void onItemClicked(OCFile file) {
onItemClicked(file, null);
}

@Override
@OptIn(markerClass = UnstableApi.class)
public void onItemClicked(OCFile file, @Nullable View sourceView) {
if (getCommonAdapter() != null && getCommonAdapter().isMultiSelect()) {
toggleItemToCheckedList(file);
} else {
Expand All @@ -1273,7 +1278,7 @@ public void onItemClicked(OCFile file) {
requireActivity().setResult(Activity.RESULT_OK, intent);
requireActivity().finish();
} else if (!mOnlyFoldersClickable) {
fileOnItemClick(file);
fileOnItemClick(file, sourceView);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public interface OCFileListFragmentInterface {

void onItemClicked(OCFile file);

default void onItemClicked(OCFile file, View sourceView) {
onItemClicked(file);
}

boolean onLongItemClicked(OCFile file);

boolean isLoading();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.owncloud.android.ui.navigation.animator

import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.transition.Slide
import android.view.Gravity
import android.view.View
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityOptionsCompat
import androidx.core.app.SharedElementCallback
import androidx.core.view.ViewCompat
import com.owncloud.android.R
import com.owncloud.android.datamodel.OCFile

class NavigationAnimator(private val activity: AppCompatActivity) {

private val isLaunchedWithSharedElement: Boolean
get() = activity.intent.getBooleanExtra(EXTRA_HAS_SHARED_ELEMENT, false)

fun slideUp(intent: Intent, sharedView: View? = null) {
if (startWithSharedElement(intent, sharedView)) {
return
}

val options = ActivityOptionsCompat.makeCustomAnimation(activity, R.anim.slide_up, R.anim.hold)
activity.startActivity(intent, options.toBundle())
}

fun prepareSlideUpEnter(savedInstanceState: Bundle?, sharedViewProvider: () -> View?) {
if (!isLaunchedWithSharedElement) {
return
}

activity.window.enterTransition = bottomSlide()
activity.window.returnTransition = bottomSlide()

val sharedElementTransition = SharedElementTransition(
activity,
startScaleType = ImageView.ScaleType.FIT_CENTER,
sharedViewProvider = sharedViewProvider
)
sharedElementTransition.register()
if (savedInstanceState == null) {
sharedElementTransition.postponeUntilSharedViewReady()
}
}

fun finishWithSlideDown() {
if (isLaunchedWithSharedElement) {
activity.setEnterSharedElementCallback(withoutSharedElements())
activity.supportFinishAfterTransition()
return
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
activity.overrideActivityTransition(Activity.OVERRIDE_TRANSITION_CLOSE, R.anim.hold, R.anim.slide_down)
activity.finish()
return
}

activity.finish()
@Suppress("DEPRECATION")
activity.overridePendingTransition(R.anim.hold, R.anim.slide_down)
}

fun scaleUp(intent: Intent, sourceView: View?) {
if (!startWithSharedElement(intent, sourceView)) {
activity.startActivity(intent)
}
}

fun finishWithScaleDown() {
activity.supportFinishAfterTransition()
}

private fun startWithSharedElement(intent: Intent, sharedView: View?): Boolean {
val sharedElementName = sharedView?.let { ViewCompat.getTransitionName(it) } ?: return false

intent.putExtra(EXTRA_HAS_SHARED_ELEMENT, true)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, sharedView, sharedElementName)
activity.startActivity(intent, options.toBundle())
return true
}

private fun withoutSharedElements() = object : SharedElementCallback() {
override fun onMapSharedElements(names: MutableList<String>, sharedElements: MutableMap<String, View>) {
names.clear()
sharedElements.clear()
}
}

private fun bottomSlide() = Slide(Gravity.BOTTOM).apply {
excludeTarget(android.R.id.statusBarBackground, true)
excludeTarget(android.R.id.navigationBarBackground, true)
}

companion object {
const val EXTRA_HAS_SHARED_ELEMENT = "HAS_SHARED_ELEMENT"
private const val SHARED_ELEMENT_NAME_PREFIX = "file_"

fun sharedElementName(file: OCFile): String = SHARED_ELEMENT_NAME_PREFIX + file.fileId
}
}
Loading