Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -49,6 +51,10 @@ abstract class MyViewPagerFragment<Binding : MyViewPagerFragment.InnerBinding>(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) {
Expand All @@ -63,6 +69,8 @@ abstract class MyViewPagerFragment<Binding : MyViewPagerFragment.InnerBinding>(c
}

innerBinding.fragmentPlaceholder2.underlineText()
setupDragHandle()
restoreSidebarPosition()

when (this) {
is ContactsFragment -> {
Expand Down Expand Up @@ -379,6 +387,96 @@ abstract class MyViewPagerFragment<Binding : MyViewPagerFragment.InnerBinding>(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()
Expand All @@ -391,6 +489,8 @@ abstract class MyViewPagerFragment<Binding : MyViewPagerFragment.InnerBinding>(c
val fragmentWrapper: RelativeLayout
val letterFastscroller: FastScrollerView?
val letterFastscrollerThumb: FastScrollerThumbView?
val letterFastscrollerContainer: ViewGroup?
val letterFastscrollerDragHandle: View?
val fragmentFastscroller: RecyclerViewFastScroller?
}

Expand All @@ -402,6 +502,8 @@ abstract class MyViewPagerFragment<Binding : MyViewPagerFragment.InnerBinding>(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
}

Expand All @@ -413,6 +515,8 @@ abstract class MyViewPagerFragment<Binding : MyViewPagerFragment.InnerBinding>(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
}
}
3 changes: 3 additions & 0 deletions app/src/main/kotlin/org/fossify/contacts/helpers/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/ic_dot.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,12m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" />
</vector>
37 changes: 28 additions & 9 deletions app/src/main/res/layout/fragment_letters_layout.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,41 @@
android:paddingBottom="@dimen/secondary_fab_bottom_margin"
app:layoutManager="org.fossify.commons.views.MyLinearLayoutManager" />

<com.reddit.indicatorfastscroll.FastScrollerView
android:id="@+id/letter_fastscroller"
android:layout_width="32dp"
<LinearLayout
android:id="@+id/letter_fastscroller_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:paddingTop="@dimen/big_margin"
android:paddingBottom="@dimen/big_margin" />
android:gravity="center_horizontal"
android:orientation="vertical">

<com.reddit.indicatorfastscroll.FastScrollerView
android:id="@+id/letter_fastscroller"
android:layout_width="32dp"
android:layout_height="wrap_content"
android:paddingTop="@dimen/normal_margin"
android:paddingBottom="@dimen/normal_margin" />

<ImageView
android:id="@+id/letter_fastscroller_drag_handle"
android:layout_width="32dp"
android:layout_height="32dp"
android:contentDescription="@null"
android:padding="4dp"
android:src="@drawable/ic_dot" />

</LinearLayout>

<com.reddit.indicatorfastscroll.FastScrollerThumbView
android:id="@+id/letter_fastscroller_thumb"
android:layout_width="@dimen/fab_size"
android:layout_height="match_parent"
android:layout_alignTop="@+id/letter_fastscroller"
android:layout_alignBottom="@+id/letter_fastscroller"
android:layout_height="@dimen/fab_size"
android:layout_alignTop="@+id/letter_fastscroller_container"
android:layout_alignBottom="@+id/letter_fastscroller_container"
android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/activity_margin"
android:layout_toStartOf="@+id/letter_fastscroller" />
android:layout_toStartOf="@+id/letter_fastscroller_container"
android:padding="@dimen/small_margin" />

</RelativeLayout>

Expand Down
Loading