diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 86d9c8c..decde6c 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -53,11 +53,11 @@ android {
}
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
+ sourceCompatibility = JavaVersion.VERSION_18
+ targetCompatibility = JavaVersion.VERSION_18
}
kotlinOptions {
- jvmTarget = "1.8"
+ jvmTarget = "18"
}
buildFeatures {
viewBinding = true
@@ -89,10 +89,8 @@ dependencies {
implementation(libs.androidx.room.ktx)
annotationProcessor(libs.androidx.room.compiler)
ksp(libs.androidx.room.compiler)
- // Room Backup
- // implementation(libs.room.backup)
- // Fast Scroll
- implementation(libs.fast.scroll)
+ // FastScroll
+ implementation(project(":fastscroll"))
// Test
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
diff --git a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoFragment.kt b/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoFragment.kt
index f0b3bae..051252e 100644
--- a/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoFragment.kt
+++ b/app/src/main/kotlin/cn/super12138/todo/views/fragments/ToDoFragment.kt
@@ -9,7 +9,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
-import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
diff --git a/build.gradle.kts b/build.gradle.kts
index cfa4f5d..13fdfe0 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -3,4 +3,5 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.ksp) apply false
+ alias(libs.plugins.android.library) apply false
}
\ No newline at end of file
diff --git a/fastscroll/.gitignore b/fastscroll/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/fastscroll/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/fastscroll/build.gradle.kts b/fastscroll/build.gradle.kts
new file mode 100644
index 0000000..aad3a3f
--- /dev/null
+++ b/fastscroll/build.gradle.kts
@@ -0,0 +1,42 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+}
+
+android {
+ namespace = "me.zhanghai.android.fastscroll"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 21
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_18
+ targetCompatibility = JavaVersion.VERSION_18
+ }
+ kotlinOptions {
+ jvmTarget = "18"
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.recyclerview)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+}
\ No newline at end of file
diff --git a/fastscroll/consumer-rules.pro b/fastscroll/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/fastscroll/proguard-rules.pro b/fastscroll/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/fastscroll/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/fastscroll/src/main/AndroidManifest.xml b/fastscroll/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..dacfad7
--- /dev/null
+++ b/fastscroll/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/AutoMirrorDrawable.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/AutoMirrorDrawable.java
new file mode 100644
index 0000000..7ff5de3
--- /dev/null
+++ b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/AutoMirrorDrawable.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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
+ *
+ * https://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 me.zhanghai.android.fastscroll;
+
+import android.annotation.SuppressLint;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.graphics.drawable.DrawableWrapperCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+
+@SuppressLint("RestrictedApi")
+class AutoMirrorDrawable extends DrawableWrapperCompat {
+
+ public AutoMirrorDrawable(@NonNull Drawable drawable) {
+ super(drawable);
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ if (needMirroring()) {
+ float centerX = getBounds().exactCenterX();
+ canvas.scale(-1, 1, centerX, 0);
+ super.draw(canvas);
+ canvas.scale(-1, 1, centerX, 0);
+ } else {
+ super.draw(canvas);
+ }
+ }
+
+ @Override
+ public boolean onLayoutDirectionChanged(int layoutDirection) {
+ super.onLayoutDirectionChanged(layoutDirection);
+ return true;
+ }
+
+ @Override
+ public boolean isAutoMirrored() {
+ return true;
+ }
+
+ private boolean needMirroring() {
+ return DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL;
+ }
+
+ @Override
+ public boolean getPadding(@NonNull Rect padding) {
+ boolean hasPadding = super.getPadding(padding);
+ if (needMirroring()) {
+ int paddingStart = padding.left;
+ int paddingEnd = padding.right;
+ padding.left = paddingEnd;
+ padding.right = paddingStart;
+ }
+ return hasPadding;
+ }
+}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/DefaultAnimationHelper.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/DefaultAnimationHelper.java
new file mode 100644
index 0000000..2c953b3
--- /dev/null
+++ b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/DefaultAnimationHelper.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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
+ *
+ * https://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 me.zhanghai.android.fastscroll;
+
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import androidx.annotation.NonNull;
+import androidx.interpolator.view.animation.FastOutLinearInInterpolator;
+import androidx.interpolator.view.animation.LinearOutSlowInInterpolator;
+
+public class DefaultAnimationHelper implements FastScroller.AnimationHelper {
+
+ private static final int SHOW_DURATION_MILLIS = 150;
+ private static final int HIDE_DURATION_MILLIS = 200;
+ private static final Interpolator SHOW_SCROLLBAR_INTERPOLATOR =
+ new LinearOutSlowInInterpolator();
+ private static final Interpolator HIDE_SCROLLBAR_INTERPOLATOR =
+ new FastOutLinearInInterpolator();
+ private static final int AUTO_HIDE_SCROLLBAR_DELAY_MILLIS = 1500;
+
+ @NonNull
+ private final View mView;
+
+ private boolean mScrollbarAutoHideEnabled = true;
+
+ private boolean mShowingScrollbar = true;
+ private boolean mShowingPopup;
+
+ public DefaultAnimationHelper(@NonNull View view) {
+ mView = view;
+ }
+
+ @Override
+ public void showScrollbar(@NonNull View trackView, @NonNull View thumbView) {
+
+ if (mShowingScrollbar) {
+ return;
+ }
+ mShowingScrollbar = true;
+
+ trackView.animate()
+ .alpha(1)
+ .translationX(0)
+ .setDuration(SHOW_DURATION_MILLIS)
+ .setInterpolator(SHOW_SCROLLBAR_INTERPOLATOR)
+ .start();
+ thumbView.animate()
+ .alpha(1)
+ .translationX(0)
+ .setDuration(SHOW_DURATION_MILLIS)
+ .setInterpolator(SHOW_SCROLLBAR_INTERPOLATOR)
+ .start();
+ }
+
+ @Override
+ public void hideScrollbar(@NonNull View trackView, @NonNull View thumbView) {
+
+ if (!mShowingScrollbar) {
+ return;
+ }
+ mShowingScrollbar = false;
+
+ boolean isLayoutRtl = mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ int width = Math.max(trackView.getWidth(), thumbView.getWidth());
+ float translationX;
+ if (isLayoutRtl) {
+ translationX = trackView.getLeft() == 0 ? -width : 0;
+ } else {
+ translationX = trackView.getRight() == mView.getWidth() ? width : 0;
+ }
+ trackView.animate()
+ .alpha(0)
+ .translationX(translationX)
+ .setDuration(HIDE_DURATION_MILLIS)
+ .setInterpolator(HIDE_SCROLLBAR_INTERPOLATOR)
+ .start();
+ thumbView.animate()
+ .alpha(0)
+ .translationX(translationX)
+ .setDuration(HIDE_DURATION_MILLIS)
+ .setInterpolator(HIDE_SCROLLBAR_INTERPOLATOR)
+ .start();
+ }
+
+ @Override
+ public boolean isScrollbarAutoHideEnabled() {
+ return mScrollbarAutoHideEnabled;
+ }
+
+ public void setScrollbarAutoHideEnabled(boolean enabled) {
+ mScrollbarAutoHideEnabled = enabled;
+ }
+
+ @Override
+ public int getScrollbarAutoHideDelayMillis() {
+ return AUTO_HIDE_SCROLLBAR_DELAY_MILLIS;
+ }
+
+ @Override
+ public void showPopup(@NonNull View popupView) {
+
+ if (mShowingPopup) {
+ return;
+ }
+ mShowingPopup = true;
+
+ popupView.animate()
+ .alpha(1)
+ .setDuration(SHOW_DURATION_MILLIS)
+ .start();
+ }
+
+ @Override
+ public void hidePopup(@NonNull View popupView) {
+
+ if (!mShowingPopup) {
+ return;
+ }
+ mShowingPopup = false;
+
+ popupView.animate()
+ .alpha(0)
+ .setDuration(HIDE_DURATION_MILLIS)
+ .start();
+ }
+}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollNestedScrollView.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollNestedScrollView.java
new file mode 100644
index 0000000..960fc46
--- /dev/null
+++ b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollNestedScrollView.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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
+ *
+ * https://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 me.zhanghai.android.fastscroll;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.widget.NestedScrollView;
+
+@SuppressLint("MissingSuperCall")
+public class FastScrollNestedScrollView extends NestedScrollView implements ViewHelperProvider {
+
+ @NonNull
+ private final ViewHelper mViewHelper = new ViewHelper();
+
+ public FastScrollNestedScrollView(@NonNull Context context) {
+ super(context);
+
+ init();
+ }
+
+ public FastScrollNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+
+ init();
+ }
+
+ public FastScrollNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ init();
+ }
+
+ private void init() {
+ setScrollContainer(true);
+ }
+
+ @NonNull
+ @Override
+ public FastScroller.ViewHelper getViewHelper() {
+ return mViewHelper;
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ mViewHelper.draw(canvas);
+ }
+
+ @Override
+ protected void onScrollChanged(int left, int top, int oldLeft, int oldTop) {
+ mViewHelper.onScrollChanged(left, top, oldLeft, oldTop);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(@NonNull MotionEvent event) {
+ return mViewHelper.onInterceptTouchEvent(event);
+ }
+
+ @Override
+ @SuppressLint("ClickableViewAccessibility")
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ return mViewHelper.onTouchEvent(event);
+ }
+
+ private class ViewHelper extends SimpleViewHelper {
+
+ @Override
+ public int getScrollRange() {
+ return super.getScrollRange() + getPaddingTop() + getPaddingBottom();
+ }
+
+ @Override
+ protected void superDraw(@NonNull Canvas canvas) {
+ FastScrollNestedScrollView.super.draw(canvas);
+ }
+
+ @Override
+ protected void superOnScrollChanged(int left, int top, int oldLeft, int oldTop) {
+ FastScrollNestedScrollView.super.onScrollChanged(left, top, oldLeft, oldTop);
+ }
+
+ @Override
+ protected boolean superOnInterceptTouchEvent(@NonNull MotionEvent event) {
+ return FastScrollNestedScrollView.super.onInterceptTouchEvent(event);
+ }
+
+ @Override
+ protected boolean superOnTouchEvent(@NonNull MotionEvent event) {
+ return FastScrollNestedScrollView.super.onTouchEvent(event);
+ }
+
+ @Override
+ @SuppressLint("RestrictedApi")
+ protected int computeVerticalScrollRange() {
+ return FastScrollNestedScrollView.this.computeVerticalScrollRange();
+ }
+
+ @Override
+ @SuppressLint("RestrictedApi")
+ protected int computeVerticalScrollOffset() {
+ return FastScrollNestedScrollView.this.computeVerticalScrollOffset();
+ }
+
+ @Override
+ protected int getScrollX() {
+ return FastScrollNestedScrollView.this.getScrollX();
+ }
+
+ @Override
+ protected void scrollTo(int x, int y) {
+ FastScrollNestedScrollView.this.scrollTo(x, y);
+ }
+ }
+}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollScrollView.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollScrollView.java
new file mode 100644
index 0000000..5d3790c
--- /dev/null
+++ b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollScrollView.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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
+ *
+ * https://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 me.zhanghai.android.fastscroll;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.ScrollView;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleRes;
+
+@SuppressLint("MissingSuperCall")
+public class FastScrollScrollView extends ScrollView implements ViewHelperProvider {
+
+ @NonNull
+ private final ViewHelper mViewHelper = new ViewHelper();
+
+ public FastScrollScrollView(@NonNull Context context) {
+ super(context);
+
+ init();
+ }
+
+ public FastScrollScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+
+ init();
+ }
+
+ public FastScrollScrollView(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ init();
+ }
+
+ public FastScrollScrollView(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ init();
+ }
+
+ private void init() {
+ setVerticalScrollBarEnabled(false);
+ setScrollContainer(true);
+ }
+
+ @NonNull
+ @Override
+ public FastScroller.ViewHelper getViewHelper() {
+ return mViewHelper;
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ mViewHelper.draw(canvas);
+ }
+
+ @Override
+ protected void onScrollChanged(int left, int top, int oldLeft, int oldTop) {
+ mViewHelper.onScrollChanged(left, top, oldLeft, oldTop);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(@NonNull MotionEvent event) {
+ return mViewHelper.onInterceptTouchEvent(event);
+ }
+
+ @Override
+ @SuppressLint("ClickableViewAccessibility")
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ return mViewHelper.onTouchEvent(event);
+ }
+
+ private class ViewHelper extends SimpleViewHelper {
+
+ @Override
+ public int getScrollRange() {
+ return super.getScrollRange() + getPaddingTop() + getPaddingBottom();
+ }
+
+ @Override
+ protected void superDraw(@NonNull Canvas canvas) {
+ FastScrollScrollView.super.draw(canvas);
+ }
+
+ @Override
+ protected void superOnScrollChanged(int left, int top, int oldLeft, int oldTop) {
+ FastScrollScrollView.super.onScrollChanged(left, top, oldLeft, oldTop);
+ }
+
+ @Override
+ protected boolean superOnInterceptTouchEvent(@NonNull MotionEvent event) {
+ return FastScrollScrollView.super.onInterceptTouchEvent(event);
+ }
+
+ @Override
+ protected boolean superOnTouchEvent(@NonNull MotionEvent event) {
+ return FastScrollScrollView.super.onTouchEvent(event);
+ }
+
+ @Override
+ protected int computeVerticalScrollRange() {
+ return FastScrollScrollView.this.computeVerticalScrollRange();
+ }
+
+ @Override
+ protected int computeVerticalScrollOffset() {
+ return FastScrollScrollView.this.computeVerticalScrollOffset();
+ }
+
+ @Override
+ protected int getScrollX() {
+ return FastScrollScrollView.this.getScrollX();
+ }
+
+ @Override
+ protected void scrollTo(int x, int y) {
+ FastScrollScrollView.this.scrollTo(x, y);
+ }
+ }
+}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollWebView.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollWebView.java
new file mode 100644
index 0000000..f6839e6
--- /dev/null
+++ b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollWebView.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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
+ *
+ * https://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 me.zhanghai.android.fastscroll;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.webkit.WebView;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleRes;
+
+@SuppressLint("MissingSuperCall")
+public class FastScrollWebView extends WebView implements ViewHelperProvider {
+
+ @NonNull
+ private final ViewHelper mViewHelper = new ViewHelper();
+
+ public FastScrollWebView(@NonNull Context context) {
+ super(context);
+
+ init();
+ }
+
+ public FastScrollWebView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+
+ init();
+ }
+
+ public FastScrollWebView(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ init();
+ }
+
+ public FastScrollWebView(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ init();
+ }
+
+ private void init() {
+ setVerticalScrollBarEnabled(false);
+ setScrollContainer(true);
+ }
+
+ @NonNull
+ @Override
+ public FastScroller.ViewHelper getViewHelper() {
+ return mViewHelper;
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ mViewHelper.draw(canvas);
+ }
+
+ @Override
+ protected void onScrollChanged(int left, int top, int oldLeft, int oldTop) {
+ mViewHelper.onScrollChanged(left, top, oldLeft, oldTop);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(@NonNull MotionEvent event) {
+ return mViewHelper.onInterceptTouchEvent(event);
+ }
+
+ @Override
+ @SuppressLint("ClickableViewAccessibility")
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+ return mViewHelper.onTouchEvent(event);
+ }
+
+ private class ViewHelper extends SimpleViewHelper {
+
+ @Override
+ protected void superDraw(@NonNull Canvas canvas) {
+ FastScrollWebView.super.draw(canvas);
+ }
+
+ @Override
+ protected void superOnScrollChanged(int left, int top, int oldLeft, int oldTop) {
+ FastScrollWebView.super.onScrollChanged(left, top, oldLeft, oldTop);
+ }
+
+ @Override
+ protected boolean superOnInterceptTouchEvent(@NonNull MotionEvent event) {
+ return FastScrollWebView.super.onInterceptTouchEvent(event);
+ }
+
+ @Override
+ protected boolean superOnTouchEvent(@NonNull MotionEvent event) {
+ return FastScrollWebView.super.onTouchEvent(event);
+ }
+
+ @Override
+ protected int computeVerticalScrollRange() {
+ return FastScrollWebView.this.computeVerticalScrollRange();
+ }
+
+ @Override
+ protected int computeVerticalScrollOffset() {
+ return FastScrollWebView.this.computeVerticalScrollOffset();
+ }
+
+ @Override
+ protected int getScrollX() {
+ return FastScrollWebView.this.getScrollX();
+ }
+
+ @Override
+ protected void scrollTo(int x, int y) {
+ FastScrollWebView.this.scrollTo(x, y);
+ }
+ }
+}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScroller.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScroller.java
new file mode 100644
index 0000000..eff1895
--- /dev/null
+++ b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScroller.java
@@ -0,0 +1,465 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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
+ *
+ * https://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 me.zhanghai.android.fastscroll;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewGroupOverlay;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import java.util.Objects;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.core.math.MathUtils;
+import androidx.core.util.Consumer;
+
+public class FastScroller {
+
+ private final int mMinTouchTargetSize;
+ private final int mTouchSlop;
+
+ @NonNull
+ private final ViewGroup mView;
+ @NonNull
+ private final ViewHelper mViewHelper;
+ @Nullable
+ private Rect mUserPadding;
+ @NonNull
+ private final AnimationHelper mAnimationHelper;
+
+ private final int mTrackWidth;
+ private final int mThumbWidth;
+ private final int mThumbHeight;
+
+ @NonNull
+ private final View mTrackView;
+ @NonNull
+ private final View mThumbView;
+ @NonNull
+ private final TextView mPopupView;
+
+ private boolean mScrollbarEnabled;
+ private int mThumbOffset;
+
+ private float mDownX;
+ private float mDownY;
+ private float mLastY;
+ private float mDragStartY;
+ private int mDragStartThumbOffset;
+ private boolean mDragging;
+
+ @NonNull
+ private final Runnable mAutoHideScrollbarRunnable = this::autoHideScrollbar;
+
+ @NonNull
+ private final Rect mTempRect = new Rect();
+
+ public FastScroller(@NonNull ViewGroup view, @NonNull ViewHelper viewHelper,
+ @Nullable Rect padding, @NonNull Drawable trackDrawable,
+ @NonNull Drawable thumbDrawable, @NonNull Consumer popupStyle,
+ @NonNull AnimationHelper animationHelper) {
+
+ mMinTouchTargetSize = view.getResources().getDimensionPixelSize(
+ R.dimen.afs_min_touch_target_size);
+ Context context = view.getContext();
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+
+ mView = view;
+ mViewHelper = viewHelper;
+ mUserPadding = padding;
+ mAnimationHelper = animationHelper;
+
+ mTrackWidth = requireNonNegative(trackDrawable.getIntrinsicWidth(),
+ "trackDrawable.getIntrinsicWidth() < 0");
+ mThumbWidth = requireNonNegative(thumbDrawable.getIntrinsicWidth(),
+ "thumbDrawable.getIntrinsicWidth() < 0");
+ mThumbHeight = requireNonNegative(thumbDrawable.getIntrinsicHeight(),
+ "thumbDrawable.getIntrinsicHeight() < 0");
+
+ mTrackView = new View(context);
+ mTrackView.setBackground(trackDrawable);
+ mThumbView = new View(context);
+ mThumbView.setBackground(thumbDrawable);
+ mPopupView = new AppCompatTextView(context);
+ mPopupView.setLayoutParams(new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ popupStyle.accept(mPopupView);
+
+ ViewGroupOverlay overlay = mView.getOverlay();
+ overlay.add(mTrackView);
+ overlay.add(mThumbView);
+ overlay.add(mPopupView);
+
+ postAutoHideScrollbar();
+ mPopupView.setAlpha(0);
+
+ mViewHelper.addOnPreDrawListener(this::onPreDraw);
+ mViewHelper.addOnScrollChangedListener(this::onScrollChanged);
+ mViewHelper.addOnTouchEventListener(this::onTouchEvent);
+ }
+
+ private static int requireNonNegative(int value, @NonNull String message) {
+ if (value < 0) {
+ throw new IllegalArgumentException(message);
+ }
+ return value;
+ }
+
+ public void setPadding(int left, int top, int right, int bottom) {
+ if (mUserPadding != null && mUserPadding.left == left && mUserPadding.top == top
+ && mUserPadding.right == right && mUserPadding.bottom == bottom) {
+ return;
+ }
+ if (mUserPadding == null) {
+ mUserPadding = new Rect();
+ }
+ mUserPadding.set(left, top, right, bottom);
+ mView.invalidate();
+ }
+
+ public void setPadding(@Nullable Rect padding) {
+ if (Objects.equals(mUserPadding, padding)) {
+ return;
+ }
+ if (padding != null) {
+ if (mUserPadding == null) {
+ mUserPadding = new Rect();
+ }
+ mUserPadding.set(padding);
+ } else {
+ mUserPadding = null;
+ }
+ mView.invalidate();
+ }
+
+ @NonNull
+ private Rect getPadding() {
+ if (mUserPadding != null) {
+ mTempRect.set(mUserPadding);
+ } else {
+ mTempRect.set(mView.getPaddingLeft(), mView.getPaddingTop(), mView.getPaddingRight(),
+ mView.getPaddingBottom());
+ }
+ return mTempRect;
+ }
+
+ private void onPreDraw() {
+
+ updateScrollbarState();
+ mTrackView.setVisibility(mScrollbarEnabled ? View.VISIBLE : View.INVISIBLE);
+ mThumbView.setVisibility(mScrollbarEnabled ? View.VISIBLE : View.INVISIBLE);
+ if (!mScrollbarEnabled) {
+ mPopupView.setVisibility(View.INVISIBLE);
+ return;
+ }
+
+ int layoutDirection = mView.getLayoutDirection();
+ mTrackView.setLayoutDirection(layoutDirection);
+ mThumbView.setLayoutDirection(layoutDirection);
+ mPopupView.setLayoutDirection(layoutDirection);
+
+ boolean isLayoutRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL;
+ int viewWidth = mView.getWidth();
+ int viewHeight = mView.getHeight();
+
+ Rect padding = getPadding();
+ int trackLeft = isLayoutRtl ? padding.left : viewWidth - padding.right - mTrackWidth;
+ layoutView(mTrackView, trackLeft, padding.top, trackLeft + mTrackWidth,
+ Math.max(viewHeight - padding.bottom, padding.top));
+ int thumbLeft = isLayoutRtl ? padding.left : viewWidth - padding.right - mThumbWidth;
+ int thumbTop = padding.top + mThumbOffset;
+ layoutView(mThumbView, thumbLeft, thumbTop, thumbLeft + mThumbWidth,
+ thumbTop + mThumbHeight);
+
+ CharSequence popupText = mViewHelper.getPopupText();
+ boolean hasPopup = !TextUtils.isEmpty(popupText);
+ mPopupView.setVisibility(hasPopup ? View.VISIBLE : View.INVISIBLE);
+ if (hasPopup) {
+ FrameLayout.LayoutParams popupLayoutParams = (FrameLayout.LayoutParams)
+ mPopupView.getLayoutParams();
+ if (!Objects.equals(mPopupView.getText(), popupText)) {
+ mPopupView.setText(popupText);
+ int widthMeasureSpec = ViewGroup.getChildMeasureSpec(
+ View.MeasureSpec.makeMeasureSpec(viewWidth, View.MeasureSpec.EXACTLY),
+ padding.left + padding.right + mThumbWidth + popupLayoutParams.leftMargin
+ + popupLayoutParams.rightMargin, popupLayoutParams.width);
+ int heightMeasureSpec = ViewGroup.getChildMeasureSpec(
+ View.MeasureSpec.makeMeasureSpec(viewHeight, View.MeasureSpec.EXACTLY),
+ padding.top + padding.bottom + popupLayoutParams.topMargin
+ + popupLayoutParams.bottomMargin, popupLayoutParams.height);
+ mPopupView.measure(widthMeasureSpec, heightMeasureSpec);
+ }
+ int popupWidth = mPopupView.getMeasuredWidth();
+ int popupHeight = mPopupView.getMeasuredHeight();
+ int popupLeft = isLayoutRtl ? padding.left + mThumbWidth + popupLayoutParams.leftMargin
+ : viewWidth - padding.right - mThumbWidth - popupLayoutParams.rightMargin
+ - popupWidth;
+ int popupAnchorY;
+ switch (popupLayoutParams.gravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.LEFT:
+ default:
+ popupAnchorY = 0;
+ break;
+ case Gravity.CENTER_HORIZONTAL:
+ popupAnchorY = popupHeight / 2;
+ break;
+ case Gravity.RIGHT:
+ popupAnchorY = popupHeight;
+ break;
+ }
+ int thumbAnchorY;
+ switch (popupLayoutParams.gravity & Gravity.VERTICAL_GRAVITY_MASK) {
+ case Gravity.TOP:
+ default:
+ thumbAnchorY = mThumbView.getPaddingTop();
+ break;
+ case Gravity.CENTER_VERTICAL: {
+ int thumbPaddingTop = mThumbView.getPaddingTop();
+ thumbAnchorY = thumbPaddingTop + (mThumbHeight - thumbPaddingTop
+ - mThumbView.getPaddingBottom()) / 2;
+ break;
+ }
+ case Gravity.BOTTOM:
+ thumbAnchorY = mThumbHeight - mThumbView.getPaddingBottom();
+ break;
+ }
+ int popupTop = MathUtils.clamp(thumbTop + thumbAnchorY - popupAnchorY,
+ padding.top + popupLayoutParams.topMargin,
+ viewHeight - padding.bottom - popupLayoutParams.bottomMargin - popupHeight);
+ layoutView(mPopupView, popupLeft, popupTop, popupLeft + popupWidth,
+ popupTop + popupHeight);
+ }
+ }
+
+ private void updateScrollbarState() {
+ int scrollOffsetRange = getScrollOffsetRange();
+ mScrollbarEnabled = scrollOffsetRange > 0;
+ mThumbOffset = mScrollbarEnabled ? (int) ((long) getThumbOffsetRange()
+ * mViewHelper.getScrollOffset() / scrollOffsetRange) : 0;
+ }
+
+ private void layoutView(@NonNull View view, int left, int top, int right, int bottom) {
+ int scrollX = mView.getScrollX();
+ int scrollY = mView.getScrollY();
+ view.layout(scrollX + left, scrollY + top, scrollX + right, scrollY + bottom);
+ }
+
+ private void onScrollChanged() {
+
+ updateScrollbarState();
+ if (!mScrollbarEnabled) {
+ return;
+ }
+
+ mAnimationHelper.showScrollbar(mTrackView, mThumbView);
+ postAutoHideScrollbar();
+ }
+
+ private boolean onTouchEvent(@NonNull MotionEvent event) {
+
+ if (!mScrollbarEnabled) {
+ return false;
+ }
+
+ float eventX = event.getX();
+ float eventY = event.getY();
+ Rect padding = getPadding();
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+
+ mDownX = eventX;
+ mDownY = eventY;
+
+ if (mThumbView.getAlpha() > 0 && isInViewTouchTarget(mThumbView, eventX, eventY)) {
+ mDragStartY = eventY;
+ mDragStartThumbOffset = mThumbOffset;
+ setDragging(true);
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+
+ if (!mDragging && isInViewTouchTarget(mTrackView, mDownX, mDownY)
+ && Math.abs(eventY - mDownY) > mTouchSlop) {
+ if (isInViewTouchTarget(mThumbView, mDownX, mDownY)) {
+ mDragStartY = mLastY;
+ mDragStartThumbOffset = mThumbOffset;
+ } else {
+ mDragStartY = eventY;
+ mDragStartThumbOffset = (int) (eventY - padding.top - mThumbHeight / 2f);
+ scrollToThumbOffset(mDragStartThumbOffset);
+ }
+ setDragging(true);
+ }
+
+ if (mDragging) {
+ int thumbOffset = mDragStartThumbOffset + (int) (eventY - mDragStartY);
+ scrollToThumbOffset(thumbOffset);
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+
+ setDragging(false);
+ break;
+ }
+
+ mLastY = eventY;
+
+ return mDragging;
+ }
+
+ private boolean isInView(@NonNull View view, float x, float y) {
+ int scrollX = mView.getScrollX();
+ int scrollY = mView.getScrollY();
+ return x >= view.getLeft() - scrollX && x < view.getRight() - scrollX
+ && y >= view.getTop() - scrollY && y < view.getBottom() - scrollY;
+ }
+
+ private boolean isInViewTouchTarget(@NonNull View view, float x, float y) {
+ int scrollX = mView.getScrollX();
+ int scrollY = mView.getScrollY();
+ return isInTouchTarget(x, view.getLeft() - scrollX, view.getRight() - scrollX, 0,
+ mView.getWidth())
+ && isInTouchTarget(y, view.getTop() - scrollY, view.getBottom() - scrollY, 0,
+ mView.getHeight());
+ }
+
+ private boolean isInTouchTarget(float position, int viewStart, int viewEnd, int parentStart,
+ int parentEnd) {
+ int viewSize = viewEnd - viewStart;
+ if (viewSize >= mMinTouchTargetSize) {
+ return position >= viewStart && position < viewEnd;
+ }
+ int touchTargetStart = viewStart - (mMinTouchTargetSize - viewSize) / 2;
+ if (touchTargetStart < parentStart) {
+ touchTargetStart = parentStart;
+ }
+ int touchTargetEnd = touchTargetStart + mMinTouchTargetSize;
+ if (touchTargetEnd > parentEnd) {
+ touchTargetEnd = parentEnd;
+ touchTargetStart = touchTargetEnd - mMinTouchTargetSize;
+ if (touchTargetStart < parentStart) {
+ touchTargetStart = parentStart;
+ }
+ }
+ return position >= touchTargetStart && position < touchTargetEnd;
+ }
+
+ private void scrollToThumbOffset(int thumbOffset) {
+ int thumbOffsetRange = getThumbOffsetRange();
+ thumbOffset = MathUtils.clamp(thumbOffset, 0, thumbOffsetRange);
+ int scrollOffset = (int) ((long) getScrollOffsetRange() * thumbOffset / thumbOffsetRange);
+ mViewHelper.scrollTo(scrollOffset);
+ }
+
+ private int getScrollOffsetRange() {
+ return mViewHelper.getScrollRange() - mView.getHeight();
+ }
+
+ private int getThumbOffsetRange() {
+ Rect padding = getPadding();
+ return mView.getHeight() - padding.top - padding.bottom - mThumbHeight;
+ }
+
+ private void setDragging(boolean dragging) {
+
+ if (mDragging == dragging) {
+ return;
+ }
+ mDragging = dragging;
+
+ if (mDragging) {
+ mView.getParent().requestDisallowInterceptTouchEvent(true);
+ }
+
+ mTrackView.setPressed(mDragging);
+ mThumbView.setPressed(mDragging);
+
+ if (mDragging) {
+ cancelAutoHideScrollbar();
+ mAnimationHelper.showScrollbar(mTrackView, mThumbView);
+ mAnimationHelper.showPopup(mPopupView);
+ } else {
+ postAutoHideScrollbar();
+ mAnimationHelper.hidePopup(mPopupView);
+ }
+ }
+
+ private void postAutoHideScrollbar() {
+ cancelAutoHideScrollbar();
+ if (mAnimationHelper.isScrollbarAutoHideEnabled()) {
+ mView.postDelayed(mAutoHideScrollbarRunnable,
+ mAnimationHelper.getScrollbarAutoHideDelayMillis());
+ }
+ }
+
+ private void autoHideScrollbar() {
+ if (mDragging) {
+ return;
+ }
+ mAnimationHelper.hideScrollbar(mTrackView, mThumbView);
+ }
+
+ private void cancelAutoHideScrollbar() {
+ mView.removeCallbacks(mAutoHideScrollbarRunnable);
+ }
+
+ public interface ViewHelper {
+
+ void addOnPreDrawListener(@NonNull Runnable onPreDraw);
+
+ void addOnScrollChangedListener(@NonNull Runnable onScrollChanged);
+
+ void addOnTouchEventListener(@NonNull Predicate onTouchEvent);
+
+ int getScrollRange();
+
+ int getScrollOffset();
+
+ void scrollTo(int offset);
+
+ @Nullable
+ default CharSequence getPopupText() {
+ return null;
+ }
+ }
+
+ public interface AnimationHelper {
+
+ void showScrollbar(@NonNull View trackView, @NonNull View thumbView);
+
+ void hideScrollbar(@NonNull View trackView, @NonNull View thumbView);
+
+ boolean isScrollbarAutoHideEnabled();
+
+ int getScrollbarAutoHideDelayMillis();
+
+ void showPopup(@NonNull View popupView);
+
+ void hidePopup(@NonNull View popupView);
+ }
+}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollerBuilder.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollerBuilder.java
new file mode 100644
index 0000000..57ceaf3
--- /dev/null
+++ b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FastScrollerBuilder.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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
+ *
+ * https://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 me.zhanghai.android.fastscroll;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.ViewGroup;
+import android.webkit.WebView;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.util.Consumer;
+import androidx.core.widget.NestedScrollView;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class FastScrollerBuilder {
+
+ @NonNull
+ private final ViewGroup mView;
+
+ @Nullable
+ private FastScroller.ViewHelper mViewHelper;
+
+ @Nullable
+ private PopupTextProvider mPopupTextProvider;
+
+ @Nullable
+ private Rect mPadding;
+
+ @NonNull
+ private Drawable mTrackDrawable;
+
+ @NonNull
+ private Drawable mThumbDrawable;
+
+ @NonNull
+ private Consumer mPopupStyle;
+
+ @Nullable
+ private FastScroller.AnimationHelper mAnimationHelper;
+
+ public FastScrollerBuilder(@NonNull ViewGroup view) {
+ mView = view;
+ useDefaultStyle();
+ }
+
+ @NonNull
+ public FastScrollerBuilder setViewHelper(@Nullable FastScroller.ViewHelper viewHelper) {
+ mViewHelper = viewHelper;
+ return this;
+ }
+
+ @NonNull
+ public FastScrollerBuilder setPopupTextProvider(@Nullable PopupTextProvider popupTextProvider) {
+ mPopupTextProvider = popupTextProvider;
+ return this;
+ }
+
+ @NonNull
+ public FastScrollerBuilder setPadding(int left, int top, int right, int bottom) {
+ if (mPadding == null) {
+ mPadding = new Rect();
+ }
+ mPadding.set(left, top, right, bottom);
+ return this;
+ }
+
+ @NonNull
+ public FastScrollerBuilder setPadding(@Nullable Rect padding) {
+ if (padding != null) {
+ if (mPadding == null) {
+ mPadding = new Rect();
+ }
+ mPadding.set(padding);
+ } else {
+ mPadding = null;
+ }
+ return this;
+ }
+
+ @NonNull
+ public FastScrollerBuilder setTrackDrawable(@NonNull Drawable trackDrawable) {
+ mTrackDrawable = trackDrawable;
+ return this;
+ }
+
+ @NonNull
+ public FastScrollerBuilder setThumbDrawable(@NonNull Drawable thumbDrawable) {
+ mThumbDrawable = thumbDrawable;
+ return this;
+ }
+
+ @NonNull
+ public FastScrollerBuilder setPopupStyle(@NonNull Consumer popupStyle) {
+ mPopupStyle = popupStyle;
+ return this;
+ }
+
+ @NonNull
+ public FastScrollerBuilder useDefaultStyle() {
+ Context context = mView.getContext();
+ mTrackDrawable = Utils.getGradientDrawableWithTintAttr(R.drawable.afs_track,
+ androidx.appcompat.R.attr.colorControlNormal, context);
+ mThumbDrawable = Utils.getGradientDrawableWithTintAttr(R.drawable.afs_thumb,
+ androidx.appcompat.R.attr.colorControlActivated, context);
+ mPopupStyle = PopupStyles.DEFAULT;
+ return this;
+ }
+
+ @NonNull
+ public FastScrollerBuilder useMd2Style() {
+ Context context = mView.getContext();
+ mTrackDrawable = Utils.getGradientDrawableWithTintAttr(R.drawable.afs_md2_track,
+ androidx.appcompat.R.attr.colorControlNormal, context);
+ mThumbDrawable = Utils.getGradientDrawableWithTintAttr(R.drawable.afs_md2_thumb,
+ androidx.appcompat.R.attr.colorControlActivated, context);
+ mPopupStyle = PopupStyles.MD2;
+ return this;
+ }
+
+ public void setAnimationHelper(@Nullable FastScroller.AnimationHelper animationHelper) {
+ mAnimationHelper = animationHelper;
+ }
+
+ public void disableScrollbarAutoHide() {
+ DefaultAnimationHelper animationHelper = new DefaultAnimationHelper(mView);
+ animationHelper.setScrollbarAutoHideEnabled(false);
+ mAnimationHelper = animationHelper;
+ }
+
+ @NonNull
+ public FastScroller build() {
+ return new FastScroller(mView, getOrCreateViewHelper(), mPadding, mTrackDrawable,
+ mThumbDrawable, mPopupStyle, getOrCreateAnimationHelper());
+ }
+
+ @NonNull
+ private FastScroller.ViewHelper getOrCreateViewHelper() {
+ if (mViewHelper != null) {
+ return mViewHelper;
+ }
+ if (mView instanceof ViewHelperProvider) {
+ return ((ViewHelperProvider) mView).getViewHelper();
+ } else if (mView instanceof RecyclerView) {
+ return new RecyclerViewHelper((RecyclerView) mView, mPopupTextProvider);
+ } else if (mView instanceof NestedScrollView) {
+ throw new UnsupportedOperationException("Please use "
+ + FastScrollNestedScrollView.class.getSimpleName() + " instead of "
+ + NestedScrollView.class.getSimpleName() + "for fast scroll");
+ } else if (mView instanceof ScrollView) {
+ throw new UnsupportedOperationException("Please use "
+ + FastScrollScrollView.class.getSimpleName() + " instead of "
+ + ScrollView.class.getSimpleName() + "for fast scroll");
+ } else if (mView instanceof WebView) {
+ throw new UnsupportedOperationException("Please use "
+ + FastScrollWebView.class.getSimpleName() + " instead of "
+ + WebView.class.getSimpleName() + "for fast scroll");
+ } else {
+ throw new UnsupportedOperationException(mView.getClass().getSimpleName()
+ + " is not supported for fast scroll");
+ }
+ }
+
+ @NonNull
+ private FastScroller.AnimationHelper getOrCreateAnimationHelper() {
+ if (mAnimationHelper != null) {
+ return mAnimationHelper;
+ }
+ return new DefaultAnimationHelper(mView);
+ }
+}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FixItemDecorationRecyclerView.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FixItemDecorationRecyclerView.java
new file mode 100644
index 0000000..d8c3d33
--- /dev/null
+++ b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FixItemDecorationRecyclerView.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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
+ *
+ * https://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 me.zhanghai.android.fastscroll;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class FixItemDecorationRecyclerView extends RecyclerView {
+
+ public FixItemDecorationRecyclerView(@NonNull Context context) {
+ super(context);
+ }
+
+ public FixItemDecorationRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public FixItemDecorationRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void dispatchDraw(@NonNull Canvas canvas) {
+ for (int i = 0, count = getItemDecorationCount(); i < count; ++i) {
+ FixItemDecoration decor = (FixItemDecoration) super.getItemDecorationAt(i);
+ decor.getItemDecoration().onDraw(canvas, this, decor.getState());
+ }
+ super.dispatchDraw(canvas);
+ for (int i = 0, count = getItemDecorationCount(); i < count; ++i) {
+ FixItemDecoration decor = (FixItemDecoration) super.getItemDecorationAt(i);
+ decor.getItemDecoration().onDrawOver(canvas, this, decor.getState());
+ }
+ }
+
+ @Override
+ public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
+ super.addItemDecoration(new FixItemDecoration(decor), index);
+ }
+
+ @NonNull
+ @Override
+ public ItemDecoration getItemDecorationAt(int index) {
+ return ((FixItemDecoration) super.getItemDecorationAt(index)).getItemDecoration();
+ }
+
+ @Override
+ public void removeItemDecoration(@NonNull ItemDecoration decor) {
+ if (!(decor instanceof FixItemDecoration)) {
+ for (int i = 0, count = getItemDecorationCount(); i < count; ++i) {
+ FixItemDecoration fixDecor = (FixItemDecoration) super.getItemDecorationAt(i);
+ if (fixDecor.getItemDecoration() == decor) {
+ decor = fixDecor;
+ break;
+ }
+ }
+ }
+ super.removeItemDecoration(decor);
+ }
+
+ private static class FixItemDecoration extends ItemDecoration {
+
+ @NonNull
+ private final ItemDecoration mItemDecoration;
+
+ private State mState;
+
+ private FixItemDecoration(@NonNull ItemDecoration itemDecoration) {
+ mItemDecoration = itemDecoration;
+ }
+
+ @NonNull
+ public ItemDecoration getItemDecoration() {
+ return mItemDecoration;
+ }
+
+ public State getState() {
+ return mState;
+ }
+
+ @Override
+ public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
+ mState = state;
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) {}
+
+ @Override
+ public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
+ @NonNull State state) {}
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent) {}
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public void getItemOffsets(@NonNull Rect outRect, int itemPosition,
+ @NonNull RecyclerView parent) {
+ mItemDecoration.getItemOffsets(outRect, itemPosition, parent);
+ }
+
+ @Override
+ public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
+ @NonNull RecyclerView parent, @NonNull State state) {
+ mItemDecoration.getItemOffsets(outRect, view, parent, state);
+ }
+ }
+}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FixOnItemTouchListenerRecyclerView.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FixOnItemTouchListenerRecyclerView.java
new file mode 100644
index 0000000..5400792
--- /dev/null
+++ b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/FixOnItemTouchListenerRecyclerView.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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
+ *
+ * https://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 me.zhanghai.android.fastscroll;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class FixOnItemTouchListenerRecyclerView extends RecyclerView {
+
+ @NonNull
+ private final OnItemTouchDispatcher mOnItemTouchDispatcher = new OnItemTouchDispatcher();
+
+ public FixOnItemTouchListenerRecyclerView(@NonNull Context context) {
+ super(context);
+
+ init();
+ }
+
+ public FixOnItemTouchListenerRecyclerView(@NonNull Context context,
+ @Nullable AttributeSet attrs) {
+ super(context, attrs);
+
+ init();
+ }
+
+ public FixOnItemTouchListenerRecyclerView(@NonNull Context context,
+ @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ init();
+ }
+
+ private void init() {
+ super.addOnItemTouchListener(mOnItemTouchDispatcher);
+ }
+
+ @Override
+ public void addOnItemTouchListener(@NonNull OnItemTouchListener listener) {
+ mOnItemTouchDispatcher.addListener(listener);
+ }
+
+ @Override
+ public void removeOnItemTouchListener(@NonNull OnItemTouchListener listener) {
+ mOnItemTouchDispatcher.removeListener(listener);
+ }
+
+ private static class OnItemTouchDispatcher implements OnItemTouchListener {
+
+ @NonNull
+ private final List mListeners = new ArrayList<>();
+
+ @NonNull
+ private final Set mTrackingListeners = new LinkedHashSet<>();
+
+ @Nullable
+ private OnItemTouchListener mInterceptingListener;
+
+ public void addListener(@NonNull OnItemTouchListener listener) {
+ mListeners.add(listener);
+ }
+
+ public void removeListener(@NonNull OnItemTouchListener listener) {
+ mListeners.remove(listener);
+ mTrackingListeners.remove(listener);
+ if (mInterceptingListener == listener) {
+ mInterceptingListener = null;
+ }
+ }
+
+ // @see RecyclerView#findInterceptingOnItemTouchListener
+ @Override
+ public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
+ @NonNull MotionEvent event) {
+ int action = event.getAction();
+ for (OnItemTouchListener listener : mListeners) {
+ boolean intercepted = listener.onInterceptTouchEvent(recyclerView, event);
+ if (action == MotionEvent.ACTION_CANCEL) {
+ mTrackingListeners.remove(listener);
+ continue;
+ }
+ if (intercepted) {
+ mTrackingListeners.remove(listener);
+ event.setAction(MotionEvent.ACTION_CANCEL);
+ for (OnItemTouchListener trackingListener : mTrackingListeners) {
+ trackingListener.onInterceptTouchEvent(recyclerView, event);
+ }
+ event.setAction(action);
+ mTrackingListeners.clear();
+ mInterceptingListener = listener;
+ return true;
+ } else {
+ mTrackingListeners.add(listener);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) {
+ if (mInterceptingListener == null) {
+ return;
+ }
+ mInterceptingListener.onTouchEvent(recyclerView, event);
+ int action = event.getAction();
+ if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+ mInterceptingListener = null;
+ }
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ for (OnItemTouchListener listener : mListeners) {
+ listener.onRequestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+ }
+ }
+}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/Md2PopupBackground.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/Md2PopupBackground.java
new file mode 100644
index 0000000..453c20b
--- /dev/null
+++ b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/Md2PopupBackground.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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
+ *
+ * https://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 me.zhanghai.android.fastscroll;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.graphics.drawable.DrawableCompat;
+
+class Md2PopupBackground extends Drawable {
+
+ @NonNull
+ private final Paint mPaint;
+ private final int mPaddingStart;
+ private final int mPaddingEnd;
+
+ @NonNull
+ private final Path mPath = new Path();
+
+ @NonNull
+ private final Matrix mTempMatrix = new Matrix();
+
+ public Md2PopupBackground(@NonNull Context context) {
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+ mPaint.setColor(Utils.getColorFromAttrRes(androidx.appcompat.R.attr.colorControlActivated, context));
+ mPaint.setStyle(Paint.Style.FILL);
+ Resources resources = context.getResources();
+ mPaddingStart = resources.getDimensionPixelOffset(R.dimen.afs_md2_popup_padding_start);
+ mPaddingEnd = resources.getDimensionPixelOffset(R.dimen.afs_md2_popup_padding_end);
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ canvas.drawPath(mPath, mPaint);
+ }
+
+ @Override
+ public boolean onLayoutDirectionChanged(int layoutDirection) {
+ updatePath();
+ return true;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {}
+
+ @Override
+ public void setColorFilter(@Nullable ColorFilter colorFilter) {}
+
+ @Override
+ public boolean isAutoMirrored() {
+ return true;
+ }
+
+ private boolean needMirroring() {
+ return DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL;
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ protected void onBoundsChange(@NonNull Rect bounds) {
+ updatePath();
+ }
+
+ private void updatePath() {
+
+ mPath.reset();
+
+ Rect bounds = getBounds();
+ float width = bounds.width();
+ float height = bounds.height();
+ float r = height / 2;
+ float sqrt2 = (float) Math.sqrt(2);
+ // Ensure we are convex.
+ width = Math.max(r + sqrt2 * r, width);
+ pathArcTo(mPath, r, r, r, 90, 180);
+ float o1X = width - sqrt2 * r;
+ pathArcTo(mPath, o1X, r, r, -90, 45f);
+ float r2 = r / 5;
+ float o2X = width - sqrt2 * r2;
+ pathArcTo(mPath, o2X, r, r2, -45, 90);
+ pathArcTo(mPath, o1X, r, r, 45f, 45f);
+ mPath.close();
+
+ if (needMirroring()) {
+ mTempMatrix.setScale(-1, 1, width / 2, 0);
+ } else {
+ mTempMatrix.reset();
+ }
+ mTempMatrix.postTranslate(bounds.left, bounds.top);
+ mPath.transform(mTempMatrix);
+ }
+
+ private static void pathArcTo(@NonNull Path path, float centerX, float centerY, float radius,
+ float startAngle, float sweepAngle) {
+ path.arcTo(centerX - radius, centerY - radius, centerX + radius, centerY + radius,
+ startAngle, sweepAngle, false);
+ }
+
+ @Override
+ public boolean getPadding(@NonNull Rect padding) {
+ if (needMirroring()) {
+ padding.set(mPaddingEnd, 0, mPaddingStart, 0);
+ } else {
+ padding.set(mPaddingStart, 0, mPaddingEnd, 0);
+ }
+ return true;
+ }
+
+ @Override
+ public void getOutline(@NonNull Outline outline) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && !mPath.isConvex()) {
+ // The outline path must be convex before Q, but we may run into floating point error
+ // caused by calculation involving sqrt(2) or OEM implementation difference, so in this
+ // case we just omit the shadow instead of crashing.
+ super.getOutline(outline);
+ return;
+ }
+ outline.setConvexPath(mPath);
+ }
+}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/PopupStyles.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/PopupStyles.java
new file mode 100644
index 0000000..10ea3f2
--- /dev/null
+++ b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/PopupStyles.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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
+ *
+ * https://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 me.zhanghai.android.fastscroll;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.TextUtils;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import androidx.core.util.Consumer;
+
+public class PopupStyles {
+
+ private PopupStyles() {}
+
+ public static Consumer DEFAULT = popupView -> {
+ Resources resources = popupView.getResources();
+ int minimumSize = resources.getDimensionPixelSize(R.dimen.afs_popup_min_size);
+ popupView.setMinimumWidth(minimumSize);
+ popupView.setMinimumHeight(minimumSize);
+ FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams)
+ popupView.getLayoutParams();
+ layoutParams.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL;
+ layoutParams.setMarginEnd(resources.getDimensionPixelOffset(R.dimen.afs_popup_margin_end));
+ popupView.setLayoutParams(layoutParams);
+ Context context = popupView.getContext();
+ popupView.setBackground(new AutoMirrorDrawable(Utils.getGradientDrawableWithTintAttr(
+ R.drawable.afs_popup_background, androidx.appcompat.R.attr.colorControlActivated, context)));
+ popupView.setEllipsize(TextUtils.TruncateAt.MIDDLE);
+ popupView.setGravity(Gravity.CENTER);
+ popupView.setIncludeFontPadding(false);
+ popupView.setSingleLine(true);
+ popupView.setTextColor(Utils.getColorFromAttrRes(android.R.attr.textColorPrimaryInverse,
+ context));
+ popupView.setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimensionPixelSize(
+ R.dimen.afs_popup_text_size));
+ };
+
+ public static Consumer MD2 = popupView -> {
+ Resources resources = popupView.getResources();
+ popupView.setMinimumWidth(resources.getDimensionPixelSize(
+ R.dimen.afs_md2_popup_min_width));
+ popupView.setMinimumHeight(resources.getDimensionPixelSize(
+ R.dimen.afs_md2_popup_min_height));
+ FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams)
+ popupView.getLayoutParams();
+ layoutParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
+ layoutParams.setMarginEnd(resources.getDimensionPixelOffset(
+ R.dimen.afs_md2_popup_margin_end));
+ popupView.setLayoutParams(layoutParams);
+ Context context = popupView.getContext();
+ popupView.setBackground(new Md2PopupBackground(context));
+ popupView.setElevation(resources.getDimensionPixelOffset(R.dimen.afs_md2_popup_elevation));
+ popupView.setEllipsize(TextUtils.TruncateAt.MIDDLE);
+ popupView.setGravity(Gravity.CENTER);
+ popupView.setIncludeFontPadding(false);
+ popupView.setSingleLine(true);
+ popupView.setTextColor(Utils.getColorFromAttrRes(android.R.attr.textColorPrimaryInverse,
+ context));
+ popupView.setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimensionPixelSize(
+ R.dimen.afs_md2_popup_text_size));
+ };
+}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/PopupTextProvider.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/PopupTextProvider.java
new file mode 100644
index 0000000..69cc848
--- /dev/null
+++ b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/PopupTextProvider.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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
+ *
+ * https://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 me.zhanghai.android.fastscroll;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+public interface PopupTextProvider {
+
+ @NonNull
+ CharSequence getPopupText(@NonNull View view, int position);
+}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/Predicate.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/Predicate.java
new file mode 100644
index 0000000..0bc6793
--- /dev/null
+++ b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/Predicate.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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
+ *
+ * https://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 me.zhanghai.android.fastscroll;
+
+@FunctionalInterface
+public interface Predicate {
+
+ boolean test(T t);
+}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/RecyclerViewHelper.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/RecyclerViewHelper.java
new file mode 100644
index 0000000..18e799f
--- /dev/null
+++ b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/RecyclerViewHelper.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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
+ *
+ * https://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 me.zhanghai.android.fastscroll;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+class RecyclerViewHelper implements FastScroller.ViewHelper {
+
+ @NonNull
+ private final RecyclerView mView;
+ @Nullable
+ private final PopupTextProvider mPopupTextProvider;
+
+ @NonNull
+ private final Rect mTempRect = new Rect();
+
+ public RecyclerViewHelper(@NonNull RecyclerView view,
+ @Nullable PopupTextProvider popupTextProvider) {
+ mView = view;
+ mPopupTextProvider = popupTextProvider;
+ }
+
+ @Override
+ public void addOnPreDrawListener(@NonNull Runnable onPreDraw) {
+ mView.addItemDecoration(new RecyclerView.ItemDecoration() {
+ @Override
+ public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent,
+ @NonNull RecyclerView.State state) {
+ onPreDraw.run();
+ }
+ });
+ }
+
+ @Override
+ public void addOnScrollChangedListener(@NonNull Runnable onScrollChanged) {
+ mView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+ onScrollChanged.run();
+ }
+ });
+ }
+
+ @Override
+ public void addOnTouchEventListener(@NonNull Predicate onTouchEvent) {
+ mView.addOnItemTouchListener(new RecyclerView.SimpleOnItemTouchListener() {
+ @Override
+ public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
+ @NonNull MotionEvent event) {
+ return onTouchEvent.test(event);
+ }
+ @Override
+ public void onTouchEvent(@NonNull RecyclerView recyclerView,
+ @NonNull MotionEvent event) {
+ onTouchEvent.test(event);
+ }
+ });
+ }
+
+ @Override
+ public int getScrollRange() {
+ int itemCount = getItemCount();
+ if (itemCount == 0) {
+ return 0;
+ }
+ int itemHeight = getItemHeight();
+ if (itemHeight == 0) {
+ return 0;
+ }
+ return mView.getPaddingTop() + itemCount * itemHeight + mView.getPaddingBottom();
+ }
+
+ @Override
+ public int getScrollOffset() {
+ int firstItemPosition = getFirstItemPosition();
+ if (firstItemPosition == RecyclerView.NO_POSITION) {
+ return 0;
+ }
+ int itemHeight = getItemHeight();
+ int firstItemTop = getFirstItemOffset();
+ return mView.getPaddingTop() + firstItemPosition * itemHeight - firstItemTop;
+ }
+
+ @Override
+ public void scrollTo(int offset) {
+ // Stop any scroll in progress for RecyclerView.
+ mView.stopScroll();
+ offset -= mView.getPaddingTop();
+ int itemHeight = getItemHeight();
+ // firstItemPosition should be non-negative even if paddingTop is greater than item height.
+ int firstItemPosition = Math.max(0, offset / itemHeight);
+ int firstItemTop = firstItemPosition * itemHeight - offset;
+ scrollToPositionWithOffset(firstItemPosition, firstItemTop);
+ }
+
+ @Nullable
+ @Override
+ public CharSequence getPopupText() {
+ PopupTextProvider popupTextProvider = mPopupTextProvider;
+ if (popupTextProvider == null) {
+ RecyclerView.Adapter> adapter = mView.getAdapter();
+ if (adapter instanceof PopupTextProvider) {
+ popupTextProvider = (PopupTextProvider) adapter;
+ }
+ }
+ if (popupTextProvider == null) {
+ return null;
+ }
+ int position = getFirstItemAdapterPosition();
+ if (position == RecyclerView.NO_POSITION) {
+ return null;
+ }
+ return popupTextProvider.getPopupText(mView, position);
+ }
+
+ private int getItemCount() {
+ LinearLayoutManager linearLayoutManager = getVerticalLinearLayoutManager();
+ if (linearLayoutManager == null) {
+ return 0;
+ }
+ int itemCount = linearLayoutManager.getItemCount();
+ if (itemCount == 0) {
+ return 0;
+ }
+ if (linearLayoutManager instanceof GridLayoutManager) {
+ GridLayoutManager gridLayoutManager = (GridLayoutManager) linearLayoutManager;
+ itemCount = (itemCount - 1) / gridLayoutManager.getSpanCount() + 1;
+ }
+ return itemCount;
+ }
+
+ private int getItemHeight() {
+ if (mView.getChildCount() == 0) {
+ return 0;
+ }
+ View itemView = mView.getChildAt(0);
+ mView.getDecoratedBoundsWithMargins(itemView, mTempRect);
+ return mTempRect.height();
+ }
+
+ private int getFirstItemPosition() {
+ int position = getFirstItemAdapterPosition();
+ LinearLayoutManager linearLayoutManager = getVerticalLinearLayoutManager();
+ if (linearLayoutManager == null) {
+ return RecyclerView.NO_POSITION;
+ }
+ if (linearLayoutManager instanceof GridLayoutManager) {
+ GridLayoutManager gridLayoutManager = (GridLayoutManager) linearLayoutManager;
+ position /= gridLayoutManager.getSpanCount();
+ }
+ return position;
+ }
+
+ private int getFirstItemAdapterPosition() {
+ if (mView.getChildCount() == 0) {
+ return RecyclerView.NO_POSITION;
+ }
+ View itemView = mView.getChildAt(0);
+ LinearLayoutManager linearLayoutManager = getVerticalLinearLayoutManager();
+ if (linearLayoutManager == null) {
+ return RecyclerView.NO_POSITION;
+ }
+ return linearLayoutManager.getPosition(itemView);
+ }
+
+ private int getFirstItemOffset() {
+ if (mView.getChildCount() == 0) {
+ return RecyclerView.NO_POSITION;
+ }
+ View itemView = mView.getChildAt(0);
+ mView.getDecoratedBoundsWithMargins(itemView, mTempRect);
+ return mTempRect.top;
+ }
+
+ private void scrollToPositionWithOffset(int position, int offset) {
+ LinearLayoutManager linearLayoutManager = getVerticalLinearLayoutManager();
+ if (linearLayoutManager == null) {
+ return;
+ }
+ if (linearLayoutManager instanceof GridLayoutManager) {
+ GridLayoutManager gridLayoutManager = (GridLayoutManager) linearLayoutManager;
+ position *= gridLayoutManager.getSpanCount();
+ }
+ // LinearLayoutManager actually takes offset from paddingTop instead of top of RecyclerView.
+ offset -= mView.getPaddingTop();
+ linearLayoutManager.scrollToPositionWithOffset(position, offset);
+ }
+
+ @Nullable
+ private LinearLayoutManager getVerticalLinearLayoutManager() {
+ RecyclerView.LayoutManager layoutManager = mView.getLayoutManager();
+ if (!(layoutManager instanceof LinearLayoutManager)) {
+ return null;
+ }
+ LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
+ if (linearLayoutManager.getOrientation() != RecyclerView.VERTICAL) {
+ return null;
+ }
+ return linearLayoutManager;
+ }
+}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/SimpleViewHelper.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/SimpleViewHelper.java
new file mode 100644
index 0000000..2a8d01d
--- /dev/null
+++ b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/SimpleViewHelper.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (c) 2019 Hai Zhang
+ * All Rights Reserved.
+ */
+
+package me.zhanghai.android.fastscroll;
+
+import android.graphics.Canvas;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public abstract class SimpleViewHelper implements FastScroller.ViewHelper {
+
+ @Nullable
+ private Runnable mOnPreDrawListener;
+
+ @Nullable
+ private Runnable mOnScrollChangedListener;
+
+ @Nullable
+ private Predicate mOnTouchEventListener;
+ private boolean mListenerInterceptingTouchEvent;
+
+ @Override
+ public void addOnPreDrawListener(@Nullable Runnable listener) {
+ mOnPreDrawListener = listener;
+ }
+
+ public void draw(@NonNull Canvas canvas) {
+
+ if (mOnPreDrawListener != null) {
+ mOnPreDrawListener.run();
+ }
+
+ superDraw(canvas);
+ }
+
+ @Override
+ public void addOnScrollChangedListener(@Nullable Runnable listener) {
+ mOnScrollChangedListener = listener;
+ }
+
+ public void onScrollChanged(int left, int top, int oldLeft, int oldTop) {
+ superOnScrollChanged(left, top, oldLeft, oldTop);
+
+ if (mOnScrollChangedListener != null) {
+ mOnScrollChangedListener.run();
+ }
+ }
+
+ @Override
+ public void addOnTouchEventListener(@Nullable Predicate listener) {
+ mOnTouchEventListener = listener;
+ }
+
+ public boolean onInterceptTouchEvent(@NonNull MotionEvent event) {
+
+ if (mOnTouchEventListener != null && mOnTouchEventListener.test(event)) {
+
+ int actionMasked = event.getActionMasked();
+ if (actionMasked != MotionEvent.ACTION_UP
+ && actionMasked != MotionEvent.ACTION_CANCEL) {
+ mListenerInterceptingTouchEvent = true;
+ }
+
+ if (actionMasked != MotionEvent.ACTION_CANCEL) {
+ MotionEvent cancelEvent = MotionEvent.obtain(event);
+ cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
+ superOnInterceptTouchEvent(cancelEvent);
+ cancelEvent.recycle();
+ } else {
+ superOnInterceptTouchEvent(event);
+ }
+
+ return true;
+ }
+
+ return superOnInterceptTouchEvent(event);
+ }
+
+ public boolean onTouchEvent(@NonNull MotionEvent event) {
+
+ if (mOnTouchEventListener != null) {
+ if (mListenerInterceptingTouchEvent) {
+
+ mOnTouchEventListener.test(event);
+
+ int actionMasked = event.getActionMasked();
+ if (actionMasked == MotionEvent.ACTION_UP
+ || actionMasked == MotionEvent.ACTION_CANCEL) {
+ mListenerInterceptingTouchEvent = false;
+ }
+
+ return true;
+ } else {
+ int actionMasked = event.getActionMasked();
+ if (actionMasked != MotionEvent.ACTION_DOWN && mOnTouchEventListener.test(event)) {
+
+ if (actionMasked != MotionEvent.ACTION_UP
+ && actionMasked != MotionEvent.ACTION_CANCEL) {
+ mListenerInterceptingTouchEvent = true;
+ }
+
+ if (actionMasked != MotionEvent.ACTION_CANCEL) {
+ MotionEvent cancelEvent = MotionEvent.obtain(event);
+ cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
+ superOnTouchEvent(cancelEvent);
+ cancelEvent.recycle();
+ } else {
+ superOnTouchEvent(event);
+ }
+
+ return true;
+ }
+ }
+ }
+
+ return superOnTouchEvent(event);
+ }
+
+ @Override
+ public int getScrollRange() {
+ return computeVerticalScrollRange();
+ }
+
+ @Override
+ public int getScrollOffset() {
+ return computeVerticalScrollOffset();
+ }
+
+ @Override
+ public void scrollTo(int offset) {
+ scrollTo(getScrollX(), offset);
+ }
+
+ protected abstract void superDraw(@NonNull Canvas canvas);
+
+ protected abstract void superOnScrollChanged(int left, int top, int oldLeft, int oldTop);
+
+ protected abstract boolean superOnInterceptTouchEvent(@NonNull MotionEvent event);
+
+ protected abstract boolean superOnTouchEvent(@NonNull MotionEvent event);
+
+ protected abstract int computeVerticalScrollRange();
+
+ protected abstract int computeVerticalScrollOffset();
+
+ protected abstract int getScrollX();
+
+ protected abstract void scrollTo(int x, int y);
+}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/Utils.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/Utils.java
new file mode 100644
index 0000000..4ed5234
--- /dev/null
+++ b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/Utils.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * 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
+ *
+ * https://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 me.zhanghai.android.fastscroll;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.os.Build;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.content.res.AppCompatResources;
+import androidx.core.graphics.drawable.DrawableCompat;
+
+class Utils {
+
+ @ColorInt
+ public static int getColorFromAttrRes(@AttrRes int attrRes, @NonNull Context context) {
+ ColorStateList colorStateList = getColorStateListFromAttrRes(attrRes, context);
+ return colorStateList != null ? colorStateList.getDefaultColor() : 0;
+ }
+
+ @Nullable
+ public static ColorStateList getColorStateListFromAttrRes(@AttrRes int attrRes,
+ @NonNull Context context) {
+ TypedArray a = context.obtainStyledAttributes(new int[] { attrRes });
+ int resId;
+ try {
+ resId = a.getResourceId(0, 0);
+ if (resId != 0) {
+ return AppCompatResources.getColorStateList(context, resId);
+ }
+ return a.getColorStateList(0);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ // Work around the bug that GradientDrawable didn't actually implement tinting until
+ // Lollipop MR1 (API 22).
+ @Nullable
+ public static Drawable getGradientDrawableWithTintAttr(@DrawableRes int drawableRes,
+ @AttrRes int tintAttrRes,
+ @NonNull Context context) {
+ Drawable drawable = AppCompatResources.getDrawable(context, drawableRes);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1
+ && drawable instanceof GradientDrawable) {
+ drawable = DrawableCompat.wrap(drawable);
+ drawable.setTintList(getColorStateListFromAttrRes(tintAttrRes, context));
+ }
+ return drawable;
+ }
+}
diff --git a/fastscroll/src/main/java/me/zhanghai/android/fastscroll/ViewHelperProvider.java b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/ViewHelperProvider.java
new file mode 100644
index 0000000..9bf2d30
--- /dev/null
+++ b/fastscroll/src/main/java/me/zhanghai/android/fastscroll/ViewHelperProvider.java
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2019 Hai Zhang
+ * All Rights Reserved.
+ */
+
+package me.zhanghai.android.fastscroll;
+
+import androidx.annotation.NonNull;
+
+public interface ViewHelperProvider {
+
+ @NonNull
+ FastScroller.ViewHelper getViewHelper();
+}
diff --git a/fastscroll/src/main/res/drawable/afs_md2_thumb.xml b/fastscroll/src/main/res/drawable/afs_md2_thumb.xml
new file mode 100644
index 0000000..f45fa91
--- /dev/null
+++ b/fastscroll/src/main/res/drawable/afs_md2_thumb.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fastscroll/src/main/res/drawable/afs_md2_track.xml b/fastscroll/src/main/res/drawable/afs_md2_track.xml
new file mode 100644
index 0000000..02f03a5
--- /dev/null
+++ b/fastscroll/src/main/res/drawable/afs_md2_track.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fastscroll/src/main/res/drawable/afs_popup_background.xml b/fastscroll/src/main/res/drawable/afs_popup_background.xml
new file mode 100644
index 0000000..f0a8c43
--- /dev/null
+++ b/fastscroll/src/main/res/drawable/afs_popup_background.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fastscroll/src/main/res/drawable/afs_thumb.xml b/fastscroll/src/main/res/drawable/afs_thumb.xml
new file mode 100644
index 0000000..23565a8
--- /dev/null
+++ b/fastscroll/src/main/res/drawable/afs_thumb.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fastscroll/src/main/res/drawable/afs_thumb_stateful.xml b/fastscroll/src/main/res/drawable/afs_thumb_stateful.xml
new file mode 100644
index 0000000..c71f527
--- /dev/null
+++ b/fastscroll/src/main/res/drawable/afs_thumb_stateful.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
diff --git a/fastscroll/src/main/res/drawable/afs_track.xml b/fastscroll/src/main/res/drawable/afs_track.xml
new file mode 100644
index 0000000..e858cb5
--- /dev/null
+++ b/fastscroll/src/main/res/drawable/afs_track.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fastscroll/src/main/res/values/dimens.xml b/fastscroll/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..73e24af
--- /dev/null
+++ b/fastscroll/src/main/res/values/dimens.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ 48dp
+
+ 88dp
+ 16dp
+ 45dp
+
+ 78dp
+ 64dp
+ 14dp
+ 16dp
+ 29dp
+ 3dp
+ 34dp
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3961d71..f5cde0e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -65,4 +65,5 @@ androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "j
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin-android" }
-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
\ No newline at end of file
+ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
+android-library = { id = "com.android.library", version.ref = "agp" }
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index d600cc5..640ece8 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -15,3 +15,4 @@ dependencyResolutionManagement {
rootProject.name = "ToDo"
include(":app")
+include(":fastscroll")