diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java
index 97f2f24fcb00..56c9a6add7d5 100644
--- a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java
+++ b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java
@@ -27,6 +27,7 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
+import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceCategory;
@@ -966,6 +967,30 @@ private void setupGeneralCategory() {
return true;
});
}
+
+ setupGridDensityPreference();
+ }
+
+ /**
+ * The grid density shares its stored value with the pinch to zoom gesture in the file list,
+ * so picking a density here and pinching the grid are two ways of setting the same thing.
+ */
+ private void setupGridDensityPreference() {
+ if (!(findPreference("grid_density") instanceof ListPreference densityPref)) {
+ return;
+ }
+
+ densityPref.setValue(String.valueOf(Math.round(preferences.getGridColumns())));
+
+ densityPref.setOnPreferenceChangeListener((preference, newValue) -> {
+ try {
+ preferences.setGridColumns(Float.parseFloat(newValue.toString()));
+ } catch (NumberFormatException e) {
+ Log_OC.w(TAG, "Ignoring unusable grid density value: " + newValue);
+ return false;
+ }
+ return true;
+ });
}
private void updateThemePreferenceSummary(String themeValue) {
diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/AutofitGridLayoutManager.kt b/app/src/main/java/com/owncloud/android/ui/fragment/AutofitGridLayoutManager.kt
new file mode 100644
index 000000000000..653807aa8c3a
--- /dev/null
+++ b/app/src/main/java/com/owncloud/android/ui/fragment/AutofitGridLayoutManager.kt
@@ -0,0 +1,55 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.owncloud.android.ui.fragment
+
+import android.content.Context
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import kotlin.math.max
+import kotlin.math.roundToInt
+
+/**
+ * A [GridLayoutManager] that works out its own column count from the width it is actually given.
+ *
+ * The width is read during layout rather than from the display metrics, because the metrics are
+ * not a reliable stand in: they describe the display rather than the space this list occupies,
+ * they are not yet meaningful while the view is being created, and they can still describe the
+ * previous orientation while a rotation is being delivered.
+ *
+ * @param columnWidthProvider target width of a single column in pixels, read on every layout so
+ * that a changed preference is picked up without recreating the layout manager.
+ */
+class AutofitGridLayoutManager(context: Context, private val columnWidthProvider: () -> Int) :
+ GridLayoutManager(context, 1) {
+
+ private var lastWidth = 0
+ private var lastColumnWidth = 0
+
+ override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {
+ updateSpanCount()
+ super.onLayoutChildren(recycler, state)
+ }
+
+ private fun updateSpanCount() {
+ val columnWidth = columnWidthProvider()
+ if (width <= 0 || columnWidth <= 0) {
+ return
+ }
+
+ if (width == lastWidth && columnWidth == lastColumnWidth) {
+ return
+ }
+
+ lastWidth = width
+ lastColumnWidth = columnWidth
+ spanCount = max(MIN_COLUMN_COUNT, (width.toFloat() / columnWidth).roundToInt())
+ }
+
+ companion object {
+ private const val MIN_COLUMN_COUNT = 2
+ }
+}
diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.kt
index 45f9063369b3..38f719fb61fa 100644
--- a/app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.kt
+++ b/app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.kt
@@ -86,8 +86,6 @@ open class ExtendedListFragment :
SearchView.OnQueryTextListener,
SearchView.OnCloseListener,
Injectable {
- private var maxColumnSize = 5
-
@Inject
lateinit var preferences: AppPreferences
@@ -146,7 +144,7 @@ open class ExtendedListFragment :
open fun switchToGridView() {
if (!isGridEnabled) {
- recyclerView?.layoutManager = GridLayoutManager(context, columnsCount)
+ recyclerView?.layoutManager = AutofitGridLayoutManager(requireContext()) { targetColumnWidth }
}
}
@@ -375,9 +373,8 @@ open class ExtendedListFragment :
mScale = gridLayoutManager.spanCount.toFloat()
}
mScale *= 2f - scaleFactor
- mScale = max(MIN_COLUMN_SIZE, min(mScale, maxColumnSize.toFloat()))
- val scaleInt = mScale.roundToInt()
- gridLayoutManager.setSpanCount(scaleInt)
+ mScale = max(MIN_COLUMN_SIZE, min(mScale, MAX_COLUMN_SCALE))
+ gridLayoutManager.requestLayout()
mRecyclerView?.adapter?.notifyDataSetChanged()
}
}
@@ -421,6 +418,21 @@ open class ExtendedListFragment :
scrollToPosition(referencePosition)
}
+ @SuppressLint("NotifyDataSetChanged")
+ override fun onResume() {
+ super.onResume()
+
+ // The density may have been changed in the settings while this list was in the
+ // background, and the stored value is only read when the view is created.
+ val stored = preferences.getGridColumns()
+ if (stored != mScale) {
+ mScale = stored
+ // The layout manager reads the target column width again on its next layout.
+ recyclerView?.requestLayout()
+ recyclerView?.adapter?.notifyDataSetChanged()
+ }
+ }
+
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
Log_OC.d(TAG, "onSaveInstanceState()")
@@ -437,14 +449,24 @@ open class ExtendedListFragment :
preferences.setGridColumns(mScale)
}
- open val columnsCount: Int
+ /**
+ * Target width of one grid column in pixels.
+ *
+ * [mScale] is a zoom level: asking for more columns than the default means asking for
+ * smaller cells. The column count itself is worked out by [AutofitGridLayoutManager] from
+ * the width the list is actually given.
+ */
+ internal val targetColumnWidth: Int
get() {
- if (mScale == -1f) {
- return AppPreferencesImpl.DEFAULT_GRID_COLUMN.roundToInt()
- }
- return mScale.roundToInt()
+ val zoom = (if (mScale > 0f) mScale else AppPreferencesImpl.DEFAULT_GRID_COLUMN) /
+ AppPreferencesImpl.DEFAULT_GRID_COLUMN
+ return (resources.getDimension(R.dimen.grid_item_default_width) / zoom).roundToInt()
}
+ open val columnsCount: Int
+ get() = (recyclerView?.layoutManager as? GridLayoutManager)?.spanCount
+ ?: AppPreferencesImpl.DEFAULT_GRID_COLUMN.roundToInt()
+
/*
* Restore index and position
*/
@@ -778,15 +800,7 @@ open class ExtendedListFragment :
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
- if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
- maxColumnSize = 10
- } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
- maxColumnSize = 5
- }
-
- if (isGridEnabled && columnsCount > maxColumnSize) {
- (recyclerView?.layoutManager as GridLayoutManager).spanCount = maxColumnSize
- }
+ // The column count follows the width the list is given, so nothing to do here.
}
protected fun setLayoutSwitchButton() {
@@ -821,5 +835,10 @@ open class ExtendedListFragment :
private const val KEY_EMPTY_LIST_MESSAGE = "EMPTY_LIST_MESSAGE"
private const val KEY_IS_GRID_VISIBLE = "IS_GRID_VISIBLE"
private const val MIN_COLUMN_SIZE: Float = 2.0f
+
+ /**
+ * Highest number of columns the pinch gesture can select on a reference-width screen.
+ */
+ private const val MAX_COLUMN_SCALE: Float = 5.0f
}
}
diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt
index 1f4375466bc7..39b7f146ee23 100644
--- a/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt
+++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt
@@ -87,7 +87,7 @@ class FileListLayoutManager(private val fragment: OCFileListFragment, private va
val layoutManager: RecyclerView.LayoutManager?
if (grid) {
- layoutManager = GridLayoutManager(context, fragment.columnsCount)
+ layoutManager = AutofitGridLayoutManager(context) { fragment.targetColumnWidth }
layoutManager.spanSizeLookup = object : SpanSizeLookup() {
override fun getSpanSize(position: Int): Int = if (position == fragment.adapter.itemCount - 1 ||
(position == 0 && fragment.adapter.shouldShowHeader())
diff --git a/app/src/main/res/values/dims.xml b/app/src/main/res/values/dims.xml
index ffc32ec8b7a8..fc54132e8982 100644
--- a/app/src/main/res/values/dims.xml
+++ b/app/src/main/res/values/dims.xml
@@ -46,6 +46,9 @@
12sp
20dp
10dp
+
+ 140dp
2dp
22sp
2dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3b6cb7d99a11..f5881a573a79 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1549,4 +1549,15 @@
You do not have permission to change this label
Governance
Image details
+ Grid density
+
+ - Spacious
+ - Default
+ - Compact
+
+
+ - 2
+ - 3
+ - 4
+
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index aa73c18f2f11..37567d165aae 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -48,6 +48,12 @@
android:defaultValue="true"
android:title="@string/sort_favorites_first"
android:key="sort_favorites_first" />
+