Skip to content

Commit f24ef3a

Browse files
Merge pull request nextcloud#15161 from nextcloud/bugfix/gallery-image-scaling
fix: gallery image scaling
2 parents 6979ce8 + 34e62bf commit f24ef3a

File tree

5 files changed

+118
-75
lines changed

5 files changed

+118
-75
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.nextcloud.utils
8+
9+
import androidx.exifinterface.media.ExifInterface
10+
import com.owncloud.android.datamodel.OCFile
11+
import com.owncloud.android.lib.common.utils.Log_OC
12+
import com.owncloud.android.utils.BitmapUtils
13+
14+
object OCFileUtils {
15+
private const val TAG = "OCFileUtils"
16+
17+
@Suppress("ReturnCount", "NestedBlockDepth")
18+
fun getImageSize(ocFile: OCFile, defaultThumbnailSize: Float): Pair<Int, Int> {
19+
try {
20+
Log_OC.d(TAG, "Getting image size for: ${ocFile.fileName}")
21+
22+
if (!ocFile.exists()) {
23+
ocFile.imageDimension?.width?.let { w ->
24+
ocFile.imageDimension?.height?.let { h ->
25+
return w.toInt() to h.toInt()
26+
}
27+
}
28+
val size = defaultThumbnailSize.toInt().coerceAtLeast(1)
29+
return size to size
30+
}
31+
32+
val exif = ExifInterface(ocFile.storagePath)
33+
val width = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0)
34+
val height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0)
35+
36+
if (width > 0 && height > 0) {
37+
Log_OC.d(TAG, "Exif used width: $width and height: $height")
38+
return width to height
39+
}
40+
41+
val (bitmapWidth, bitmapHeight) = BitmapUtils.getImageResolution(ocFile.storagePath)
42+
.let { it[0] to it[1] }
43+
44+
if (bitmapWidth > 0 && bitmapHeight > 0) {
45+
Log_OC.d(TAG, "BitmapUtils.getImageResolution used width: $bitmapWidth and height: $bitmapHeight")
46+
return bitmapWidth to bitmapHeight
47+
}
48+
49+
val fallback = defaultThumbnailSize.toInt().coerceAtLeast(1)
50+
Log_OC.d(TAG, "Default size used width: $fallback and height: $fallback")
51+
return fallback to fallback
52+
} finally {
53+
Log_OC.d(TAG, "-----------------------------")
54+
}
55+
}
56+
}

app/src/main/java/com/owncloud/android/datamodel/GalleryRow.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
*/
99
package com.owncloud.android.datamodel
1010

11-
data class GalleryRow(val files: List<OCFile>, val defaultHeight: Int, val defaultWidth: Int) {
12-
fun getMaxHeight(): Float = files.map { it.imageDimension?.height ?: defaultHeight.toFloat() }.maxOrNull() ?: 0f
11+
import com.nextcloud.utils.OCFileUtils
1312

13+
data class GalleryRow(val files: List<OCFile>, val defaultHeight: Int, val defaultWidth: Int) {
14+
fun getMaxHeight(): Float = files.maxOfOrNull {
15+
OCFileUtils.getImageSize(it, defaultHeight.toFloat()).second.toFloat()
16+
} ?: 0f
1417
fun calculateHashCode(): Long = files.sumOf { it.hashCode() }.toLong()
1518
}

app/src/main/java/com/owncloud/android/datamodel/OCFile.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,4 +1139,9 @@ public long getUploadTimestamp() {
11391139
public void setUploadTimestamp(long uploadTimestamp) {
11401140
this.uploadTimestamp = uploadTimestamp;
11411141
}
1142+
1143+
public boolean exists() {
1144+
final String storagePath = getStoragePath();
1145+
return storagePath != null && new File(storagePath).exists();
1146+
}
11421147
}

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,10 @@ class GalleryAdapter(
194194
photoFragment.setEmptyListMessage(SearchType.GALLERY_SEARCH)
195195
}
196196

197-
files = finalSortedList.toGalleryItems()
198-
Handler(Looper.getMainLooper()).post { notifyDataSetChanged() }
197+
Handler(Looper.getMainLooper()).post {
198+
files = finalSortedList.toGalleryItems()
199+
notifyDataSetChanged()
200+
}
199201
}
200202

