Skip to content

Commit

Permalink
Merge pull request #170 from rubensousa/text_scrolling
Browse files Browse the repository at this point in the history
Add support for scrollbars & discrete scrolling
  • Loading branch information
rubensousa committed Nov 25, 2023
2 parents 23e636b + aa54100 commit 4221bfc
Show file tree
Hide file tree
Showing 21 changed files with 684 additions and 13 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Pull requests

env:
GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false"
GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g"

on:
pull_request:
Expand Down
11 changes: 11 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## Version 1.2.0

### 1.2.0-alpha01

2023-11-25

#### New Features

- Added support for scrollbars
- Added `DpadScroller` for scrolling without any alignment. Typical use case is for long text displays (terms & conditions and consent pages).

## Version 1.1.0

### 1.1.0
Expand Down
19 changes: 19 additions & 0 deletions dpadrecyclerview/api/dpadrecyclerview.api
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,19 @@ public abstract interface class com/rubensousa/dpadrecyclerview/DpadRecyclerView
public abstract fun configSmoothScrollByInterpolator (II)Landroid/view/animation/Interpolator;
}

public final class com/rubensousa/dpadrecyclerview/DpadScroller {
public fun <init> ()V
public fun <init> (Lcom/rubensousa/dpadrecyclerview/DpadScroller$ScrollDistanceCalculator;)V
public synthetic fun <init> (Lcom/rubensousa/dpadrecyclerview/DpadScroller$ScrollDistanceCalculator;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun attach (Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView;)V
public final fun detach ()V
public final fun setSmoothScrollEnabled (Z)V
}

public abstract interface class com/rubensousa/dpadrecyclerview/DpadScroller$ScrollDistanceCalculator {
public abstract fun calculateScrollDistance (Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView;Landroid/view/KeyEvent;)I
}

public abstract class com/rubensousa/dpadrecyclerview/DpadSpanSizeLookup {
public fun <init> ()V
public static final fun findFirstKeyLessThan$dpadrecyclerview_release (Landroid/util/SparseIntArray;I)I
Expand Down Expand Up @@ -378,6 +391,12 @@ public final class com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutMana
public final fun clearOnViewHolderSelectedListeners ()V
public fun collectAdjacentPrefetchPositions (IILandroidx/recyclerview/widget/RecyclerView$State;Landroidx/recyclerview/widget/RecyclerView$LayoutManager$LayoutPrefetchRegistry;)V
public fun collectInitialPrefetchPositions (ILandroidx/recyclerview/widget/RecyclerView$LayoutManager$LayoutPrefetchRegistry;)V
public fun computeHorizontalScrollExtent (Landroidx/recyclerview/widget/RecyclerView$State;)I
public fun computeHorizontalScrollOffset (Landroidx/recyclerview/widget/RecyclerView$State;)I
public fun computeHorizontalScrollRange (Landroidx/recyclerview/widget/RecyclerView$State;)I
public fun computeVerticalScrollExtent (Landroidx/recyclerview/widget/RecyclerView$State;)I
public fun computeVerticalScrollOffset (Landroidx/recyclerview/widget/RecyclerView$State;)I
public fun computeVerticalScrollRange (Landroidx/recyclerview/widget/RecyclerView$State;)I
public final fun findFirstCompletelyVisibleItemPosition ()I
public final fun findFirstVisibleItemPosition ()I
public final fun findLastCompletelyVisibleItemPosition ()I
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* 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
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView

/**
* A helper class that allows scrolling a [DpadRecyclerView] based on specific scroll distances,
* ignoring the default alignment behavior.
*
* A typical use case for this class is a terms & conditions page,
* where a large amount of text is displayed, which the user isn't expected to interact with
*/
class DpadScroller(
private val calculator: ScrollDistanceCalculator = DefaultScrollDistanceCalculator(),
) {

private var recyclerView: DpadRecyclerView? = null
private val keyListener = KeyListener()
private var smoothScrollEnabled = true

/**
* Attaches this [DpadScroller] to a new [DpadRecyclerView] to start observing key events.
* If you no longer need this behavior, call [detach]
*
* @param recyclerView The RecyclerView that will be scrolled discretely
*/
fun attach(recyclerView: DpadRecyclerView) {
detach()
this.recyclerView = recyclerView
recyclerView.descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS
recyclerView.setOnKeyInterceptListener(keyListener)
}

/**
* Stops observing key events to scroll the current attached [DpadRecyclerView], if any exists
*/
fun detach() {
recyclerView?.descendantFocusability = ViewGroup.FOCUS_AFTER_DESCENDANTS
recyclerView?.setOnKeyInterceptListener(null)
recyclerView = null
}

/**
* Enables or disables smooth scrolling on key events
*/
fun setSmoothScrollEnabled(enabled: Boolean) {
smoothScrollEnabled = enabled
}

private inner class KeyListener : DpadRecyclerView.OnKeyInterceptListener {

override fun onInterceptKeyEvent(event: KeyEvent): Boolean {
val currentRecyclerView = recyclerView ?: return false
when (event.action) {
KeyEvent.ACTION_DOWN -> {
return if (currentRecyclerView.getOrientation() == RecyclerView.VERTICAL) {
scrollVertically(currentRecyclerView, event)
} else {
scrollHorizontally(currentRecyclerView, event)
}
}
}
return false
}

private fun scrollVertically(recyclerView: DpadRecyclerView, event: KeyEvent): Boolean {
val scrollDistance = calculator.calculateScrollDistance(recyclerView, event)
when (event.keyCode) {
KeyEvent.KEYCODE_DPAD_DOWN -> {
if (smoothScrollEnabled) {
recyclerView.smoothScrollBy(0, scrollDistance)
} else {
recyclerView.scrollBy(0, scrollDistance)
}
return true
}

KeyEvent.KEYCODE_DPAD_UP -> {
if (smoothScrollEnabled) {
recyclerView.smoothScrollBy(0, -scrollDistance)
} else {
recyclerView.scrollBy(0, -scrollDistance)
}
return true
}
}
return false
}

private fun scrollHorizontally(recyclerView: DpadRecyclerView, event: KeyEvent): Boolean {
val scrollDistance = calculator.calculateScrollDistance(recyclerView, event)
when (event.keyCode) {
KeyEvent.KEYCODE_DPAD_RIGHT -> {
if (smoothScrollEnabled) {
recyclerView.smoothScrollBy(0, scrollDistance)
} else {
recyclerView.scrollBy(0, scrollDistance)
}
return true
}

KeyEvent.KEYCODE_DPAD_LEFT -> {
if (smoothScrollEnabled) {
recyclerView.smoothScrollBy(0, -scrollDistance)
} else {
recyclerView.scrollBy(0, -scrollDistance)
}
return true
}
}
return false
}

}


interface ScrollDistanceCalculator {
/**
* @return the number of pixels we should scroll for this [event]
*/
fun calculateScrollDistance(recyclerView: DpadRecyclerView, event: KeyEvent): Int
}

private class DefaultScrollDistanceCalculator : ScrollDistanceCalculator {
override fun calculateScrollDistance(
recyclerView: DpadRecyclerView,
event: KeyEvent
): Int {
return if (recyclerView.getOrientation() == RecyclerView.VERTICAL) {
recyclerView.height / 4
} else {
recyclerView.width / 4
}
}
}

}
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

0 comments on commit 4221bfc

Please sign in to comment.