Skip to content

Commit

Permalink
Merge pull request #18 from nanihadesuka/selection_actionable
Browse files Browse the repository at this point in the history
Add scrollbar actionable state modes
  • Loading branch information
nanihadesuka authored Jul 14, 2023
2 parents 9d43af6 + 88d5f4d commit 886b6ba
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 58 deletions.
26 changes: 11 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ Compose implementation of the scroll bar. Can drag, scroll smoothly and includes
- Support for LazyColumn's sticky headers
- Support for LazyColumn's reverseLayout
- Optional current position indicator
- Multiple selection states (Disabled, Full or Thumb)
- Multiple selection states (Disabled, Full, Thumb)
- Multiple selection actionable states (Always, WhenVisible)
- Customizable look
- Easy integration with other composables
- UI tests
Expand All @@ -31,8 +32,8 @@ Add it to your app build.gradle

```groovy
dependencies {
implementation 'com.github.nanihadesuka:LazyColumnScrollbar:1.6.3'
}
implementation 'com.github.nanihadesuka:LazyColumnScrollbar:1.7.0'
}
```

# How to use for lazyColumn
Expand All @@ -58,22 +59,13 @@ LazyColumnScrollbar(listState) {
```

indicatorContent example:
```
```kotlin
indicatorContent = { index, isThumbSelected ->
Text(
text = "i: $index",
Modifier
.clip(
RoundedCornerShape(
topStart = 20.dp,
bottomStart = 20.dp,
bottomEnd = 16.dp
)
)
.background(Color.Green)
.padding(8.dp)
.clip(CircleShape)
.background(if (isThumbSelected) Color.Red else Color.Black)
.padding(4.dp)
.background(if (isThumbSelected) Color.Red else Color.Black, CircleShape)
.padding(12.dp)
)
}
Expand All @@ -92,6 +84,8 @@ fun LazyColumnScrollbar(
thumbSelectedColor: Color = Color(0xFF5281CA),
thumbShape: Shape = CircleShape,
selectionMode: ScrollbarSelectionMode = ScrollbarSelectionMode.Thumb,
selectionActionable: ScrollbarSelectionActionable = ScrollbarSelectionActionable.Always,
hideDelayMillis: Int = 400,
enabled: Boolean = true,
indicatorContent: (@Composable (index: Int, isThumbSelected: Boolean) -> Unit)? = null,
content: @Composable () -> Unit
Expand Down Expand Up @@ -135,6 +129,8 @@ fun ColumnScrollbar(
thumbShape: Shape = CircleShape,
enabled: Boolean = true,
selectionMode: ScrollbarSelectionMode = ScrollbarSelectionMode.Thumb,
selectionActionable: ScrollbarSelectionActionable = ScrollbarSelectionActionable.Always,
hideDelayMillis: Int = 400,
indicatorContent: (@Composable (normalizedOffset: Float, isThumbSelected: Boolean) -> Unit)? = null,
content: @Composable () -> Unit
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ package my.nanihadesuka.lazycolumnscrollbar
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
Expand Down Expand Up @@ -49,7 +53,7 @@ fun MainView() {
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyColumnView() {
val listData = (0..30).toList()
val listData = (0..1000).toList()
val listState = rememberLazyListState()

Box(
Expand Down
4 changes: 2 additions & 2 deletions lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ android {
defaultConfig {
minSdk 21
targetSdk 33
versionCode 14
versionName "1.6.3"
versionCode 15
versionName "1.7.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
Expand Down
70 changes: 52 additions & 18 deletions lib/src/main/java/my/nanihadesuka/compose/ColumnScrollbar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

/**
Expand All @@ -44,6 +45,8 @@ fun ColumnScrollbar(
thumbShape: Shape = CircleShape,
enabled: Boolean = true,
selectionMode: ScrollbarSelectionMode = ScrollbarSelectionMode.Thumb,
selectionActionable: ScrollbarSelectionActionable = ScrollbarSelectionActionable.Always,
hideDelayMillis: Int = 400,
indicatorContent: (@Composable (normalizedOffset: Float, isThumbSelected: Boolean) -> Unit)? = null,
content: @Composable () -> Unit
) {
Expand All @@ -62,6 +65,8 @@ fun ColumnScrollbar(
visibleHeightDp = with(LocalDensity.current) { constraints.maxHeight.toDp() },
indicatorContent = indicatorContent,
selectionMode = selectionMode,
selectionActionable = selectionActionable,
hideDelayMillis = hideDelayMillis,
)
}
}
Expand All @@ -87,6 +92,8 @@ fun InternalColumnScrollbar(
thumbSelectedColor: Color = Color(0xFF5281CA),
thumbShape: Shape = CircleShape,
selectionMode: ScrollbarSelectionMode = ScrollbarSelectionMode.Thumb,
selectionActionable: ScrollbarSelectionActionable = ScrollbarSelectionActionable.Always,
hideDelayMillis: Int = 400,
indicatorContent: (@Composable (normalizedOffset: Float, isThumbSelected: Boolean) -> Unit)? = null,
visibleHeightDp: Dp,
) {
Expand Down Expand Up @@ -151,16 +158,33 @@ fun InternalColumnScrollbar(

val isInAction = state.isScrollInProgress || isSelected

val isInActionSelectable = remember { mutableStateOf(isInAction) }
val durationAnimationMillis: Int = 500
LaunchedEffect(isInAction) {
if (isInAction) {
isInActionSelectable.value = true
} else {
delay(timeMillis = durationAnimationMillis.toLong() + hideDelayMillis.toLong())
isInActionSelectable.value = false
}
}

val alpha by animateFloatAsState(
targetValue = if (isInAction) 1f else 0f, animationSpec = tween(
durationMillis = if (isInAction) 75 else 500, delayMillis = if (isInAction) 0 else 500
)
targetValue = if (isInAction) 1f else 0f,
animationSpec = tween(
durationMillis = if (isInAction) 75 else durationAnimationMillis,
delayMillis = if (isInAction) 0 else hideDelayMillis
),
label = "scrollbar alpha value"
)

val displacement by animateFloatAsState(
targetValue = if (isInAction) 0f else 14f, animationSpec = tween(
durationMillis = if (isInAction) 75 else 500, delayMillis = if (isInAction) 0 else 500
)
targetValue = if (isInAction) 0f else 14f,
animationSpec = tween(
durationMillis = if (isInAction) 75 else durationAnimationMillis,
delayMillis = if (isInAction) 0 else hideDelayMillis
),
label = "scrollbar displacement value"
)

BoxWithConstraints(
Expand Down Expand Up @@ -226,21 +250,31 @@ fun InternalColumnScrollbar(
onDragStarted = { offset ->
val newOffset = offset.y / constraints.maxHeight.toFloat()
val currentOffset = normalizedOffsetPosition
when (selectionMode) {
ScrollbarSelectionMode.Full -> {
if (newOffset in currentOffset..(currentOffset + normalizedThumbSizeUpdated))
setDragOffset(currentOffset)
else
setScrollOffset(newOffset)
isSelected = true
}
ScrollbarSelectionMode.Thumb -> {
if (newOffset in currentOffset..(currentOffset + normalizedThumbSizeUpdated)) {
setDragOffset(currentOffset)

val allowedToInteract = when (selectionActionable) {
ScrollbarSelectionActionable.Always -> true
ScrollbarSelectionActionable.WhenVisible -> isInActionSelectable.value
}

if (allowedToInteract) {
when (selectionMode) {
ScrollbarSelectionMode.Full -> {
if (newOffset in currentOffset..(currentOffset + normalizedThumbSizeUpdated))
setDragOffset(currentOffset)
else
setScrollOffset(newOffset)
isSelected = true
}

ScrollbarSelectionMode.Thumb -> {
if (newOffset in currentOffset..(currentOffset + normalizedThumbSizeUpdated)) {
setDragOffset(currentOffset)
isSelected = true
}
}

ScrollbarSelectionMode.Disabled -> Unit
}
ScrollbarSelectionMode.Disabled -> Unit
}
},
onDragStopped = {
Expand Down
70 changes: 50 additions & 20 deletions lib/src/main/java/my/nanihadesuka/compose/LazyColumnScrollbar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand All @@ -34,6 +35,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.floor

Expand All @@ -56,6 +58,8 @@ fun LazyColumnScrollbar(
thumbSelectedColor: Color = Color(0xFF5281CA),
thumbShape: Shape = CircleShape,
selectionMode: ScrollbarSelectionMode = ScrollbarSelectionMode.Thumb,
selectionActionable: ScrollbarSelectionActionable = ScrollbarSelectionActionable.Always,
hideDelayMillis: Int = 400,
enabled: Boolean = true,
indicatorContent: (@Composable (index: Int, isThumbSelected: Boolean) -> Unit)? = null,
content: @Composable () -> Unit
Expand All @@ -71,6 +75,8 @@ fun LazyColumnScrollbar(
thumbMinHeight = thumbMinHeight,
thumbColor = thumbColor,
thumbSelectedColor = thumbSelectedColor,
selectionActionable = selectionActionable,
hideDelayMillis = hideDelayMillis,
thumbShape = thumbShape,
selectionMode = selectionMode,
indicatorContent = indicatorContent,
Expand Down Expand Up @@ -98,6 +104,8 @@ fun InternalLazyColumnScrollbar(
thumbSelectedColor: Color = Color(0xFF5281CA),
thumbShape: Shape = CircleShape,
selectionMode: ScrollbarSelectionMode = ScrollbarSelectionMode.Thumb,
selectionActionable: ScrollbarSelectionActionable = ScrollbarSelectionActionable.Always,
hideDelayMillis: Int = 400,
indicatorContent: (@Composable (index: Int, isThumbSelected: Boolean) -> Unit)? = null,
) {
val firstVisibleItemIndex = remember { derivedStateOf { listState.firstVisibleItemIndex } }
Expand Down Expand Up @@ -140,7 +148,8 @@ fun InternalLazyColumnScrollbar(
return@let 0f

val firstItem = realFirstVisibleItem ?: return@let 0f
val firstPartial = firstItem.fractionHiddenTop(listState.firstVisibleItemScrollOffset)
val firstPartial =
firstItem.fractionHiddenTop(listState.firstVisibleItemScrollOffset)
val lastPartial =
1f - it.visibleItemsInfo.last().fractionVisibleBottom(it.viewportEndOffset)

Expand Down Expand Up @@ -219,20 +228,33 @@ fun InternalLazyColumnScrollbar(

val isInAction = listState.isScrollInProgress || isSelected

val isInActionSelectable = remember { mutableStateOf(isInAction) }
val durationAnimationMillis: Int = 500
LaunchedEffect(isInAction) {
if (isInAction) {
isInActionSelectable.value = true
} else {
delay(timeMillis = durationAnimationMillis.toLong() + hideDelayMillis.toLong())
isInActionSelectable.value = false
}
}

val alpha by animateFloatAsState(
targetValue = if (isInAction) 1f else 0f,
animationSpec = tween(
durationMillis = if (isInAction) 75 else 500,
delayMillis = if (isInAction) 0 else 500
)
durationMillis = if (isInAction) 75 else durationAnimationMillis,
delayMillis = if (isInAction) 0 else hideDelayMillis
),
label = "scrollbar alpha value"
)

val displacement by animateFloatAsState(
targetValue = if (isInAction) 0f else 14f,
animationSpec = tween(
durationMillis = if (isInAction) 75 else 500,
delayMillis = if (isInAction) 0 else 500
)
durationMillis = if (isInAction) 75 else durationAnimationMillis,
delayMillis = if (isInAction) 0 else hideDelayMillis
),
label = "scrollbar displacement value"
)

BoxWithConstraints(
Expand Down Expand Up @@ -309,23 +331,31 @@ fun InternalLazyColumnScrollbar(
reverseLayout -> 1f - normalizedOffsetPosition - normalizedThumbSize
else -> normalizedOffsetPosition
}
when (selectionMode) {
ScrollbarSelectionMode.Full -> {
if (newOffset in currentOffset..(currentOffset + normalizedThumbSize))
setDragOffset(currentOffset)
else
setScrollOffset(newOffset)
isSelected = true
}

ScrollbarSelectionMode.Thumb -> {
if (newOffset in currentOffset..(currentOffset + normalizedThumbSize)) {
setDragOffset(currentOffset)
val allowedToInteract = when (selectionActionable) {
ScrollbarSelectionActionable.Always -> true
ScrollbarSelectionActionable.WhenVisible -> isInActionSelectable.value
}

if (allowedToInteract) {
when (selectionMode) {
ScrollbarSelectionMode.Full -> {
if (newOffset in currentOffset..(currentOffset + normalizedThumbSize))
setDragOffset(currentOffset)
else
setScrollOffset(newOffset)
isSelected = true
}
}

ScrollbarSelectionMode.Disabled -> Unit
ScrollbarSelectionMode.Thumb -> {
if (newOffset in currentOffset..(currentOffset + normalizedThumbSize)) {
setDragOffset(currentOffset)
isSelected = true
}
}

ScrollbarSelectionMode.Disabled -> Unit
}
}
},
onDragStopped = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package my.nanihadesuka.compose

/**
* Scrollbar selection modes.
*/
enum class ScrollbarSelectionActionable {
/**
* Can select scrollbar always (when visible or hidden)
*/
Always,

/**
* Can select scrollbar only when visible
*/
WhenVisible,
}
Loading

0 comments on commit 886b6ba

Please sign in to comment.