Skip to content

Commit a8f66d8

Browse files
Merge pull request #16114 from nextcloud/fix/gallery-image-scaling
fix: gallery image scaling
2 parents bc2a009 + 7fe17f2 commit a8f66d8

File tree

2 files changed

+105
-124
lines changed

2 files changed

+105
-124
lines changed

app/src/main/java/com/nextcloud/utils/OCFileUtils.kt

Lines changed: 58 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -19,52 +19,75 @@ import com.owncloud.android.lib.common.utils.Log_OC
1919
import com.owncloud.android.utils.BitmapUtils
2020
import com.owncloud.android.utils.MimeTypeUtil
2121

22+
@Suppress("TooGenericExceptionCaught", "ReturnCount")
2223
object OCFileUtils {
2324
private const val TAG = "OCFileUtils"
2425

25-
@Suppress("ReturnCount", "NestedBlockDepth")
2626
fun getImageSize(ocFile: OCFile, defaultThumbnailSize: Float): Pair<Int, Int> {
27+
val fallback = defaultThumbnailSize.toInt().coerceAtLeast(1)
28+
val fallbackPair = fallback to fallback
29+
2730
try {
2831
Log_OC.d(TAG, "Getting image size for: ${ocFile.fileName}")
2932

30-
val widthFromDimension = ocFile.imageDimension?.width
31-
val heightFromDimension = ocFile.imageDimension?.height
32-
if (widthFromDimension != null && heightFromDimension != null) {
33-
val width = widthFromDimension.toInt()
34-
val height = heightFromDimension.toInt()
35-
Log_OC.d(TAG, "Image dimensions are used, width: $width, height: $height")
36-
return width to height
33+
// Server-provided
34+
ocFile.imageDimension?.let { dim ->
35+
val w = dim.width.toInt().coerceAtLeast(1)
36+
val h = dim.height.toInt().coerceAtLeast(1)
37+
Log_OC.d(TAG, "Using server-provided imageDimension: $w x $h")
38+
return w to h
3739
}
3840

39-
return if (ocFile.exists()) {
40-
val exif = ExifInterface(ocFile.storagePath)
41-
val width = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0)
42-
val height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0)
43-
44-
if (width > 0 && height > 0) {
45-
Log_OC.d(TAG, "Exif used width: $width and height: $height")
46-
width to height
47-
}
48-
49-
val (bitmapWidth, bitmapHeight) = BitmapUtils.getImageResolution(ocFile.storagePath)
50-
.let { it[0] to it[1] }
51-
52-
if (bitmapWidth > 0 && bitmapHeight > 0) {
53-
Log_OC.d(TAG, "BitmapUtils.getImageResolution used width: $bitmapWidth and height: $bitmapHeight")
54-
bitmapWidth to bitmapHeight
55-
}
56-
57-
val fallback = defaultThumbnailSize.toInt().coerceAtLeast(1)
58-
Log_OC.d(TAG, "Default size used width: $fallback and height: $fallback")
59-
fallback to fallback
60-
} else {
61-
Log_OC.d(TAG, "Default size is used: $defaultThumbnailSize")
62-
val size = defaultThumbnailSize.toInt().coerceAtLeast(1)
63-
size to size
41+
// Local file
42+
val path = ocFile.storagePath
43+
if (!path.isNullOrEmpty() && ocFile.exists()) {
44+
getExifSize(path)?.let { return it }
45+
getBitmapSize(path)?.let { return it }
6446
}
65-
} finally {
66-
Log_OC.d(TAG, "-----------------------------")
47+
48+
// 3 Fallback
49+
Log_OC.d(TAG, "Fallback to default size: $fallback x $fallback")
50+
return fallbackPair
51+
} catch (e: Exception) {
52+
Log_OC.e(TAG, "Error getting image size for ${ocFile.fileName}", e)
53+
}
54+
55+
return fallbackPair
56+
}
57+
58+
private fun getExifSize(path: String): Pair<Int, Int>? = try {
59+
val exif = ExifInterface(path)
60+
var w = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0)
61+
var h = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0)
62+
63+
val orientation = exif.getAttributeInt(
64+
ExifInterface.TAG_ORIENTATION,
65+
ExifInterface.ORIENTATION_NORMAL
66+
)
67+
if (orientation == ExifInterface.ORIENTATION_ROTATE_90 ||
68+
orientation == ExifInterface.ORIENTATION_ROTATE_270
69+
) {
70+
val tmp = w
71+
w = h
72+
h = tmp
6773
}
74+
75+
Log_OC.d(TAG, "Using exif imageDimension: $w x $h")
76+
if (w > 0 && h > 0) w to h else null
77+
} catch (_: Exception) {
78+
null
79+
}
80+
81+
private fun getBitmapSize(path: String): Pair<Int, Int>? = try {
82+
val options = android.graphics.BitmapFactory.Options().apply { inJustDecodeBounds = true }
83+
android.graphics.BitmapFactory.decodeFile(path, options)
84+
val w = options.outWidth
85+
val h = options.outHeight
86+
87+
Log_OC.d(TAG, "Using bitmap factory imageDimension: $w x $h")
88+
if (w > 0 && h > 0) w to h else null
89+
} catch (_: Exception) {
90+
null
6891
}
6992

