diff --git a/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt b/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt index 142c6edfa009..cdcc3506e018 100644 --- a/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt +++ b/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt @@ -19,52 +19,75 @@ import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.utils.BitmapUtils import com.owncloud.android.utils.MimeTypeUtil +@Suppress("TooGenericExceptionCaught", "ReturnCount") object OCFileUtils { private const val TAG = "OCFileUtils" - @Suppress("ReturnCount", "NestedBlockDepth") fun getImageSize(ocFile: OCFile, defaultThumbnailSize: Float): Pair { + val fallback = defaultThumbnailSize.toInt().coerceAtLeast(1) + val fallbackPair = fallback to fallback + try { Log_OC.d(TAG, "Getting image size for: ${ocFile.fileName}") - val widthFromDimension = ocFile.imageDimension?.width - val heightFromDimension = ocFile.imageDimension?.height - if (widthFromDimension != null && heightFromDimension != null) { - val width = widthFromDimension.toInt() - val height = heightFromDimension.toInt() - Log_OC.d(TAG, "Image dimensions are used, width: $width, height: $height") - return width to height + // Server-provided + ocFile.imageDimension?.let { dim -> + val w = dim.width.toInt().coerceAtLeast(1) + val h = dim.height.toInt().coerceAtLeast(1) + Log_OC.d(TAG, "Using server-provided imageDimension: $w x $h") + return w to h } - return if (ocFile.exists()) { - val exif = ExifInterface(ocFile.storagePath) - val width = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0) - val height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0) - - if (width > 0 && height > 0) { - Log_OC.d(TAG, "Exif used width: $width and height: $height") - width to height - } - - val (bitmapWidth, bitmapHeight) = BitmapUtils.getImageResolution(ocFile.storagePath) - .let { it[0] to it[1] } - - if (bitmapWidth > 0 && bitmapHeight > 0) { - Log_OC.d(TAG, "BitmapUtils.getImageResolution used width: $bitmapWidth and height: $bitmapHeight") - bitmapWidth to bitmapHeight - } - - val fallback = defaultThumbnailSize.toInt().coerceAtLeast(1) - Log_OC.d(TAG, "Default size used width: $fallback and height: $fallback") - fallback to fallback - } else { - Log_OC.d(TAG, "Default size is used: $defaultThumbnailSize") - val size = defaultThumbnailSize.toInt().coerceAtLeast(1) - size to size + // Local file + val path = ocFile.storagePath + if (!path.isNullOrEmpty() && ocFile.exists()) { + getExifSize(path)?.let { return it } + getBitmapSize(path)?.let { return it } } - } finally { - Log_OC.d(TAG, "-----------------------------") + + // 3 Fallback + Log_OC.d(TAG, "Fallback to default size: $fallback x $fallback") + return fallbackPair + } catch (e: Exception) { + Log_OC.e(TAG, "Error getting image size for ${ocFile.fileName}", e) + } + + return fallbackPair + } + + private fun getExifSize(path: String): Pair? = try { + val exif = ExifInterface(path) + var w = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0) + var h = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0) + + val orientation = exif.getAttributeInt( + ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_NORMAL + ) + if (orientation == ExifInterface.ORIENTATION_ROTATE_90 || + orientation == ExifInterface.ORIENTATION_ROTATE_270 + ) { + val tmp = w + w = h + h = tmp } + + Log_OC.d(TAG, "Using exif imageDimension: $w x $h") + if (w > 0 && h > 0) w to h else null + } catch (_: Exception) { + null + } + + private fun getBitmapSize(path: String): Pair? = try { + val options = android.graphics.BitmapFactory.Options().apply { inJustDecodeBounds = true } + android.graphics.BitmapFactory.decodeFile(path, options) + val w = options.outWidth + val h = options.outHeight + + Log_OC.d(TAG, "Using bitmap factory imageDimension: $w x $h") + if (w > 0 && h > 0) w to h else null + } catch (_: Exception) { + null } fun getMediaPlaceholder(file: OCFile, imageDimension: Pair): BitmapDrawable { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt index 877ed965f7e0..fead84f34822 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt @@ -10,7 +10,6 @@ package com.owncloud.android.ui.adapter import android.view.Gravity import android.view.View -import android.view.ViewGroup import android.widget.FrameLayout import android.widget.ImageView import androidx.core.content.ContextCompat @@ -20,15 +19,12 @@ import com.elyeproj.loaderviewlibrary.LoaderImageView import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.utils.OCFileUtils import com.nextcloud.utils.extensions.makeRounded -import com.nextcloud.utils.extensions.mediaSize import com.nextcloud.utils.extensions.setVisibleIf import com.owncloud.android.R import com.owncloud.android.databinding.GalleryRowBinding import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.GalleryRow import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.lib.resources.files.model.ImageDimension -import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.theme.ViewThemeUtils @Suppress("LongParameterList") @@ -37,7 +33,7 @@ class GalleryRowHolder( private val defaultThumbnailSize: Float, private val ocFileListDelegate: OCFileListDelegate, val storageManager: FileDataStorageManager, - private val galleryAdapter: GalleryAdapter, + galleryAdapter: GalleryAdapter, private val viewThemeUtils: ViewThemeUtils ) : SectionedViewHolder(binding.root) { val context = galleryAdapter.context @@ -71,25 +67,25 @@ class GalleryRowHolder( // Only rebuild if file count changed if (lastFileCount != requiredCount) { binding.rowLayout.removeAllViews() - for (file in row.files) { - val rowLayout = getRowLayout(file) - binding.rowLayout.addView(rowLayout) + row.files.forEach { file -> + binding.rowLayout.addView(getRowLayout(file)) } lastFileCount = requiredCount } - val shrinkRatio = computeShrinkRatio(row) + val dimensions = getDimensions(row) for (i in row.files.indices) { - adjustFile(i, row.files[i], shrinkRatio, row) + val dim = dimensions.getOrNull(i) ?: (defaultThumbnailSize.toInt() to defaultThumbnailSize.toInt()) + adjustFile(i, row.files[i], dim, row) } } - fun updateRowVisuals() { - bind(currentRow) - } + fun updateRowVisuals() = bind(currentRow) private fun getRowLayout(file: OCFile): FrameLayout { + val (width, height) = OCFileUtils.getImageSize(file, defaultThumbnailSize) + val checkbox = ImageView(context).apply { visibility = View.GONE layoutParams = FrameLayout.LayoutParams( @@ -102,20 +98,17 @@ class GalleryRowHolder( } } - val mediaSize = file.mediaSize(defaultThumbnailSize) - val (width, height) = mediaSize - val shimmer = LoaderImageView(context).apply { setImageResource(R.drawable.background) resetLoader() layoutParams = FrameLayout.LayoutParams(width, height) } - val drawable = OCFileUtils.getMediaPlaceholder(file, mediaSize) + val drawable = OCFileUtils.getMediaPlaceholder(file, width to height) val rowCellImageView = ImageView(context).apply { setImageDrawable(drawable) adjustViewBounds = true - scaleType = ImageView.ScaleType.FIT_XY + scaleType = ImageView.ScaleType.CENTER_CROP layoutParams = FrameLayout.LayoutParams(width, height) } @@ -126,101 +119,66 @@ class GalleryRowHolder( } } - @SuppressWarnings("MagicNumber") - private fun computeShrinkRatio(row: GalleryRow): Float { - val screenWidth = DisplayUtils.convertDpToPixel( - context.resources.configuration.screenWidthDp.toFloat(), - context - ).toFloat() - - return if (row.files.size > 1) { - computeMultiFileShrinkRatio(row, screenWidth) - } else { - computeSingleFileShrinkRatio(row, screenWidth) + private fun getDimensions(row: GalleryRow): List> { + val screenWidthPx = context.resources.displayMetrics.widthPixels.toFloat() + val marginPx = smallMargin.toFloat() + val totalMargins = marginPx * (row.files.size - 1) + val availableWidth = screenWidthPx - totalMargins + + val aspectRatios = row.files.map { file -> + val (w, h) = OCFileUtils.getImageSize(file, defaultThumbnailSize) + if (h > 0) w.toFloat() / h else 1.0f } - } - private fun computeMultiFileShrinkRatio(row: GalleryRow, screenWidth: Float): Float { - val targetHeight = row.getMaxHeight() - var totalUnscaledWidth = 0f + val sumAspectRatios = aspectRatios.sum() - for (file in row.files) { - val (originalWidth, originalHeight) = OCFileUtils.getImageSize(file, defaultThumbnailSize) + // calculate row height based on aspect ratios + val rowHeightFloat = if (sumAspectRatios > 0) availableWidth / sumAspectRatios else defaultThumbnailSize + val finalHeight = rowHeightFloat.toInt() - val scaledWidth = targetHeight * (originalWidth.toFloat() / originalHeight) - file.imageDimension = ImageDimension(scaledWidth, targetHeight) + // for each aspect ratio calculate widths + val finalWidths = aspectRatios.map { ratio -> (rowHeightFloat * ratio).toInt() }.toMutableList() + val usedWidth = finalWidths.sum() - totalUnscaledWidth += scaledWidth - } + // based on screen width get remaining pixels + val remainingPixels = (availableWidth - usedWidth).toInt() - val totalAvailableWidth = screenWidth - ((row.files.size - 1) * smallMargin) - return totalAvailableWidth / totalUnscaledWidth - } + // add to remaining pixels to last image + if (remainingPixels > 0 && finalWidths.isNotEmpty()) { + val lastIndex = finalWidths.lastIndex + finalWidths[lastIndex] = finalWidths[lastIndex] + remainingPixels + } - private fun computeSingleFileShrinkRatio(row: GalleryRow, screenWidth: Float): Float { - val width = OCFileUtils.getImageSize(row.files[0], defaultThumbnailSize).first - return (screenWidth / galleryAdapter.columns) / width + return finalWidths.map { w -> w to finalHeight } } - private fun adjustFile(index: Int, file: OCFile, shrinkRatio: Float, row: GalleryRow) { - val width = file.imageDimension?.width?.times(shrinkRatio)?.toInt() ?: 0 - val height = file.imageDimension?.height?.times(shrinkRatio)?.toInt() ?: 0 - + private fun adjustFile(index: Int, file: OCFile, dims: Pair, row: GalleryRow) { + val (width, height) = dims val frameLayout = binding.rowLayout[index] as FrameLayout val shimmer = frameLayout[0] as LoaderImageView val thumbnail = frameLayout[1] as ImageView - val checkBoxImageView = frameLayout[2] as ImageView + val checkbox = frameLayout[2] as ImageView val isChecked = ocFileListDelegate.isCheckedFile(file) - adjustRowCell(thumbnail, isChecked) - adjustCheckBox(checkBoxImageView, isChecked) - - ocFileListDelegate.bindGalleryRow( - shimmer, - thumbnail, - file, - this, - width to height - ) - - // Update layout params only if they differ - val thumbLp = thumbnail.layoutParams - if (thumbLp.width != width || thumbLp.height != height) { - thumbnail.layoutParams = thumbLp.getFrameLayout(width, height).apply { - val endMargin = if (index < row.files.size - 1) smallMargin else zero - this.setMargins(zero, zero, endMargin, smallMargin) - } - } + adjustCheckBox(checkbox, isChecked) - val shimmerLp = shimmer.layoutParams - if (shimmerLp.width != width || shimmerLp.height != height) { - shimmer.layoutParams = shimmerLp.getFrameLayout(width, height) - } + ocFileListDelegate.bindGalleryRow(shimmer, thumbnail, file, this, dims) - // Force layout update + val endMargin = if (index < row.files.size - 1) smallMargin else zero + thumbnail.layoutParams = FrameLayout.LayoutParams(width, height).apply { + setMargins(0, 0, endMargin, smallMargin) + } + shimmer.layoutParams = FrameLayout.LayoutParams(width, height) frameLayout.requestLayout() } - private fun ViewGroup.LayoutParams?.getFrameLayout(width: Int, height: Int): FrameLayout.LayoutParams = ( - this as? FrameLayout.LayoutParams - ?: FrameLayout.LayoutParams(width, height) - ).apply { - this.width = width - this.height = height - } - @Suppress("MagicNumber") private fun adjustRowCell(imageView: ImageView, isChecked: Boolean) { val scale = if (isChecked) 0.8f else 1.0f val radius = if (isChecked) iconRadius else 0f - - // Only update if values changed - if (imageView.scaleX != scale) { - imageView.scaleX = scale - imageView.scaleY = scale - } - + imageView.scaleX = scale + imageView.scaleY = scale imageView.makeRounded(context, radius) }