diff --git a/CHANGELOG.md b/CHANGELOG.md index a9a92d320..76622932f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Draggable sidebar ## [1.6.0] - 2026-01-30 ### Added diff --git a/app/src/main/kotlin/org/fossify/contacts/fragments/MyViewPagerFragment.kt b/app/src/main/kotlin/org/fossify/contacts/fragments/MyViewPagerFragment.kt index bef154ebc..364f2c76c 100644 --- a/app/src/main/kotlin/org/fossify/contacts/fragments/MyViewPagerFragment.kt +++ b/app/src/main/kotlin/org/fossify/contacts/fragments/MyViewPagerFragment.kt @@ -3,6 +3,8 @@ package org.fossify.contacts.fragments import android.content.Context import android.content.Intent import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View import android.view.ViewGroup import android.widget.RelativeLayout import androidx.coordinatorlayout.widget.CoordinatorLayout @@ -49,6 +51,10 @@ abstract class MyViewPagerFragment(c var skipHashComparing = false var forceListRedraw = false + private var initialY = 0f + private var dY = 0f + private var isPositionRestored = false + fun setupFragment(activity: SimpleActivity) { config = activity.config if (this.activity == null) { @@ -63,6 +69,8 @@ abstract class MyViewPagerFragment(c } innerBinding.fragmentPlaceholder2.underlineText() + setupDragHandle() + restoreSidebarPosition() when (this) { is ContactsFragment -> { @@ -379,6 +387,96 @@ abstract class MyViewPagerFragment(c innerBinding.fragmentList.beVisibleIf(hasItemsToShow) } + private fun setupDragHandle() { + val container = innerBinding.letterFastscrollerContainer ?: return + val dragHandle = innerBinding.letterFastscrollerDragHandle ?: return + val parent = container.parent as? View ?: return + + dragHandle.setOnTouchListener { v, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + isPositionRestored = true + initialY = container.y + dY = event.rawY - initialY + v.performClick() + } + + MotionEvent.ACTION_MOVE -> { + isPositionRestored = true + var newY = event.rawY - dY + val fab = innerBinding.fragmentFab + val parentHeight = parent.height.toFloat() + val fabY = if (fab.isVisible()) fab.y else parentHeight + val maxY = (fabY - container.height).coerceAtLeast(0f) + newY = newY.coerceIn(0f, maxY) + container.y = newY + } + + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + if (isPositionRestored && container.y != initialY) { + config.sidebarYOffset = container.y + container.height + } + } + } + true + } + + /* + * To ensure the sidebar expands upwards when new characters are added (instead of pushing + * the dot down), we listen for layout changes and adjust the Y position if the height changes. + */ + container.addOnLayoutChangeListener { _, _, top, _, bottom, _, oldTop, _, oldBottom -> + handleOnLayoutChange(container, parent, top, bottom, oldTop, oldBottom) + } + } + + private fun handleOnLayoutChange( + container: ViewGroup, + parent: View, + top: Int, + bottom: Int, + oldTop: Int, + oldBottom: Int + ) { + if (!isPositionRestored) return + val oldHeight = oldBottom - oldTop + val newHeight = bottom - top + if (oldHeight > 0 && newHeight != oldHeight) { + val heightDiff = newHeight - oldHeight + val fab = innerBinding.fragmentFab + val parentHeight = parent.height.toFloat() + val fabY = if (fab.isVisible()) fab.y else parentHeight + val maxY = (fabY - newHeight).coerceAtLeast(0f) + + val newY = (container.y - heightDiff).coerceIn(0f, maxY) + if (container.y != newY) { + container.y = newY + } + config.sidebarYOffset = container.y + newHeight + } + } + + private fun restoreSidebarPosition() { + val container = innerBinding.letterFastscrollerContainer ?: return + val parent = container.parent as? View ?: return + container.onGlobalLayout { + val fab = innerBinding.fragmentFab + val parentHeight = parent.height.toFloat() + val fabY = if (fab.isVisible()) fab.y else parentHeight + val maxY = (fabY - container.height).coerceAtLeast(0f) + + val savedBottomY = config.sidebarYOffset + val targetY = if (savedBottomY == 0f) { + 0f + } else { + (savedBottomY - container.height).coerceIn(0f, maxY) + } + + container.y = targetY + isPositionRestored = true + } + } + abstract fun fabClicked() abstract fun placeholderClicked() @@ -391,6 +489,8 @@ abstract class MyViewPagerFragment(c val fragmentWrapper: RelativeLayout val letterFastscroller: FastScrollerView? val letterFastscrollerThumb: FastScrollerThumbView? + val letterFastscrollerContainer: ViewGroup? + val letterFastscrollerDragHandle: View? val fragmentFastscroller: RecyclerViewFastScroller? } @@ -402,6 +502,8 @@ abstract class MyViewPagerFragment(c override val fragmentWrapper: RelativeLayout = binding.fragmentWrapper override val letterFastscroller: FastScrollerView = binding.letterFastscroller override val letterFastscrollerThumb: FastScrollerThumbView = binding.letterFastscrollerThumb + override val letterFastscrollerContainer: ViewGroup = binding.letterFastscrollerContainer + override val letterFastscrollerDragHandle: View = binding.letterFastscrollerDragHandle override val fragmentFastscroller: RecyclerViewFastScroller? = null } @@ -413,6 +515,8 @@ abstract class MyViewPagerFragment(c override val fragmentWrapper: RelativeLayout = binding.fragmentWrapper override val letterFastscroller: FastScrollerView? = null override val letterFastscrollerThumb: FastScrollerThumbView? = null + override val letterFastscrollerContainer: ViewGroup? = null + override val letterFastscrollerDragHandle: View? = null override val fragmentFastscroller: RecyclerViewFastScroller = binding.fragmentFastscroller } } diff --git a/app/src/main/kotlin/org/fossify/contacts/helpers/Config.kt b/app/src/main/kotlin/org/fossify/contacts/helpers/Config.kt index 5be4a497f..2526e3964 100644 --- a/app/src/main/kotlin/org/fossify/contacts/helpers/Config.kt +++ b/app/src/main/kotlin/org/fossify/contacts/helpers/Config.kt @@ -18,4 +18,7 @@ class Config(context: Context) : BaseConfig(context) { set(autoBackupContactSources) = prefs.edit().remove(AUTO_BACKUP_CONTACT_SOURCES).putStringSet(AUTO_BACKUP_CONTACT_SOURCES, autoBackupContactSources) .apply() + var sidebarYOffset: Float + get() = prefs.getFloat(SIDEBAR_Y_OFFSET, 0f) + set(sidebarYOffset) = prefs.edit().putFloat(SIDEBAR_Y_OFFSET, sidebarYOffset).apply() } diff --git a/app/src/main/kotlin/org/fossify/contacts/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/contacts/helpers/Constants.kt index c6e8c5a45..75aa3212b 100644 --- a/app/src/main/kotlin/org/fossify/contacts/helpers/Constants.kt +++ b/app/src/main/kotlin/org/fossify/contacts/helpers/Constants.kt @@ -17,6 +17,7 @@ const val AUTOMATIC_BACKUP_REQUEST_CODE = 10001 const val AUTO_BACKUP_INTERVAL_IN_DAYS = 1 const val AUTO_BACKUP_CONTACT_SOURCES = "auto_backup_contact_sources" +const val SIDEBAR_Y_OFFSET = "sidebar_y_offset" // extras used at third party intents const val KEY_NAME = "name" diff --git a/app/src/main/res/drawable/ic_dot.xml b/app/src/main/res/drawable/ic_dot.xml new file mode 100644 index 000000000..ce4bafe46 --- /dev/null +++ b/app/src/main/res/drawable/ic_dot.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/layout/fragment_letters_layout.xml b/app/src/main/res/layout/fragment_letters_layout.xml index 56b20ae14..7105200e1 100644 --- a/app/src/main/res/layout/fragment_letters_layout.xml +++ b/app/src/main/res/layout/fragment_letters_layout.xml @@ -45,22 +45,41 @@ android:paddingBottom="@dimen/secondary_fab_bottom_margin" app:layoutManager="org.fossify.commons.views.MyLinearLayoutManager" /> - + android:gravity="center_horizontal" + android:orientation="vertical"> + + + + + + + android:layout_toStartOf="@+id/letter_fastscroller_container" + android:padding="@dimen/small_margin" />