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..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,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("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/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..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 @@ -17,13 +17,14 @@ */ 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 import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentContainerView @@ -52,12 +53,19 @@ 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() { 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) } + 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 // between the ThreadList's RecyclerView and its Adapter as it throws an NPE. @@ -70,6 +78,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 +91,7 @@ abstract class TwoPaneFragment : Fragment() { observeCurrentFolder() observeThreadUid() observeThreadNavigation() + observeDragSeparator() } override fun onConfigurationChanged(newConfig: Configuration) { @@ -136,6 +146,91 @@ abstract class TwoPaneFragment : Fragment() { } } + override fun onStop() { + twoPaneViewModel.saveLeftPaneRatio() + super.onStop() + } + + private fun observeDragSeparator() { + 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 = leftPane.width + updateSeparatorAppearance(separatorView, lineSeparator, true, widthSelected) + true + } + + MotionEvent.ACTION_MOVE -> { + handleDragMove(event.rawX, leftPane, rightPane, parentGroup) + true + } + + MotionEvent.ACTION_UP -> { + updateSeparatorAppearance(separatorView, lineSeparator, false, widthNormal) + separatorView.performClick() + true + } + + MotionEvent.ACTION_CANCEL -> { + 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 availableWidth = parentWidth - separatorWidthPx + + val maxLeftWidth = availableWidth - 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() + + if (availableWidth > 0) { + twoPaneViewModel.leftPaneRatio = newLeftWidth.toFloat() / availableWidth + } + } + fun handleOnBackPressed() { when { isOnlyRightShown() -> { @@ -169,37 +264,41 @@ 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()?.isVisible = isTabletOrFoldable() && rightWidth != 0 } - 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 + + val availableWidth = (widthPixels - separatorWidthPx).coerceAtLeast(0) + val leftWidth = if (availableWidth < minLeftWidthPx + minRightWidthPx) { + (ratio * availableWidth).roundToInt().coerceIn(0, availableWidth) + } else { + (ratio * availableWidth).roundToInt().coerceIn(minLeftWidthPx, availableWidth - minRightWidthPx) + } + leftWidth to (availableWidth - 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..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 @@ -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 @@ -44,13 +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() { - private val ioCoroutineContext = viewModelScope.coroutineContext(ioDispatcher) val currentThreadUid: LiveData = state.getLiveData(CURRENT_THREAD_UID_KEY) + var leftPaneRatio: Float = localSettings.leftPaneRatio inline val isThreadOpen get() = currentThreadUid.value != null val rightPaneFolderName = MutableLiveData() var previousFolderId: String? = null @@ -62,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 } @@ -70,6 +77,10 @@ class TwoPaneViewModel @Inject constructor( state[CURRENT_THREAD_UID_KEY] = null } + fun saveLeftPaneRatio() { + localSettings.leftPaneRatio = leftPaneRatio + } + fun openDraft(thread: Thread) = viewModelScope.launch(ioCoroutineContext) { navigateToSelectedDraft(thread.messages.single()) } 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/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 new file mode 100644 index 00000000000..918e4da7c9f --- /dev/null +++ b/app/src/main/res/drawable/item_vertical_drag_handle.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 54c3529ff43..80070caa182 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -206,6 +206,32 @@ + + + + + + + + + + + + + + @@ -33,20 +34,22 @@ diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index bc81ba2244d..977faea0872 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -18,8 +18,16 @@ - 0.4 - 0.6 + 250dp + 300dp + 1dp + 3dp + 8dp + 25dp + 48dp + -24dp + 10dp + 110dp