mirror of
https://github.com/android-password-store/Android-Password-Store
synced 2025-08-30 13:57:47 +00:00
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>
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -17,8 +17,8 @@ import androidx.appcompat.view.ActionMode
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.observe
|
import androidx.lifecycle.observe
|
||||||
|
import androidx.recyclerview.widget.FixOnItemTouchDispatchRecyclerView
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
@@ -35,7 +35,7 @@ import me.zhanghai.android.fastscroll.FastScrollerBuilder
|
|||||||
|
|
||||||
class PasswordFragment : Fragment() {
|
class PasswordFragment : Fragment() {
|
||||||
private lateinit var recyclerAdapter: PasswordItemRecyclerAdapter
|
private lateinit var recyclerAdapter: PasswordItemRecyclerAdapter
|
||||||
private lateinit var recyclerView: RecyclerView
|
private lateinit var recyclerView: FixOnItemTouchDispatchRecyclerView
|
||||||
private lateinit var listener: OnFragmentInteractionListener
|
private lateinit var listener: OnFragmentInteractionListener
|
||||||
private lateinit var swipeRefresher: SwipeRefreshLayout
|
private lateinit var swipeRefresher: SwipeRefreshLayout
|
||||||
|
|
||||||
@@ -111,9 +111,6 @@ class PasswordFragment : Fragment() {
|
|||||||
adapter = recyclerAdapter
|
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()
|
FastScrollerBuilder(recyclerView).build()
|
||||||
recyclerAdapter.makeSelectable(recyclerView)
|
recyclerAdapter.makeSelectable(recyclerView)
|
||||||
registerForContextMenu(recyclerView)
|
registerForContextMenu(recyclerView)
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.FixOnItemTouchDispatchRecyclerView
|
||||||
android:id="@+id/pass_recycler"
|
android:id="@+id/pass_recycler"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
@@ -13,7 +13,7 @@ spotless {
|
|||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
target '**/src/**/*.java'
|
target '**/src/**/com/zeapo/pwdstore/*.java'
|
||||||
trimTrailingWhitespace()
|
trimTrailingWhitespace()
|
||||||
licenseHeaderFile rootProject.file('spotless.license')
|
licenseHeaderFile rootProject.file('spotless.license')
|
||||||
removeUnusedImports()
|
removeUnusedImports()
|
||||||
|
Reference in New Issue
Block a user