201203
private fun transformToRows(list: List<OCFile>): List<GalleryRow> = list
@@ -206,8 +208,10 @@ class GalleryAdapter(
206208

207209
@SuppressLint("NotifyDataSetChanged")
208210
fun clear() {
209-
files = emptyList()
210-
Handler(Looper.getMainLooper()).post { notifyDataSetChanged() }
211+
Handler(Looper.getMainLooper()).post {
212+
files = emptyList()
213+
notifyDataSetChanged()
214+
}
211215
}
212216

213217
private fun firstOfMonth(timestamp: Long): Long {
@@ -329,8 +333,10 @@ class GalleryAdapter(
329333
}
330334
}
331335

332-
files = allFiles.toGalleryItems()
333-
Handler(Looper.getMainLooper()).post { notifyDataSetChanged() }
336+
Handler(Looper.getMainLooper()).post {
337+
files = allFiles.toGalleryItems()
338+
notifyDataSetChanged()
339+
}
334340
}
335341

336342
private fun List<OCFile>.toGalleryItems(): List<GalleryItems> = this

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

Lines changed: 40 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/*
22
* Nextcloud - Android Client
33
*
4+
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
45
* SPDX-FileCopyrightText: 2022 Tobias Kaminsky <tobias@kaminsky.me>
56
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
67
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
@@ -17,6 +18,7 @@ import androidx.core.view.get
1718
import com.afollestad.sectionedrecyclerview.SectionedViewHolder
1819
import com.elyeproj.loaderviewlibrary.LoaderImageView
1920
import com.nextcloud.android.common.ui.theme.utils.ColorRole
21+
import com.nextcloud.utils.OCFileUtils
2022
import com.nextcloud.utils.extensions.makeRounded
2123
import com.nextcloud.utils.extensions.setVisibleIf
2224
import com.owncloud.android.R
@@ -58,7 +60,7 @@ class GalleryRowHolder(
5860
}
5961

6062
if (binding.rowLayout.childCount > row.files.size) {
61-
binding.rowLayout.removeViewsInLayout(row.files.size - 1, (binding.rowLayout.childCount - row.files.size))
63+
binding.rowLayout.removeViews(row.files.size, binding.rowLayout.childCount - row.files.size)
6264
}
6365

6466
val shrinkRatio = computeShrinkRatio(row)
@@ -110,76 +112,57 @@ class GalleryRowHolder(
110112
bind(currentRow)
111113
}
112114

113-
@SuppressWarnings("MagicNumber", "ComplexMethod")
115+
@SuppressWarnings("MagicNumber")
114116
private fun computeShrinkRatio(row: GalleryRow): Float {
115-
val screenWidth =
116-
DisplayUtils.convertDpToPixel(context.resources.configuration.screenWidthDp.toFloat(), context)
117-
.toFloat()
117+
val screenWidth = DisplayUtils.convertDpToPixel(
118+
context.resources.configuration.screenWidthDp.toFloat(),
119+
context
120+
).toFloat()
118121

119-
if (row.files.size > 1) {
120-
var newSummedWidth = 0f
121-
for (file in row.files) {
122-
// first adjust all thumbnails to max height
123-
val thumbnail1 = file.imageDimension ?: ImageDimension(defaultThumbnailSize, defaultThumbnailSize)
124-
125-
val height1 = thumbnail1.height
126-
val width1 = thumbnail1.width
127-
128-
val scaleFactor1 = row.getMaxHeight() / height1
129-
val newHeight1 = height1 * scaleFactor1
130-
val newWidth1 = width1 * scaleFactor1
122+
return if (row.files.size > 1) {
123+
computeMultiFileShrinkRatio(row, screenWidth)
124+
} else {
125+
computeSingleFileShrinkRatio(row, screenWidth)
126+
}
127+
}
131128

132-
file.imageDimension = ImageDimension(newWidth1, newHeight1)
129+
private fun computeMultiFileShrinkRatio(row: GalleryRow, screenWidth: Float): Float {
130+
val targetHeight = row.getMaxHeight()
131+
var totalUnscaledWidth = 0f
133132

134-
newSummedWidth += newWidth1
135-
}
133+
for (file in row.files) {
134+
val (originalWidth, originalHeight) = OCFileUtils.getImageSize(file, defaultThumbnailSize)
136135

137-
var c = 1f
138-
// this ensures that files in last row are better visible,
139-
// e.g. when 2 images are there, it uses 2/5 of screen
140-
if (galleryAdapter.columns == 5) {
141-
when (row.files.size) {
142-
2 -> {
143-
c = 5 / 2f
144-
}
136+
val scaledWidth = targetHeight * (originalWidth.toFloat() / originalHeight)
137+
file.imageDimension = ImageDimension(scaledWidth, targetHeight)
145138

146-
3 -> {
147-
c = 4 / 3f
148-
}
149-
150-
4 -> {
151-
c = 4 / 5f
152-
}
139+
totalUnscaledWidth += scaledWidth
140+
}
153141

154-
5 -> {
155-
c = 1f
156-
}
157-
}
158-
}
142+
val totalAvailableWidth = screenWidth - ((row.files.size - 1) * smallMargin)
143+
return totalAvailableWidth / totalUnscaledWidth
144+
}
159145

160-
return (screenWidth / c) / newSummedWidth
161-
} else {
162-
val thumbnail1 = row.files[0].imageDimension ?: ImageDimension(defaultThumbnailSize, defaultThumbnailSize)
163-
return (screenWidth / galleryAdapter.columns) / thumbnail1.width
164-
}
146+
private fun computeSingleFileShrinkRatio(row: GalleryRow, screenWidth: Float): Float {
147+
val width = OCFileUtils.getImageSize(row.files[0], defaultThumbnailSize).first
148+
return (screenWidth / galleryAdapter.columns) / width
165149
}
166150

167151
private fun adjustFile(indexedFile: IndexedValue<OCFile>, shrinkRatio: Float, row: GalleryRow) {
168152
val file = indexedFile.value
169153
val index = indexedFile.index
170-
val fileWidth = file.imageDimension?.width
171-
val fileHeight = file.imageDimension?.height
172154

173-
val width = ((fileWidth ?: defaultThumbnailSize) * shrinkRatio).toInt()
174-
val height = ((fileHeight ?: defaultThumbnailSize) * shrinkRatio).toInt()
155+
val width = file.imageDimension?.width?.times(shrinkRatio)?.toInt() ?: 0
156+
val height = file.imageDimension?.height?.times(shrinkRatio)?.toInt() ?: 0
175157

176158
val frameLayout = binding.rowLayout[index] as FrameLayout
177159
val checkBoxImageView = frameLayout[2] as ImageView
178160
val shimmer = frameLayout[0] as LoaderImageView
179161
val thumbnail = (frameLayout[1] as ImageView).apply {
180162
adjustViewBounds = true
181-
scaleType = ImageView.ScaleType.FIT_CENTER
163+
scaleType = ImageView.ScaleType.FIT_XY
182164
}
165+
183166
val isChecked = ocFileListDelegate.isCheckedFile(file)
184167

185168
adjustRowCell(thumbnail, isChecked)
@@ -193,25 +176,15 @@ class GalleryRowHolder(
193176
width
194177
)
195178

196-
val params = FrameLayout.LayoutParams(width, height)
197-
198-
if (index < (row.files.size - 1)) {
199-
params.setMargins(zero, zero, smallMargin, smallMargin)
200-
} else {
201-
params.setMargins(zero, zero, zero, smallMargin)
202-
}
179+
// Force layout update
180+
frameLayout.requestLayout()
203181

204-
thumbnail.run {
205-
layoutParams = params
206-
layoutParams.height = height
207-
layoutParams.width = width
208-
}
182+
val params = FrameLayout.LayoutParams(width, height)
183+
val endMargin = if (index < row.files.size - 1) smallMargin else zero
184+
params.setMargins(zero, zero, endMargin, smallMargin)
209185

210-
shimmer.run {
211-
layoutParams = params
212-
layoutParams.height = height
213-
layoutParams.width = width
214-
}
186+
thumbnail.layoutParams = params
187+
shimmer.layoutParams = FrameLayout.LayoutParams(params)
215188
}
216189

217190
@Suppress("MagicNumber")

0 commit comments

Comments
 (0)