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")