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" /> +