7093
fun getMediaPlaceholder(file: OCFile, imageDimension: Pair<Int, Int>): BitmapDrawable {

app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt

Lines changed: 47 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ package com.owncloud.android.ui.adapter
1010

1111
import android.view.Gravity
1212
import android.view.View
13-
import android.view.ViewGroup
1413
import android.widget.FrameLayout
1514
import android.widget.ImageView
1615
import androidx.core.content.ContextCompat
@@ -20,15 +19,12 @@ import com.elyeproj.loaderviewlibrary.LoaderImageView
2019
import com.nextcloud.android.common.ui.theme.utils.ColorRole
2120
import com.nextcloud.utils.OCFileUtils
2221
import com.nextcloud.utils.extensions.makeRounded
23-
import com.nextcloud.utils.extensions.mediaSize
2422
import com.nextcloud.utils.extensions.setVisibleIf
2523
import com.owncloud.android.R
2624
import com.owncloud.android.databinding.GalleryRowBinding
2725
import com.owncloud.android.datamodel.FileDataStorageManager
2826
import com.owncloud.android.datamodel.GalleryRow
2927
import com.owncloud.android.datamodel.OCFile
30-
import com.owncloud.android.lib.resources.files.model.ImageDimension
31-
import com.owncloud.android.utils.DisplayUtils
3228
import com.owncloud.android.utils.theme.ViewThemeUtils
3329

3430
@Suppress("LongParameterList")
@@ -37,7 +33,7 @@ class GalleryRowHolder(
3733
private val defaultThumbnailSize: Float,
3834
private val ocFileListDelegate: OCFileListDelegate,
3935
val storageManager: FileDataStorageManager,
40-
private val galleryAdapter: GalleryAdapter,
36+
galleryAdapter: GalleryAdapter,
4137
private val viewThemeUtils: ViewThemeUtils
4238
) : SectionedViewHolder(binding.root) {
4339
val context = galleryAdapter.context
@@ -71,25 +67,25 @@ class GalleryRowHolder(
7167
// Only rebuild if file count changed
7268
if (lastFileCount != requiredCount) {
7369
binding.rowLayout.removeAllViews()
74-
for (file in row.files) {
75-
val rowLayout = getRowLayout(file)
76-
binding.rowLayout.addView(rowLayout)
70+
row.files.forEach { file ->
71+
binding.rowLayout.addView(getRowLayout(file))
7772
}
7873
lastFileCount = requiredCount
7974
}
8075

81-
val shrinkRatio = computeShrinkRatio(row)
76+
val dimensions = getDimensions(row)
8277

8378
for (i in row.files.indices) {
84-
adjustFile(i, row.files[i], shrinkRatio, row)
79+
val dim = dimensions.getOrNull(i) ?: (defaultThumbnailSize.toInt() to defaultThumbnailSize.toInt())
80+
adjustFile(i, row.files[i], dim, row)
8581
}
8682
}
8783

88-
fun updateRowVisuals() {
89-
bind(currentRow)
90-
}
84+
fun updateRowVisuals() = bind(currentRow)
9185

9286
private fun getRowLayout(file: OCFile): FrameLayout {
87+
val (width, height) = OCFileUtils.getImageSize(file, defaultThumbnailSize)
88+
9389
val checkbox = ImageView(context).apply {
9490
visibility = View.GONE
9591
layoutParams = FrameLayout.LayoutParams(
@@ -102,20 +98,17 @@ class GalleryRowHolder(
10298
}
10399
}
104100

105-
val mediaSize = file.mediaSize(defaultThumbnailSize)
106-
val (width, height) = mediaSize
107-
108101
val shimmer = LoaderImageView(context).apply {
109102
setImageResource(R.drawable.background)
110103
resetLoader()
111104
layoutParams = FrameLayout.LayoutParams(width, height)
112105
}
113106

114-
val drawable = OCFileUtils.getMediaPlaceholder(file, mediaSize)
107+
val drawable = OCFileUtils.getMediaPlaceholder(file, width to height)
115108
val rowCellImageView = ImageView(context).apply {
116109
setImageDrawable(drawable)
117110
adjustViewBounds = true
118-
scaleType = ImageView.ScaleType.FIT_XY
111+
scaleType = ImageView.ScaleType.CENTER_CROP
119112
layoutParams = FrameLayout.LayoutParams(width, height)
120113
}
121114

@@ -126,101 +119,66 @@ class GalleryRowHolder(
126119
}
127120
}
128121

129-
@SuppressWarnings("MagicNumber")
130-
private fun computeShrinkRatio(row: GalleryRow): Float {
131-
val screenWidth = DisplayUtils.convertDpToPixel(
132-
context.resources.configuration.screenWidthDp.toFloat(),
133-
context
134-
).toFloat()
135-
136-
return if (row.files.size > 1) {
137-
computeMultiFileShrinkRatio(row, screenWidth)
138-
} else {
139-
computeSingleFileShrinkRatio(row, screenWidth)
122+
private fun getDimensions(row: GalleryRow): List<Pair<Int, Int>> {
123+
val screenWidthPx = context.resources.displayMetrics.widthPixels.toFloat()
124+
val marginPx = smallMargin.toFloat()
125+
val totalMargins = marginPx * (row.files.size - 1)
126+
val availableWidth = screenWidthPx - totalMargins
127+
128+
val aspectRatios = row.files.map { file ->
129+
val (w, h) = OCFileUtils.getImageSize(file, defaultThumbnailSize)
130+
if (h > 0) w.toFloat() / h else 1.0f
140131
}
141-
}
142132

143-
private fun computeMultiFileShrinkRatio(row: GalleryRow, screenWidth: Float): Float {
144-
val targetHeight = row.getMaxHeight()
145-
var totalUnscaledWidth = 0f
133+
val sumAspectRatios = aspectRatios.sum()
146134

147-
for (file in row.files) {
148-
val (originalWidth, originalHeight) = OCFileUtils.getImageSize(file, defaultThumbnailSize)
135+
// calculate row height based on aspect ratios
136+
val rowHeightFloat = if (sumAspectRatios > 0) availableWidth / sumAspectRatios else defaultThumbnailSize
137+
val finalHeight = rowHeightFloat.toInt()
149138

150-
val scaledWidth = targetHeight * (originalWidth.toFloat() / originalHeight)
151-
file.imageDimension = ImageDimension(scaledWidth, targetHeight)
139+
// for each aspect ratio calculate widths
140+
val finalWidths = aspectRatios.map { ratio -> (rowHeightFloat * ratio).toInt() }.toMutableList()
141+
val usedWidth = finalWidths.sum()
152142

153-
totalUnscaledWidth += scaledWidth
154-
}
143+
// based on screen width get remaining pixels
144+
val remainingPixels = (availableWidth - usedWidth).toInt()
155145

156-
val totalAvailableWidth = screenWidth - ((row.files.size - 1) * smallMargin)
157-
return totalAvailableWidth / totalUnscaledWidth
158-
}
146+
// add to remaining pixels to last image
147+
if (remainingPixels > 0 && finalWidths.isNotEmpty()) {
148+
val lastIndex = finalWidths.lastIndex
149+
finalWidths[lastIndex] = finalWidths[lastIndex] + remainingPixels
150+
}
159151

160-
private fun computeSingleFileShrinkRatio(row: GalleryRow, screenWidth: Float): Float {
161-
val width = OCFileUtils.getImageSize(row.files[0], defaultThumbnailSize).first
162-
return (screenWidth / galleryAdapter.columns) / width
152+
return finalWidths.map { w -> w to finalHeight }
163153
}
164154

165-
private fun adjustFile(index: Int, file: OCFile, shrinkRatio: Float, row: GalleryRow) {
166-
val width = file.imageDimension?.width?.times(shrinkRatio)?.toInt() ?: 0
167-
val height = file.imageDimension?.height?.times(shrinkRatio)?.toInt() ?: 0
168-
155+
private fun adjustFile(index: Int, file: OCFile, dims: Pair<Int, Int>, row: GalleryRow) {
156+
val (width, height) = dims
169157
val frameLayout = binding.rowLayout[index] as FrameLayout
170158
val shimmer = frameLayout[0] as LoaderImageView
171159
val thumbnail = frameLayout[1] as ImageView
172-
val checkBoxImageView = frameLayout[2] as ImageView
160+
val checkbox = frameLayout[2] as ImageView
173161

174162
val isChecked = ocFileListDelegate.isCheckedFile(file)
175-
176163
adjustRowCell(thumbnail, isChecked)
177-
adjustCheckBox(checkBoxImageView, isChecked)
178-
179-
ocFileListDelegate.bindGalleryRow(
180-
shimmer,
181-
thumbnail,
182-
file,
183-
this,
184-
width to height
185-
)
186-
187-
// Update layout params only if they differ
188-
val thumbLp = thumbnail.layoutParams
189-
if (thumbLp.width != width || thumbLp.height != height) {
190-
thumbnail.layoutParams = thumbLp.getFrameLayout(width, height).apply {
191-
val endMargin = if (index < row.files.size - 1) smallMargin else zero
192-
this.setMargins(zero, zero, endMargin, smallMargin)
193-
}
194-
}
164+
adjustCheckBox(checkbox, isChecked)
195165

196-
val shimmerLp = shimmer.layoutParams
197-
if (shimmerLp.width != width || shimmerLp.height != height) {
198-
shimmer.layoutParams = shimmerLp.getFrameLayout(width, height)
199-
}
166+
ocFileListDelegate.bindGalleryRow(shimmer, thumbnail, file, this, dims)
200167

201-
// Force layout update
168+
val endMargin = if (index < row.files.size - 1) smallMargin else zero
169+
thumbnail.layoutParams = FrameLayout.LayoutParams(width, height).apply {
170+
setMargins(0, 0, endMargin, smallMargin)
171+
}
172+
shimmer.layoutParams = FrameLayout.LayoutParams(width, height)
202173
frameLayout.requestLayout()
203174
}
204175

205-
private fun ViewGroup.LayoutParams?.getFrameLayout(width: Int, height: Int): FrameLayout.LayoutParams = (
206-
this as? FrameLayout.LayoutParams
207-
?: FrameLayout.LayoutParams(width, height)
208-
).apply {
209-
this.width = width
210-
this.height = height
211-
}
212-
213176
@Suppress("MagicNumber")
214177
private fun adjustRowCell(imageView: ImageView, isChecked: Boolean) {
215178
val scale = if (isChecked) 0.8f else 1.0f
216179
val radius = if (isChecked) iconRadius else 0f
217-
218-
// Only update if values changed
219-
if (imageView.scaleX != scale) {
220-
imageView.scaleX = scale
221-
imageView.scaleY = scale
222-
}
223-
180+
imageView.scaleX = scale
181+
imageView.scaleY = scale
224182
imageView.makeRounded(context, radius)
225183
}
226184

0 commit comments

Comments
 (0)