Skip to content

Commit

Permalink
Add support for scrollbars
Browse files Browse the repository at this point in the history
  • Loading branch information
rubensousa committed Nov 25, 2023
1 parent 702479e commit 1eaa0c0
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
/*
* Copyright 2023 Rúben Sousa
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.rubensousa.dpadrecyclerview

import android.view.KeyEvent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import android.os.Parcelable
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.recyclerview.widget.RecyclerView
import com.rubensousa.dpadrecyclerview.ChildAlignment
Expand All @@ -41,6 +40,7 @@ import com.rubensousa.dpadrecyclerview.layoutmanager.focus.SpanFocusFinder
import com.rubensousa.dpadrecyclerview.layoutmanager.layout.LayoutInfo
import com.rubensousa.dpadrecyclerview.layoutmanager.layout.LayoutPrefetchCollector
import com.rubensousa.dpadrecyclerview.layoutmanager.layout.PivotLayout
import com.rubensousa.dpadrecyclerview.layoutmanager.scroll.DpadScrollbarHelper
import com.rubensousa.dpadrecyclerview.layoutmanager.scroll.LayoutScroller

/**
Expand Down Expand Up @@ -169,6 +169,73 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager()
state: RecyclerView.State
): Int = pivotLayout.scrollVerticallyBy(dy, recycler, state)

override fun computeHorizontalScrollOffset(state: RecyclerView.State): Int {
return computeScrollOffset(state)
}

override fun computeVerticalScrollOffset(state: RecyclerView.State): Int {
return computeScrollOffset(state)
}

override fun computeHorizontalScrollExtent(state: RecyclerView.State): Int {
return computeScrollExtent(state)
}

override fun computeVerticalScrollExtent(state: RecyclerView.State): Int {
return computeScrollExtent(state)
}

override fun computeHorizontalScrollRange(state: RecyclerView.State): Int {
return computeScrollRange(state)
}

override fun computeVerticalScrollRange(state: RecyclerView.State): Int {
return computeScrollRange(state)
}

private fun computeScrollOffset(state: RecyclerView.State): Int {
if (childCount == 0) {
return 0
}
return DpadScrollbarHelper.computeScrollOffset(
state = state,
orientationHelper = layoutInfo.orientationHelper,
startChild = layoutInfo.findFirstVisibleChild(),
endChild = layoutInfo.findLastVisibleChild(),
lm = this,
smoothScrollbarEnabled = true,
reverseLayout = configuration.reverseLayout
)
}

private fun computeScrollExtent(state: RecyclerView.State): Int {
if (childCount == 0) {
return 0
}
return DpadScrollbarHelper.computeScrollExtent(
state = state,
orientationHelper = layoutInfo.orientationHelper,
startChild = layoutInfo.findFirstVisibleChild(),
endChild = layoutInfo.findLastVisibleChild(),
lm = this,
smoothScrollbarEnabled = true,
)
}

private fun computeScrollRange(state: RecyclerView.State): Int {
if (childCount == 0) {
return 0
}
return DpadScrollbarHelper.computeScrollRange(
state = state,
orientationHelper = layoutInfo.orientationHelper,
startChild = layoutInfo.findFirstVisibleChild(),
endChild = layoutInfo.findLastVisibleChild(),
lm = this,
smoothScrollbarEnabled = true,
)
}

override fun scrollToPosition(position: Int) {
scroller.scrollToPosition(position)
}
Expand Down Expand Up @@ -571,9 +638,4 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager()
}
}

@VisibleForTesting
internal fun getFocusFinderSpanCount(): Int {
return spanFocusFinder.spanCount
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,22 @@ internal class LayoutInfo(
)
}

fun findFirstVisibleChild(): View? {
val childPosition = findFirstVisiblePosition()
if (childPosition == RecyclerView.NO_POSITION) {
return null
}
return layout.findViewByPosition(childPosition)
}

fun findLastVisibleChild(): View? {
val childPosition = findLastVisiblePosition()
if (childPosition == RecyclerView.NO_POSITION) {
return null
}
return layout.findViewByPosition(childPosition)
}

/**
* @param startIndex index at which the search should start
* @param endIndex index at which the search should stop (not inclusive)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/*
* Copyright 2023 Rúben Sousa
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.rubensousa.dpadrecyclerview.layoutmanager.scroll

import android.view.View
import androidx.recyclerview.widget.OrientationHelper
import androidx.recyclerview.widget.RecyclerView

/**
* A helper class to do scroll offset calculations.
* Adapted from ScrollbarHelper of androidx.recyclerview
*/
internal object DpadScrollbarHelper {

/**
* @param startChild View closest to start of the list. (top or left)
* @param endChild View closest to end of the list (bottom or right)
*/
fun computeScrollOffset(
state: RecyclerView.State,
orientationHelper: OrientationHelper,
startChild: View?,
endChild: View?,
lm: RecyclerView.LayoutManager,
smoothScrollbarEnabled: Boolean,
reverseLayout: Boolean
): Int {
if (lm.childCount == 0 || state.itemCount == 0 || startChild == null || endChild == null) {
return 0
}
val minPosition = Math.min(lm.getPosition(startChild), lm.getPosition(endChild))
val maxPosition = Math.max(lm.getPosition(startChild), lm.getPosition(endChild))
val itemsBefore = if (reverseLayout) {
Math.max(0, state.itemCount - maxPosition - 1)
} else {
Math.max(0, minPosition)
}
if (!smoothScrollbarEnabled) {
return itemsBefore
}
val laidOutArea = Math.abs(
orientationHelper.getDecoratedEnd(endChild)
- orientationHelper.getDecoratedStart(startChild)
)
val itemRange = Math.abs((lm.getPosition(startChild) - lm.getPosition(endChild))) + 1
val avgSizePerRow = laidOutArea.toFloat() / itemRange
return Math.round(
itemsBefore * avgSizePerRow + ((orientationHelper.startAfterPadding
- orientationHelper.getDecoratedStart(startChild)))
)
}

/**
* @param startChild View closest to start of the list. (top or left)
* @param endChild View closest to end of the list (bottom or right)
*/
fun computeScrollExtent(
state: RecyclerView.State,
orientationHelper: OrientationHelper,
startChild: View?,
endChild: View?,
lm: RecyclerView.LayoutManager,
smoothScrollbarEnabled: Boolean
): Int {
if ((lm.childCount == 0) || (state.itemCount == 0) || (startChild == null
) || (endChild == null)
) {
return 0
}
if (!smoothScrollbarEnabled) {
return Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1
}
val extend = (orientationHelper.getDecoratedEnd(endChild)
- orientationHelper.getDecoratedStart(startChild))
return Math.min(orientationHelper.totalSpace, extend)
}

/**
* @param startChild View closest to start of the list. (top or left)
* @param endChild View closest to end of the list (bottom or right)
*/
fun computeScrollRange(
state: RecyclerView.State,
orientationHelper: OrientationHelper,
startChild: View?,
endChild: View?,
lm: RecyclerView.LayoutManager,
smoothScrollbarEnabled: Boolean
): Int {
if ((lm.childCount == 0)
|| (state.itemCount == 0)
|| (startChild == null)
|| (endChild == null)
) {
return 0
}
if (!smoothScrollbarEnabled) {
return state.itemCount
}
// smooth scrollbar enabled. try to estimate better.
val laidOutArea = (orientationHelper.getDecoratedEnd(endChild)
- orientationHelper.getDecoratedStart(startChild))
val laidOutRange = (Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1)
// estimate a size for full list.
return (laidOutArea.toFloat() / laidOutRange * state.itemCount).toInt()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class TextScrollingFragment : Fragment(R.layout.screen_text_scrolling) {
val adapter = TextAdapter()
val text = view.resources.getString(R.string.placeholder_text)
val list = mutableListOf<String>()
repeat(50) {
repeat(20) {
list.add(text)
}
adapter.submitList(list)
Expand Down
8 changes: 8 additions & 0 deletions sample/src/main/res/drawable/scrollbar_thumb.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<corners android:radius="4dp" />
<solid android:color="@color/white" />

</shape>
7 changes: 7 additions & 0 deletions sample/src/main/res/drawable/scrollbar_track.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<solid android:color="@color/black" />

</shape>
6 changes: 6 additions & 0 deletions sample/src/main/res/layout/screen_text_scrolling.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@
android:layout_height="match_parent"
android:layout_marginStart="24dp"
android:background="@color/list_text_background"
android:fadeScrollbars="false"
android:scrollbarAlwaysDrawVerticalTrack="true"
android:scrollbarSize="6dp"
android:scrollbarThumbVertical="@drawable/scrollbar_thumb"
android:scrollbarTrackVertical="@drawable/scrollbar_track"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/guideline"
app:layout_constraintStart_toEndOf="@id/backButton"
Expand Down

0 comments on commit 1eaa0c0

Please sign in to comment.