From c2de51a078a8e827df5a60e470e6747f21253d7f Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Wed, 22 Apr 2026 13:48:14 +0200 Subject: [PATCH 01/16] feat: Add logic to move both panes --- .../mail/ui/main/folder/ThreadListFragment.kt | 2 + .../mail/ui/main/folder/TwoPaneFragment.kt | 59 +++++++++++++++++-- .../mail/ui/main/folder/TwoPaneViewModel.kt | 6 ++ .../mail/ui/main/search/SearchFragment.kt | 2 + app/src/main/res/layout/fragment_search.xml | 8 +++ .../main/res/layout/fragment_thread_list.xml | 8 +++ app/src/main/res/values/dimens.xml | 1 + 7 files changed, 82 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListFragment.kt index 7abbd2232e6..ec5caff7586 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListFragment.kt @@ -212,6 +212,8 @@ class ThreadListFragment : TwoPaneFragment(), PickerEmojiObserver { override fun getRightPane(): FragmentContainerView? = _binding?.threadHostFragment + override fun getDragSeparator(): View? = _binding?.dragSeparator + override fun getAnchor(): View? { return if (isOnlyRightShown()) { _binding?.threadHostFragment?.getFragment()?.getAnchor() diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt index ffe68685a30..fa7ab40b8ab 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt @@ -17,10 +17,13 @@ */ package com.infomaniak.mail.ui.main.folder +import android.annotation.SuppressLint import android.content.res.Configuration import android.os.Build.VERSION.SDK_INT import android.os.Bundle +import android.view.MotionEvent import android.view.View +import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.core.content.res.ResourcesCompat import androidx.core.view.isGone @@ -58,6 +61,12 @@ abstract class TwoPaneFragment : Fragment() { val mainViewModel: MainViewModel by activityViewModels() private val twoPaneViewModel: TwoPaneViewModel by activityViewModels() + private var dragStartX = 0f + private var dragStartLeftWidth = 0 + private val separatorWidthPx by lazy { + resources.getDimensionPixelSize(R.dimen.dragSeparatorWidth) + } + // TODO: When we'll update DragDropSwipeRecyclerViewLib, we'll need to make the adapter nullable. // For now it causes a memory leak, because we can't remove the strong reference // between the ThreadList's RecyclerView and its Adapter as it throws an NPE. @@ -70,6 +79,7 @@ abstract class TwoPaneFragment : Fragment() { abstract val substituteClassName: String abstract fun getLeftPane(): View? abstract fun getRightPane(): FragmentContainerView? + abstract fun getDragSeparator(): View? abstract fun getAnchor(): View? open fun doAfterFolderChanged() = Unit @@ -82,6 +92,7 @@ abstract class TwoPaneFragment : Fragment() { observeCurrentFolder() observeThreadUid() observeThreadNavigation() + observeDragSeparator() } override fun onConfigurationChanged(newConfig: Configuration) { @@ -136,6 +147,35 @@ abstract class TwoPaneFragment : Fragment() { } } + @SuppressLint("ClickableViewAccessibility") + private fun observeDragSeparator() { + getDragSeparator()?.setOnTouchListener { _, event -> + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + dragStartX = event.rawX + dragStartLeftWidth = getLeftPane()?.width ?: 0 + true + } + MotionEvent.ACTION_MOVE -> { + val deltaX = (event.rawX - dragStartX).toInt() + val parentWidth = (view as ViewGroup).width + val newLeftWidth = (dragStartLeftWidth + deltaX).coerceIn( + (parentWidth * 0.15f).toInt(), + (parentWidth * 0.85f).toInt() + ) + + getLeftPane()?.layoutParams?.width = newLeftWidth + getRightPane()?.layoutParams?.width = parentWidth - newLeftWidth - separatorWidthPx + view?.requestLayout() + + twoPaneViewModel.setLeftPaneRatio(newLeftWidth.toFloat() / parentWidth) + true + } + else -> false + } + } + } + fun handleOnBackPressed() { when { isOnlyRightShown() -> { @@ -192,14 +232,25 @@ abstract class TwoPaneFragment : Fragment() { rightPane.isVisible = true } } + + getDragSeparator()?.let { dragSeparator -> + if (!isTabletOrFoldable() || rightWidth == 0) { + dragSeparator.isGone = true + } else { + val separatorWidth = resources.getDimensionPixelSize(R.dimen.dragSeparatorWidth) + if (dragSeparator.width != separatorWidth) dragSeparator.layoutParams?.width = separatorWidth + dragSeparator.isVisible = true + } + } } - private fun computeTwoPaneWidths(widthPixels: Int, isThreadOpen: Boolean): Pair { - val leftPaneWidthRatio = ResourcesCompat.getFloat(resources, R.dimen.leftPaneWidthRatio) - val rightPaneWidthRatio = ResourcesCompat.getFloat(resources, R.dimen.rightPaneWidthRatio) + private fun computeTwoPaneWidths(widthPixels: Int, isThreadOpen: Boolean): Pair { return if (isTabletOrFoldable()) { - (leftPaneWidthRatio * widthPixels).toInt() to (rightPaneWidthRatio * widthPixels).toInt() + val ratio = twoPaneViewModel.leftPaneRatio + ?: ResourcesCompat.getFloat(resources, R.dimen.leftPaneWidthRatio) + val leftWidth = (ratio * widthPixels).toInt() + leftWidth to (widthPixels - leftWidth) } else { if (isThreadOpen) 0 to widthPixels else widthPixels to 0 } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt index d2c726017a9..da62ae4705a 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt @@ -51,6 +51,7 @@ class TwoPaneViewModel @Inject constructor( val currentThreadUid: LiveData = state.getLiveData(CURRENT_THREAD_UID_KEY) + val leftPaneRatio: Float? get() = state[LEFT_PANE_RATIO_KEY] inline val isThreadOpen get() = currentThreadUid.value != null val rightPaneFolderName = MutableLiveData() var previousFolderId: String? = null @@ -70,6 +71,10 @@ class TwoPaneViewModel @Inject constructor( state[CURRENT_THREAD_UID_KEY] = null } + fun setLeftPaneRatio(ratio: Float) { + state[LEFT_PANE_RATIO_KEY] = ratio.coerceIn(0.15f, 0.85f) + } + fun openDraft(thread: Thread) = viewModelScope.launch(ioCoroutineContext) { navigateToSelectedDraft(thread.messages.single()) } @@ -120,5 +125,6 @@ class TwoPaneViewModel @Inject constructor( companion object { private const val CURRENT_THREAD_UID_KEY = "currentThreadUidKey" + private const val LEFT_PANE_RATIO_KEY = "leftPaneRatioKey" } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/search/SearchFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/search/SearchFragment.kt index 809589fb612..a4aa051ac73 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/search/SearchFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/search/SearchFragment.kt @@ -159,6 +159,8 @@ class SearchFragment : TwoPaneFragment() { override fun getRightPane(): FragmentContainerView? = _binding?.threadHostFragment + override fun getDragSeparator(): View? = _binding?.dragSeparator + override fun getAnchor(): View? { return if (isOnlyLeftShown()) { null diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 54c3529ff43..c701e7c58f6 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -206,6 +206,14 @@ + + + + 0.4 0.6 + 4dp 110dp From 20fb98fbecbde6c34c1c197048af8430f2154fc5 Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Wed, 22 Apr 2026 16:48:33 +0200 Subject: [PATCH 02/16] feat: Handle text display when space is limited --- app/src/main/res/layout/item_message.xml | 2 ++ app/src/main/res/layout/view_empty_state.xml | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/item_message.xml b/app/src/main/res/layout/item_message.xml index 75c0e6561bf..66e9bf18af7 100644 --- a/app/src/main/res/layout/item_message.xml +++ b/app/src/main/res/layout/item_message.xml @@ -109,6 +109,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/marginStandardSmall" + android:ellipsize="end" + android:lines="1" app:layout_constraintBaseline_toBaselineOf="@id/expeditorName" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/scheduleSendIcon" diff --git a/app/src/main/res/layout/view_empty_state.xml b/app/src/main/res/layout/view_empty_state.xml index 8955de81a85..5bd57de6320 100644 --- a/app/src/main/res/layout/view_empty_state.xml +++ b/app/src/main/res/layout/view_empty_state.xml @@ -17,9 +17,10 @@ --> @@ -33,20 +34,22 @@ From b125e69f3a5cc799a313c2e4258f87595e8ab2d4 Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Wed, 22 Apr 2026 18:43:30 +0200 Subject: [PATCH 03/16] feat: Add handle with white space --- .../mail/ui/main/folder/TwoPaneFragment.kt | 16 ++++++++++++- .../drawable/item_vertical_drag_handle.xml | 24 +++++++++++++++++++ app/src/main/res/layout/fragment_search.xml | 14 +++++++---- .../main/res/layout/fragment_thread_list.xml | 15 ++++++++---- app/src/main/res/values/dimens.xml | 1 + 5 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 app/src/main/res/drawable/item_vertical_drag_handle.xml diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt index fa7ab40b8ab..12d8681d80d 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt @@ -19,9 +19,12 @@ package com.infomaniak.mail.ui.main.folder import android.annotation.SuppressLint import android.content.res.Configuration +import android.graphics.Rect import android.os.Build.VERSION.SDK_INT import android.os.Bundle +import android.util.Log import android.view.MotionEvent +import android.view.TouchDelegate import android.view.View import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult @@ -150,13 +153,19 @@ abstract class TwoPaneFragment : Fragment() { @SuppressLint("ClickableViewAccessibility") private fun observeDragSeparator() { getDragSeparator()?.setOnTouchListener { _, event -> + val dragHandleVisual = ((view as? ViewGroup)?.getChildAt(1) as? ViewGroup)?.getChildAt(0) when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { dragStartX = event.rawX dragStartLeftWidth = getLeftPane()?.width ?: 0 + + dragHandleVisual?.alpha = 0.5f true } MotionEvent.ACTION_MOVE -> { + + + val deltaX = (event.rawX - dragStartX).toInt() val parentWidth = (view as ViewGroup).width val newLeftWidth = (dragStartLeftWidth + deltaX).coerceIn( @@ -171,6 +180,11 @@ abstract class TwoPaneFragment : Fragment() { twoPaneViewModel.setLeftPaneRatio(newLeftWidth.toFloat() / parentWidth) true } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + dragHandleVisual?.alpha = 1.0f + + true + } else -> false } } @@ -237,7 +251,7 @@ abstract class TwoPaneFragment : Fragment() { if (!isTabletOrFoldable() || rightWidth == 0) { dragSeparator.isGone = true } else { - val separatorWidth = resources.getDimensionPixelSize(R.dimen.dragSeparatorWidth) + val separatorWidth = resources.getDimensionPixelSize(R.dimen.dragSeparatorTouchWidth) if (dragSeparator.width != separatorWidth) dragSeparator.layoutParams?.width = separatorWidth dragSeparator.isVisible = true } diff --git a/app/src/main/res/drawable/item_vertical_drag_handle.xml b/app/src/main/res/drawable/item_vertical_drag_handle.xml new file mode 100644 index 00000000000..06020b10a10 --- /dev/null +++ b/app/src/main/res/drawable/item_vertical_drag_handle.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index c701e7c58f6..c2710030601 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -206,13 +206,19 @@ - + android:visibility="gone"> + + + - + android:focusable="true" + android:visibility="gone"> + + 0.4 0.6 4dp + 16dp 110dp From 01de527a64ca95bc6d69ed8ee859f04f1f044b4d Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Thu, 23 Apr 2026 09:04:08 +0200 Subject: [PATCH 04/16] feat: Add handle design --- app/src/main/res/drawable/item_drag_line.xml | 30 ++++++++ .../drawable/item_vertical_drag_handle.xml | 74 +++++++++++++++++-- app/src/main/res/layout/fragment_search.xml | 18 ++++- .../main/res/layout/fragment_thread_list.xml | 17 ++++- app/src/main/res/values/dimens.xml | 5 ++ 5 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 app/src/main/res/drawable/item_drag_line.xml diff --git a/app/src/main/res/drawable/item_drag_line.xml b/app/src/main/res/drawable/item_drag_line.xml new file mode 100644 index 00000000000..495f85ac50b --- /dev/null +++ b/app/src/main/res/drawable/item_drag_line.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/item_vertical_drag_handle.xml b/app/src/main/res/drawable/item_vertical_drag_handle.xml index 06020b10a10..3f929d233f8 100644 --- a/app/src/main/res/drawable/item_vertical_drag_handle.xml +++ b/app/src/main/res/drawable/item_vertical_drag_handle.xml @@ -4,7 +4,7 @@ ~ ~ This program is free software: you can redistribute it and/or modify ~ it under the terms of the GNU General Public License as published by - ~ the Free Software Foundation, either version 3 of the License, or + ~ the Free Software Foundation, either version 4 of the License, or ~ (at your option) any later version. ~ ~ This program is distributed in the hope that it will be useful, @@ -15,10 +15,72 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - + - + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index c2710030601..f805b7a9cf4 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -208,16 +208,28 @@ + android:focusable="true" + android:translationZ="10dp"> + android:background="@drawable/item_drag_line" /> + + + + android:translationZ="10dp"> + + + @@ -304,4 +314,5 @@ android:layout_weight="1" android:visibility="gone" /> + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 0d14fba3e76..98b341fd9e2 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -22,6 +22,11 @@ 0.6 4dp 16dp + 250dp + 300dp + 1dp + 3dp + 8dp 110dp From e819010340982d93e4d4850481b6ef83c9a34199 Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Thu, 23 Apr 2026 16:34:19 +0200 Subject: [PATCH 05/16] feat: Save ratio to localSettings --- .../main/java/com/infomaniak/mail/data/LocalSettings.kt | 1 + .../infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/data/LocalSettings.kt b/app/src/main/java/com/infomaniak/mail/data/LocalSettings.kt index bb401800f0b..81c02724a3e 100644 --- a/app/src/main/java/com/infomaniak/mail/data/LocalSettings.kt +++ b/app/src/main/java/com/infomaniak/mail/data/LocalSettings.kt @@ -51,6 +51,7 @@ class LocalSettings private constructor(context: Context) : SharedValues { var askEmailAcknowledgement by sharedValue("askEmailAcknowledgmentKey", false) var hasAlreadyEnabledNotifications by sharedValue("hasAlreadyEnabledNotificationsKey", false) var isAppLocked by sharedValue("isAppLockedKey", false) + var leftPaneRatio by sharedValue("leftPaneRatio", 0.4f) var threadDensity by sharedValue("threadDensityKey", ThreadDensity.LARGE) var theme by sharedValue("themeKey", if (SDK_INT >= 29) Theme.SYSTEM else Theme.LIGHT) var accentColor by sharedValue("accentColorKey", AccentColor.PINK) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt index da62ae4705a..4cd8abd4057 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt @@ -26,6 +26,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.infomaniak.core.legacy.utils.SingleLiveEvent +import com.infomaniak.mail.data.LocalSettings import com.infomaniak.mail.data.cache.mailboxContent.DraftController import com.infomaniak.mail.data.models.draft.Draft.DraftMode import com.infomaniak.mail.data.models.message.Message @@ -47,11 +48,13 @@ class TwoPaneViewModel @Inject constructor( @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) : ViewModel() { + @Inject + lateinit var localSettings: LocalSettings private val ioCoroutineContext = viewModelScope.coroutineContext(ioDispatcher) val currentThreadUid: LiveData = state.getLiveData(CURRENT_THREAD_UID_KEY) - val leftPaneRatio: Float? get() = state[LEFT_PANE_RATIO_KEY] + val leftPaneRatio: Float get() = localSettings.leftPaneRatio inline val isThreadOpen get() = currentThreadUid.value != null val rightPaneFolderName = MutableLiveData() var previousFolderId: String? = null @@ -72,7 +75,7 @@ class TwoPaneViewModel @Inject constructor( } fun setLeftPaneRatio(ratio: Float) { - state[LEFT_PANE_RATIO_KEY] = ratio.coerceIn(0.15f, 0.85f) + localSettings.leftPaneRatio = ratio.coerceIn(0.15f, 0.85f) } fun openDraft(thread: Thread) = viewModelScope.launch(ioCoroutineContext) { @@ -125,6 +128,5 @@ class TwoPaneViewModel @Inject constructor( companion object { private const val CURRENT_THREAD_UID_KEY = "currentThreadUidKey" - private const val LEFT_PANE_RATIO_KEY = "leftPaneRatioKey" } } From bfded29746e019e6279dd3a6c54b7e988a618b94 Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Thu, 23 Apr 2026 16:36:52 +0200 Subject: [PATCH 06/16] feat: Add restrictions to block sliding when size becomes too small --- .../mail/ui/main/folder/TwoPaneFragment.kt | 39 +++++++++---------- app/src/main/res/values/dimens.xml | 4 -- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt index 12d8681d80d..156e58261b8 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt @@ -19,16 +19,12 @@ package com.infomaniak.mail.ui.main.folder import android.annotation.SuppressLint import android.content.res.Configuration -import android.graphics.Rect import android.os.Build.VERSION.SDK_INT import android.os.Bundle -import android.util.Log import android.view.MotionEvent -import android.view.TouchDelegate import android.view.View import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult -import androidx.core.content.res.ResourcesCompat import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment @@ -66,9 +62,9 @@ abstract class TwoPaneFragment : Fragment() { private var dragStartX = 0f private var dragStartLeftWidth = 0 - private val separatorWidthPx by lazy { - resources.getDimensionPixelSize(R.dimen.dragSeparatorWidth) - } + private val separatorWidthPx by lazy { resources.getDimensionPixelSize(R.dimen.dragSeparatorWidth) } + val minLeftWidthPx by lazy { resources.getDimensionPixelSize(R.dimen.minLeftPaneWidth) } + val minRightWidthPx by lazy { resources.getDimensionPixelSize(R.dimen.minRightPaneWidth) } // TODO: When we'll update DragDropSwipeRecyclerViewLib, we'll need to make the adapter nullable. // For now it causes a memory leak, because we can't remove the strong reference @@ -152,25 +148,27 @@ abstract class TwoPaneFragment : Fragment() { @SuppressLint("ClickableViewAccessibility") private fun observeDragSeparator() { - getDragSeparator()?.setOnTouchListener { _, event -> - val dragHandleVisual = ((view as? ViewGroup)?.getChildAt(1) as? ViewGroup)?.getChildAt(0) + getDragSeparator()?.setOnTouchListener { separatorView, event -> when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { dragStartX = event.rawX dragStartLeftWidth = getLeftPane()?.width ?: 0 - dragHandleVisual?.alpha = 0.5f + separatorView.isPressed = true + + val width = resources.getDimensionPixelSize(R.dimen.dragSeparatorWidthSelected) + val param = getDragSeparator()?.findViewById(R.id.lineDragSeparator)?.layoutParams?.apply { + this.width = width + } + getDragSeparator()?.findViewById(R.id.lineDragSeparator)?.layoutParams = param true } MotionEvent.ACTION_MOVE -> { - - - val deltaX = (event.rawX - dragStartX).toInt() val parentWidth = (view as ViewGroup).width val newLeftWidth = (dragStartLeftWidth + deltaX).coerceIn( - (parentWidth * 0.15f).toInt(), - (parentWidth * 0.85f).toInt() + (minLeftWidthPx), + (parentWidth - minRightWidthPx) ) getLeftPane()?.layoutParams?.width = newLeftWidth @@ -181,8 +179,12 @@ abstract class TwoPaneFragment : Fragment() { true } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { - dragHandleVisual?.alpha = 1.0f - + separatorView.isPressed = false + val width = resources.getDimensionPixelSize(R.dimen.dragSeparatorWidth) + val param = getDragSeparator()?.findViewById(R.id.lineDragSeparator)?.layoutParams?.apply { + this.width = width + } + getDragSeparator()?.findViewById(R.id.lineDragSeparator)?.layoutParams = param true } else -> false @@ -251,8 +253,6 @@ abstract class TwoPaneFragment : Fragment() { if (!isTabletOrFoldable() || rightWidth == 0) { dragSeparator.isGone = true } else { - val separatorWidth = resources.getDimensionPixelSize(R.dimen.dragSeparatorTouchWidth) - if (dragSeparator.width != separatorWidth) dragSeparator.layoutParams?.width = separatorWidth dragSeparator.isVisible = true } } @@ -262,7 +262,6 @@ abstract class TwoPaneFragment : Fragment() { private fun computeTwoPaneWidths(widthPixels: Int, isThreadOpen: Boolean): Pair { return if (isTabletOrFoldable()) { val ratio = twoPaneViewModel.leftPaneRatio - ?: ResourcesCompat.getFloat(resources, R.dimen.leftPaneWidthRatio) val leftWidth = (ratio * widthPixels).toInt() leftWidth to (widthPixels - leftWidth) } else { diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 98b341fd9e2..50e7979eb23 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -18,10 +18,6 @@ - 0.4 - 0.6 - 4dp - 16dp 250dp 300dp 1dp From 64e99c02e466ecb6bc31e4852d239ce1f47c04b4 Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Thu, 23 Apr 2026 16:48:24 +0200 Subject: [PATCH 07/16] refactor: Optimize touch listener --- .../mail/ui/main/folder/TwoPaneFragment.kt | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt index 156e58261b8..e942fa000ba 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt @@ -27,6 +27,7 @@ import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.activityViewModels @@ -148,43 +149,52 @@ abstract class TwoPaneFragment : Fragment() { @SuppressLint("ClickableViewAccessibility") private fun observeDragSeparator() { - getDragSeparator()?.setOnTouchListener { separatorView, event -> + val separator = getDragSeparator() ?: return + val leftPane = getLeftPane() ?: return + val rightPane = getRightPane() ?: return + val lineSeparator = separator.findViewById(R.id.lineDragSeparator) + + val parentGroup = view as? ViewGroup ?: return + + val widthSelected = resources.getDimensionPixelSize(R.dimen.dragSeparatorWidthSelected) + val widthNormal = resources.getDimensionPixelSize(R.dimen.dragSeparatorWidth) + + separator.setOnTouchListener { separatorView, event -> when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { dragStartX = event.rawX - dragStartLeftWidth = getLeftPane()?.width ?: 0 - + dragStartLeftWidth = leftPane.width separatorView.isPressed = true - val width = resources.getDimensionPixelSize(R.dimen.dragSeparatorWidthSelected) - val param = getDragSeparator()?.findViewById(R.id.lineDragSeparator)?.layoutParams?.apply { - this.width = width + lineSeparator?.layoutParams = lineSeparator.layoutParams?.apply { + width = widthSelected } - getDragSeparator()?.findViewById(R.id.lineDragSeparator)?.layoutParams = param true } + MotionEvent.ACTION_MOVE -> { val deltaX = (event.rawX - dragStartX).toInt() - val parentWidth = (view as ViewGroup).width + val parentWidth = parentGroup.width + val newLeftWidth = (dragStartLeftWidth + deltaX).coerceIn( - (minLeftWidthPx), - (parentWidth - minRightWidthPx) + minLeftWidthPx, + parentWidth - minRightWidthPx ) - getLeftPane()?.layoutParams?.width = newLeftWidth - getRightPane()?.layoutParams?.width = parentWidth - newLeftWidth - separatorWidthPx - view?.requestLayout() + leftPane.layoutParams.width = newLeftWidth + rightPane.layoutParams.width = parentWidth - newLeftWidth - separatorWidthPx + parentGroup.requestLayout() twoPaneViewModel.setLeftPaneRatio(newLeftWidth.toFloat() / parentWidth) true } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { separatorView.isPressed = false - val width = resources.getDimensionPixelSize(R.dimen.dragSeparatorWidth) - val param = getDragSeparator()?.findViewById(R.id.lineDragSeparator)?.layoutParams?.apply { - this.width = width + + lineSeparator?.layoutParams = lineSeparator.layoutParams?.apply { + width = widthNormal } - getDragSeparator()?.findViewById(R.id.lineDragSeparator)?.layoutParams = param true } else -> false From 5c2ed29156a69bfea0d965294cd8ce2e2f3f7e8a Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Fri, 24 Apr 2026 08:43:03 +0200 Subject: [PATCH 08/16] fix: Correct minor offset on first tap --- .../com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt | 6 +++--- .../com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt index e942fa000ba..777c190d7ff 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt @@ -27,7 +27,6 @@ import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.core.view.isGone import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.activityViewModels @@ -272,8 +271,9 @@ abstract class TwoPaneFragment : Fragment() { private fun computeTwoPaneWidths(widthPixels: Int, isThreadOpen: Boolean): Pair { return if (isTabletOrFoldable()) { val ratio = twoPaneViewModel.leftPaneRatio - val leftWidth = (ratio * widthPixels).toInt() - leftWidth to (widthPixels - leftWidth) + val leftWidth = (ratio * widthPixels).toInt().coerceIn(minLeftWidthPx, widthPixels - minRightWidthPx) + + leftWidth to (widthPixels - leftWidth - separatorWidthPx) } else { if (isThreadOpen) 0 to widthPixels else widthPixels to 0 } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt index 4cd8abd4057..783fb8baa92 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt @@ -75,7 +75,7 @@ class TwoPaneViewModel @Inject constructor( } fun setLeftPaneRatio(ratio: Float) { - localSettings.leftPaneRatio = ratio.coerceIn(0.15f, 0.85f) + localSettings.leftPaneRatio = ratio } fun openDraft(thread: Thread) = viewModelScope.launch(ioCoroutineContext) { From 00ebc395154489be2d69e4a37fb2175f4ecb0551 Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Fri, 24 Apr 2026 12:42:27 +0200 Subject: [PATCH 09/16] refactor: Restrict localSettings read/write operations to app launch and close only --- .../mail/ui/main/folder/TwoPaneFragment.kt | 6 +++++- .../mail/ui/main/folder/TwoPaneViewModel.kt | 15 +++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt index 777c190d7ff..28e93d9bb4c 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt @@ -146,6 +146,10 @@ abstract class TwoPaneFragment : Fragment() { } } + override fun onStop() { + twoPaneViewModel.saveLeftPaneRatio() + super.onStop() + } @SuppressLint("ClickableViewAccessibility") private fun observeDragSeparator() { val separator = getDragSeparator() ?: return @@ -184,7 +188,7 @@ abstract class TwoPaneFragment : Fragment() { rightPane.layoutParams.width = parentWidth - newLeftWidth - separatorWidthPx parentGroup.requestLayout() - twoPaneViewModel.setLeftPaneRatio(newLeftWidth.toFloat() / parentWidth) + twoPaneViewModel.leftPaneRatio = newLeftWidth.toFloat() / parentWidth true } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt index 783fb8baa92..37c1d893488 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneViewModel.kt @@ -45,16 +45,14 @@ import javax.inject.Inject class TwoPaneViewModel @Inject constructor( private val state: SavedStateHandle, private val draftController: DraftController, + private val localSettings: LocalSettings, @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) : ViewModel() { - - @Inject - lateinit var localSettings: LocalSettings private val ioCoroutineContext = viewModelScope.coroutineContext(ioDispatcher) val currentThreadUid: LiveData = state.getLiveData(CURRENT_THREAD_UID_KEY) - val leftPaneRatio: Float get() = localSettings.leftPaneRatio + var leftPaneRatio: Float = localSettings.leftPaneRatio inline val isThreadOpen get() = currentThreadUid.value != null val rightPaneFolderName = MutableLiveData() var previousFolderId: String? = null @@ -66,6 +64,11 @@ class TwoPaneViewModel @Inject constructor( val newMessageArgs = SingleLiveEvent() val navArgs = SingleLiveEvent() + override fun onCleared() { + saveLeftPaneRatio() + super.onCleared() + } + fun openThread(uid: String) { state[CURRENT_THREAD_UID_KEY] = uid } @@ -74,8 +77,8 @@ class TwoPaneViewModel @Inject constructor( state[CURRENT_THREAD_UID_KEY] = null } - fun setLeftPaneRatio(ratio: Float) { - localSettings.leftPaneRatio = ratio + fun saveLeftPaneRatio() { + localSettings.leftPaneRatio = leftPaneRatio } fun openDraft(thread: Thread) = viewModelScope.launch(ioCoroutineContext) { From fffad91fc0828b064c88a6856e50a9afb4bf9f15 Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Fri, 24 Apr 2026 12:57:37 +0200 Subject: [PATCH 10/16] feat: Update color --- app/src/main/res/drawable/item_vertical_drag_handle.xml | 2 +- app/src/main/res/layout/fragment_search.xml | 1 - app/src/main/res/layout/fragment_thread_list.xml | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/res/drawable/item_vertical_drag_handle.xml b/app/src/main/res/drawable/item_vertical_drag_handle.xml index 3f929d233f8..115d3200b13 100644 --- a/app/src/main/res/drawable/item_vertical_drag_handle.xml +++ b/app/src/main/res/drawable/item_vertical_drag_handle.xml @@ -55,7 +55,7 @@ - + diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index f805b7a9cf4..2f64a201275 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -225,7 +225,6 @@ - From 2bb3be9a06d4e3371c1d13a0b0ff26ec0cbb218c Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Fri, 24 Apr 2026 13:17:34 +0200 Subject: [PATCH 11/16] fix: Add android:duplicateParentState to separator child views to enable pressed state styling --- app/src/main/res/layout/fragment_search.xml | 7 ++++--- app/src/main/res/layout/fragment_thread_list.xml | 6 ++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 2f64a201275..d22d1c525cf 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -221,14 +221,15 @@ android:layout_width="1dp" android:layout_height="match_parent" android:layout_gravity="center" - android:background="@drawable/item_drag_line" /> - + android:background="@drawable/item_drag_line" + android:duplicateParentState="true" /> + android:background="@drawable/item_vertical_drag_handle" + android:duplicateParentState="true" /> + android:background="@drawable/item_drag_line" + android:duplicateParentState="true" /> + android:background="@drawable/item_vertical_drag_handle" + android:duplicateParentState="true" /> Date: Fri, 24 Apr 2026 13:30:50 +0200 Subject: [PATCH 12/16] refactor: Reduce cognitive complexity --- .../mail/ui/main/folder/TwoPaneFragment.kt | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt index 28e93d9bb4c..ba7dc5c3468 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt @@ -238,37 +238,26 @@ abstract class TwoPaneFragment : Fragment() { } private fun updateTwoPaneVisibilities() { - val (leftWidth, rightWidth) = computeTwoPaneWidths( widthPixels = requireActivity().application.resources.displayMetrics.widthPixels, isThreadOpen = twoPaneViewModel.isThreadOpen, ) - getLeftPane()?.let { leftPane -> - if (leftWidth == 0) { - leftPane.isGone = true - } else { - if (leftPane.width != leftWidth) leftPane.layoutParams?.width = leftWidth - leftPane.isVisible = true + getLeftPane()?.apply { + isVisible = leftWidth != 0 + if (leftWidth != 0 && width != leftWidth) { + layoutParams?.width = leftWidth } } - getRightPane()?.let { rightPane -> - if (rightWidth == 0) { - rightPane.isGone = true - } else { - if (rightPane.width != rightWidth) rightPane.layoutParams?.width = rightWidth - rightPane.isVisible = true + getRightPane()?.apply { + isVisible = rightWidth != 0 + if (rightWidth != 0 && width != rightWidth) { + layoutParams?.width = rightWidth } } - getDragSeparator()?.let { dragSeparator -> - if (!isTabletOrFoldable() || rightWidth == 0) { - dragSeparator.isGone = true - } else { - dragSeparator.isVisible = true - } - } + getDragSeparator()?.isVisible = isTabletOrFoldable() && rightWidth != 0 } From 28e2915c4e4557fd14f174f31d6b41711c145a78 Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Fri, 24 Apr 2026 13:36:22 +0200 Subject: [PATCH 13/16] fix: Handle edge cases when user is in multi-window mode --- .../mail/ui/main/folder/TwoPaneFragment.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt index ba7dc5c3468..f8c37a1a258 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt @@ -25,7 +25,6 @@ import android.view.MotionEvent import android.view.View import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult -import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentContainerView @@ -179,9 +178,11 @@ abstract class TwoPaneFragment : Fragment() { val deltaX = (event.rawX - dragStartX).toInt() val parentWidth = parentGroup.width + val maxLeftWidth = parentWidth - minRightWidthPx + val safeMaxLeftWidth = maxOf(minLeftWidthPx, maxLeftWidth) val newLeftWidth = (dragStartLeftWidth + deltaX).coerceIn( minLeftWidthPx, - parentWidth - minRightWidthPx + safeMaxLeftWidth ) leftPane.layoutParams.width = newLeftWidth @@ -264,9 +265,15 @@ abstract class TwoPaneFragment : Fragment() { private fun computeTwoPaneWidths(widthPixels: Int, isThreadOpen: Boolean): Pair { return if (isTabletOrFoldable()) { val ratio = twoPaneViewModel.leftPaneRatio - val leftWidth = (ratio * widthPixels).toInt().coerceIn(minLeftWidthPx, widthPixels - minRightWidthPx) - leftWidth to (widthPixels - leftWidth - separatorWidthPx) + val availableWidth = (widthPixels - separatorWidthPx).coerceAtLeast(0) + val leftWidth = if (availableWidth < minLeftWidthPx + minRightWidthPx) { + (ratio * availableWidth).toInt().coerceIn(0, availableWidth) + } else { + (ratio * availableWidth).toInt().coerceIn(minLeftWidthPx, availableWidth - minRightWidthPx) + } + leftWidth to (availableWidth - leftWidth) + } else { if (isThreadOpen) 0 to widthPixels else widthPixels to 0 } From afb8c22c21b3acc7f358250474d195773e71bc93 Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Fri, 24 Apr 2026 13:52:25 +0200 Subject: [PATCH 14/16] fix: Add performClick to improve accessibility --- .../mail/ui/main/folder/TwoPaneFragment.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt index f8c37a1a258..602993cdad3 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt @@ -149,7 +149,7 @@ abstract class TwoPaneFragment : Fragment() { twoPaneViewModel.saveLeftPaneRatio() super.onStop() } - @SuppressLint("ClickableViewAccessibility") + private fun observeDragSeparator() { val separator = getDragSeparator() ?: return val leftPane = getLeftPane() ?: return @@ -193,9 +193,17 @@ abstract class TwoPaneFragment : Fragment() { true } - MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + MotionEvent.ACTION_UP -> { separatorView.isPressed = false + lineSeparator?.layoutParams = lineSeparator.layoutParams?.apply { + width = widthNormal + } + separatorView.performClick() + true + } + MotionEvent.ACTION_CANCEL -> { + separatorView.isPressed = false lineSeparator?.layoutParams = lineSeparator.layoutParams?.apply { width = widthNormal } From a803108e1fc232d5596ecd07abda77bbf6e5a224 Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Fri, 24 Apr 2026 14:18:59 +0200 Subject: [PATCH 15/16] refactor: Clean code --- .../com/infomaniak/mail/data/LocalSettings.kt | 2 +- .../mail/ui/main/folder/TwoPaneFragment.kt | 70 +++++++++++-------- .../drawable/item_vertical_drag_handle.xml | 6 +- app/src/main/res/layout/fragment_search.xml | 12 ++-- .../main/res/layout/fragment_thread_list.xml | 12 ++-- app/src/main/res/values/dimens.xml | 7 +- 6 files changed, 63 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/data/LocalSettings.kt b/app/src/main/java/com/infomaniak/mail/data/LocalSettings.kt index 81c02724a3e..fa1c475a42d 100644 --- a/app/src/main/java/com/infomaniak/mail/data/LocalSettings.kt +++ b/app/src/main/java/com/infomaniak/mail/data/LocalSettings.kt @@ -51,7 +51,7 @@ class LocalSettings private constructor(context: Context) : SharedValues { var askEmailAcknowledgement by sharedValue("askEmailAcknowledgmentKey", false) var hasAlreadyEnabledNotifications by sharedValue("hasAlreadyEnabledNotificationsKey", false) var isAppLocked by sharedValue("isAppLockedKey", false) - var leftPaneRatio by sharedValue("leftPaneRatio", 0.4f) + var leftPaneRatio by sharedValue("leftPaneRatioKey", 0.4f) var threadDensity by sharedValue("threadDensityKey", ThreadDensity.LARGE) var theme by sharedValue("themeKey", if (SDK_INT >= 29) Theme.SYSTEM else Theme.LIGHT) var accentColor by sharedValue("accentColorKey", AccentColor.PINK) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt index 602993cdad3..ca2740938cd 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt @@ -155,7 +155,6 @@ abstract class TwoPaneFragment : Fragment() { val leftPane = getLeftPane() ?: return val rightPane = getRightPane() ?: return val lineSeparator = separator.findViewById(R.id.lineDragSeparator) - val parentGroup = view as? ViewGroup ?: return val widthSelected = resources.getDimensionPixelSize(R.dimen.dragSeparatorWidthSelected) @@ -166,54 +165,67 @@ abstract class TwoPaneFragment : Fragment() { MotionEvent.ACTION_DOWN -> { dragStartX = event.rawX dragStartLeftWidth = leftPane.width - separatorView.isPressed = true - - lineSeparator?.layoutParams = lineSeparator.layoutParams?.apply { - width = widthSelected - } + updateSeparatorAppearance(separatorView, lineSeparator, true, widthSelected) true } MotionEvent.ACTION_MOVE -> { - val deltaX = (event.rawX - dragStartX).toInt() - val parentWidth = parentGroup.width - - val maxLeftWidth = parentWidth - minRightWidthPx - val safeMaxLeftWidth = maxOf(minLeftWidthPx, maxLeftWidth) - val newLeftWidth = (dragStartLeftWidth + deltaX).coerceIn( - minLeftWidthPx, - safeMaxLeftWidth - ) - - leftPane.layoutParams.width = newLeftWidth - rightPane.layoutParams.width = parentWidth - newLeftWidth - separatorWidthPx - parentGroup.requestLayout() - - twoPaneViewModel.leftPaneRatio = newLeftWidth.toFloat() / parentWidth + handleDragMove(event.rawX, leftPane, rightPane, parentGroup) true } MotionEvent.ACTION_UP -> { - separatorView.isPressed = false - lineSeparator?.layoutParams = lineSeparator.layoutParams?.apply { - width = widthNormal - } + updateSeparatorAppearance(separatorView, lineSeparator, false, widthNormal) separatorView.performClick() true } MotionEvent.ACTION_CANCEL -> { - separatorView.isPressed = false - lineSeparator?.layoutParams = lineSeparator.layoutParams?.apply { - width = widthNormal - } + updateSeparatorAppearance(separatorView, lineSeparator, false, widthNormal) true } + else -> false } } } + private fun updateSeparatorAppearance( + separatorView: View, + lineSeparator: View?, + isPressed: Boolean, + lineWidth: Int + ) { + separatorView.isPressed = isPressed + lineSeparator?.layoutParams = lineSeparator.layoutParams?.apply { + width = lineWidth + } + } + + private fun handleDragMove( + currentRawX: Float, + leftPane: View, + rightPane: View, + parentGroup: ViewGroup + ) { + val deltaX = (currentRawX - dragStartX).toInt() + val parentWidth = parentGroup.width + + val maxLeftWidth = parentWidth - minRightWidthPx + val safeMaxLeftWidth = maxOf(minLeftWidthPx, maxLeftWidth) + + val newLeftWidth = (dragStartLeftWidth + deltaX).coerceIn( + minLeftWidthPx, + safeMaxLeftWidth + ) + + leftPane.layoutParams.width = newLeftWidth + rightPane.layoutParams.width = parentWidth - newLeftWidth - separatorWidthPx + parentGroup.requestLayout() + + twoPaneViewModel.leftPaneRatio = newLeftWidth.toFloat() / parentWidth + } + fun handleOnBackPressed() { when { isOnlyRightShown() -> { diff --git a/app/src/main/res/drawable/item_vertical_drag_handle.xml b/app/src/main/res/drawable/item_vertical_drag_handle.xml index 115d3200b13..918e4da7c9f 100644 --- a/app/src/main/res/drawable/item_vertical_drag_handle.xml +++ b/app/src/main/res/drawable/item_vertical_drag_handle.xml @@ -4,7 +4,7 @@ ~ ~ This program is free software: you can redistribute it and/or modify ~ it under the terms of the GNU General Public License as published by - ~ the Free Software Foundation, either version 4 of the License, or + ~ the Free Software Foundation, either version 3 of the License, or ~ (at your option) any later version. ~ ~ This program is distributed in the hope that it will be useful, @@ -21,7 +21,7 @@ - + @@ -54,7 +54,7 @@ - + diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index d22d1c525cf..80070caa182 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -208,13 +208,13 @@ + android:translationZ="@dimen/dragSeparatorTranslationZ"> diff --git a/app/src/main/res/layout/fragment_thread_list.xml b/app/src/main/res/layout/fragment_thread_list.xml index 41a8aeab79f..43ced477ae4 100644 --- a/app/src/main/res/layout/fragment_thread_list.xml +++ b/app/src/main/res/layout/fragment_thread_list.xml @@ -283,13 +283,13 @@ + android:translationZ="@dimen/dragSeparatorTranslationZ"> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 50e7979eb23..977faea0872 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -22,7 +22,12 @@ 300dp 1dp 3dp - 8dp + 8dp + 25dp + 48dp + -24dp + 10dp + 110dp From 9dd1035102fb33d0bccfd9077c9537fe25a22440 Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Fri, 24 Apr 2026 15:20:17 +0200 Subject: [PATCH 16/16] fix: Remove one-pixel offset when clicking on the thread list --- .../mail/ui/main/folder/TwoPaneFragment.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt index ca2740938cd..6e2754b9ca1 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt @@ -53,6 +53,7 @@ import com.infomaniak.mail.utils.extensions.isTabletOrFoldable import com.infomaniak.mail.utils.extensions.safeNavigateToNewMessageActivity import io.realm.kotlin.types.RealmInstant import javax.inject.Inject +import kotlin.math.roundToInt abstract class TwoPaneFragment : Fragment() { @@ -211,7 +212,9 @@ abstract class TwoPaneFragment : Fragment() { val deltaX = (currentRawX - dragStartX).toInt() val parentWidth = parentGroup.width - val maxLeftWidth = parentWidth - minRightWidthPx + val availableWidth = parentWidth - separatorWidthPx + + val maxLeftWidth = availableWidth - minRightWidthPx val safeMaxLeftWidth = maxOf(minLeftWidthPx, maxLeftWidth) val newLeftWidth = (dragStartLeftWidth + deltaX).coerceIn( @@ -223,7 +226,9 @@ abstract class TwoPaneFragment : Fragment() { rightPane.layoutParams.width = parentWidth - newLeftWidth - separatorWidthPx parentGroup.requestLayout() - twoPaneViewModel.leftPaneRatio = newLeftWidth.toFloat() / parentWidth + if (availableWidth > 0) { + twoPaneViewModel.leftPaneRatio = newLeftWidth.toFloat() / availableWidth + } } fun handleOnBackPressed() { @@ -288,9 +293,9 @@ abstract class TwoPaneFragment : Fragment() { val availableWidth = (widthPixels - separatorWidthPx).coerceAtLeast(0) val leftWidth = if (availableWidth < minLeftWidthPx + minRightWidthPx) { - (ratio * availableWidth).toInt().coerceIn(0, availableWidth) + (ratio * availableWidth).roundToInt().coerceIn(0, availableWidth) } else { - (ratio * availableWidth).toInt().coerceIn(minLeftWidthPx, availableWidth - minRightWidthPx) + (ratio * availableWidth).roundToInt().coerceIn(minLeftWidthPx, availableWidth - minRightWidthPx) } leftWidth to (availableWidth - leftWidth)