summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFabian Henneke <FabianHenneke@users.noreply.github.com>2020-04-19 10:40:49 +0200
committerGitHub <noreply@github.com>2020-04-19 10:40:49 +0200
commite5d178ea3ca29367593bfc92e8f95e17dcaba459 (patch)
treeb08c2c7d4d12a492eb5197cc707cbf4a5387ce71
parent934c256edded0ecd5229a39d2581a0f1709fc075 (diff)
Work around incompatibility between AndroidFastScroll and recyclerview-selection (#721)
* Work around incompatibility between AndroidFastScroll and recyclerview-selection * move hacked recyclerview into separate package Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> * Make RecyclerViewHelper private static Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
-rw-r--r--app/src/main/java/androidx/recyclerview/widget/FixOnItemTouchDispatchRecyclerView.java341
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt7
-rw-r--r--app/src/main/res/layout/password_recycler_view.xml2
-rw-r--r--spotless.gradle2
4 files changed, 345 insertions, 7 deletions
diff --git a/app/src/main/java/androidx/recyclerview/widget/FixOnItemTouchDispatchRecyclerView.java b/app/src/main/java/androidx/recyclerview/widget/FixOnItemTouchDispatchRecyclerView.java
new file mode 100644
index 00000000..58590a66
--- /dev/null
+++ b/app/src/main/java/androidx/recyclerview/widget/FixOnItemTouchDispatchRecyclerView.java
@@ -0,0 +1,341 @@
+/*
+ * 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 androidx.recyclerview.widget;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import me.zhanghai.android.fastscroll.FastScroller;
+import me.zhanghai.android.fastscroll.PopupTextProvider;
+import me.zhanghai.android.fastscroll.Predicate;
+import me.zhanghai.android.fastscroll.ViewHelperProvider;
+
+public class FixOnItemTouchDispatchRecyclerView extends RecyclerView implements ViewHelperProvider {
+
+ @NonNull
+ private final ViewHelper mViewHelper = new ViewHelper(this);
+
+ @Nullable
+ private OnItemTouchListener mPhantomOnItemTouchListener = null;
+ private OnItemTouchListener mInterceptingOnItemTouchListener = null;
+
+ public FixOnItemTouchDispatchRecyclerView(@NonNull Context context) {
+ super(context);
+ }
+
+ public FixOnItemTouchDispatchRecyclerView(@NonNull Context context,
+ @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public FixOnItemTouchDispatchRecyclerView(@NonNull Context context,
+ @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @NonNull
+ @Override
+ public FastScroller.ViewHelper getViewHelper() {
+ return mViewHelper;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent e) {
+ mInterceptingOnItemTouchListener = null;
+ if (findInterceptingOnItemTouchListener(e)) {
+ cancelScroll();
+ return true;
+ }
+ return super.onInterceptTouchEvent(e);
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public boolean onTouchEvent(MotionEvent e) {
+ if (dispatchOnItemTouchListeners(e)) {
+ cancelScroll();
+ return true;
+ }
+ return super.onTouchEvent(e);
+ }
+
+ @Override
+ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ if (mPhantomOnItemTouchListener != null) {
+ mPhantomOnItemTouchListener.onRequestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+ super.requestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+
+ private void cancelScroll() {
+ MotionEvent syntheticCancel = MotionEvent.obtain(
+ 0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0);
+ super.onInterceptTouchEvent(syntheticCancel);
+ syntheticCancel.recycle();
+ }
+
+ private boolean dispatchOnItemTouchListeners(@NonNull MotionEvent e) {
+ if (mInterceptingOnItemTouchListener == null) {
+ if (e.getAction() == MotionEvent.ACTION_DOWN) {
+ return false;
+ }
+ return findInterceptingOnItemTouchListener(e);
+ } else {
+ mInterceptingOnItemTouchListener.onTouchEvent(this, e);
+ final int action = e.getAction();
+ if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+ mInterceptingOnItemTouchListener = null;
+ }
+ return true;
+ }
+ }
+
+ private boolean findInterceptingOnItemTouchListener(@NonNull MotionEvent e) {
+ int action = e.getAction();
+ if (mPhantomOnItemTouchListener != null
+ && mPhantomOnItemTouchListener.onInterceptTouchEvent(this, e)
+ && action != MotionEvent.ACTION_CANCEL) {
+ mInterceptingOnItemTouchListener = mPhantomOnItemTouchListener;
+ return true;
+ }
+ return false;
+ }
+
+ private static class RecyclerViewHelper implements FastScroller.ViewHelper {
+
+ @NonNull
+ private final RecyclerView mView;
+
+ @NonNull
+ private final Rect mTempRect = new Rect();
+
+ public RecyclerViewHelper(@NonNull RecyclerView view) {
+ mView = view;
+ }
+
+ @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<MotionEvent> 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 String getPopupText() {
+ RecyclerView.Adapter<?> adapter = mView.getAdapter();
+ if (!(adapter instanceof PopupTextProvider)) {
+ return null;
+ }
+ PopupTextProvider popupTextProvider = (PopupTextProvider) adapter;
+ int position = getFirstItemAdapterPosition();
+ if (position == RecyclerView.NO_POSITION) {
+ return null;
+ }
+ return popupTextProvider.getPopupText(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;
+ }
+ }
+
+ private class ViewHelper extends RecyclerViewHelper {
+
+ ViewHelper(@NonNull RecyclerView view) {
+ super(view);
+ }
+
+ @Override
+ public void addOnTouchEventListener(@NonNull Predicate<MotionEvent> onTouchEvent) {
+ mPhantomOnItemTouchListener = 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);
+ }
+ };
+ }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt
index c9b9824f..eb97f1b8 100644
--- a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt
@@ -17,8 +17,8 @@ import androidx.appcompat.view.ActionMode
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.observe
+import androidx.recyclerview.widget.FixOnItemTouchDispatchRecyclerView
import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
@@ -35,7 +35,7 @@ import me.zhanghai.android.fastscroll.FastScrollerBuilder
class PasswordFragment : Fragment() {
private lateinit var recyclerAdapter: PasswordItemRecyclerAdapter
- private lateinit var recyclerView: RecyclerView
+ private lateinit var recyclerView: FixOnItemTouchDispatchRecyclerView
private lateinit var listener: OnFragmentInteractionListener
private lateinit var swipeRefresher: SwipeRefreshLayout
@@ -111,9 +111,6 @@ class PasswordFragment : Fragment() {
adapter = recyclerAdapter
}
- // FastScrollerBuilder.build() needs to be called *before* recyclerAdapter.makeSelectable(),
- // as otherwise dragging the fast scroller will lead to items being selected.
- // See https://github.com/zhanghai/AndroidFastScroll/issues/13
FastScrollerBuilder(recyclerView).build()
recyclerAdapter.makeSelectable(recyclerView)
registerForContextMenu(recyclerView)
diff --git a/app/src/main/res/layout/password_recycler_view.xml b/app/src/main/res/layout/password_recycler_view.xml
index 5368d593..8348bea2 100644
--- a/app/src/main/res/layout/password_recycler_view.xml
+++ b/app/src/main/res/layout/password_recycler_view.xml
@@ -15,7 +15,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <androidx.recyclerview.widget.RecyclerView
+ <androidx.recyclerview.widget.FixOnItemTouchDispatchRecyclerView
android:id="@+id/pass_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
diff --git a/spotless.gradle b/spotless.gradle
index 223c5a10..10d5e620 100644
--- a/spotless.gradle
+++ b/spotless.gradle
@@ -13,7 +13,7 @@ spotless {
}
java {
- target '**/src/**/*.java'
+ target '**/src/**/com/zeapo/pwdstore/*.java'
trimTrailingWhitespace()
licenseHeaderFile rootProject.file('spotless.license')
removeUnusedImports()