Skip to content
Merged
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
93 changes: 58 additions & 35 deletions app/src/main/java/com/nextcloud/utils/OCFileUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int, Int> {
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<Int, Int>? = 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<Int, Int>? = 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<Int, Int>): BitmapDrawable {
Expand Down
136 changes: 47 additions & 89 deletions app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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)
}

Expand All @@ -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<Pair<Int, Int>> {
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<Int, Int>, 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)
}

Expand Down
Loading