Use official FastScroll fix and refactor PasswordFragment (#753)

This commit is contained in:
Fabian Henneke 2020-04-29 08:53:27 +02:00 committed by GitHub
parent edc6dcda88
commit c41100eff9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 8 additions and 354 deletions

View file

@ -1,341 +0,0 @@
/*
* 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);
}
};
}
}
}

View file

@ -19,9 +19,7 @@ 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.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.databinding.PasswordRecyclerViewBinding import com.zeapo.pwdstore.databinding.PasswordRecyclerViewBinding
import com.zeapo.pwdstore.git.BaseGitActivity import com.zeapo.pwdstore.git.BaseGitActivity
@ -38,9 +36,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: FixOnItemTouchDispatchRecyclerView
private lateinit var listener: OnFragmentInteractionListener private lateinit var listener: OnFragmentInteractionListener
private lateinit var swipeRefresher: SwipeRefreshLayout
private var recyclerViewStateToRestore: Parcelable? = null private var recyclerViewStateToRestore: Parcelable? = null
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
@ -67,8 +63,7 @@ class PasswordFragment : Fragment() {
} }
private fun initializePasswordList() { private fun initializePasswordList() {
swipeRefresher = binding.swipeRefresher binding.swipeRefresher.setOnRefreshListener {
swipeRefresher.setOnRefreshListener {
if (!PasswordRepository.isGitRepo()) { if (!PasswordRepository.isGitRepo()) {
Snackbar.make(binding.root, getString(R.string.clone_git_repo), Snackbar.LENGTH_INDEFINITE) Snackbar.make(binding.root, getString(R.string.clone_git_repo), Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.clone_button) { .setAction(R.string.clone_button) {
@ -77,7 +72,7 @@ class PasswordFragment : Fragment() {
startActivityForResult(intent, BaseGitActivity.REQUEST_CLONE) startActivityForResult(intent, BaseGitActivity.REQUEST_CLONE)
} }
.show() .show()
swipeRefresher.isRefreshing = false binding.swipeRefresher.isRefreshing = false
} else { } else {
val intent = Intent(context, GitOperationActivity::class.java) val intent = Intent(context, GitOperationActivity::class.java)
intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_SYNC) intent.putExtra(BaseGitActivity.REQUEST_ARG_OP, BaseGitActivity.REQUEST_SYNC)
@ -92,7 +87,7 @@ class PasswordFragment : Fragment() {
.onSelectionChanged { selection -> .onSelectionChanged { selection ->
// In order to not interfere with drag selection, we disable the SwipeRefreshLayout // In order to not interfere with drag selection, we disable the SwipeRefreshLayout
// once an item is selected. // once an item is selected.
swipeRefresher.isEnabled = selection.isEmpty binding.swipeRefresher.isEnabled = selection.isEmpty
if (actionMode == null) if (actionMode == null)
actionMode = requireStore().startSupportActionMode(actionModeCallback) actionMode = requireStore().startSupportActionMode(actionModeCallback)
@ -105,7 +100,7 @@ class PasswordFragment : Fragment() {
actionMode!!.finish() actionMode!!.finish()
} }
} }
recyclerView = binding.passRecycler val recyclerView = binding.passRecycler
recyclerView.apply { recyclerView.apply {
layoutManager = LinearLayoutManager(requireContext()) layoutManager = LinearLayoutManager(requireContext())
itemAnimator = OnOffItemAnimator() itemAnimator = OnOffItemAnimator()
@ -232,7 +227,7 @@ class PasswordFragment : Fragment() {
requireStore().clearSearch() requireStore().clearSearch()
model.navigateTo( model.navigateTo(
item.file, item.file,
recyclerViewState = recyclerView.layoutManager!!.onSaveInstanceState() recyclerViewState = binding.passRecycler.layoutManager!!.onSaveInstanceState()
) )
requireStore().supportActionBar?.setDisplayHomeAsUpEnabled(true) requireStore().supportActionBar?.setDisplayHomeAsUpEnabled(true)
} else { } else {
@ -250,7 +245,7 @@ class PasswordFragment : Fragment() {
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
swipeRefresher.isRefreshing = false binding.swipeRefresher.isRefreshing = false
} }
/** /**

View file

@ -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.FixOnItemTouchDispatchRecyclerView <me.zhanghai.android.fastscroll.FixOnItemTouchListenerRecyclerView
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"

View file

@ -48,7 +48,7 @@ ext.deps = [
third_party: [ third_party: [
commons_io: 'commons-io:commons-io:2.5', commons_io: 'commons-io:commons-io:2.5',
commons_codec: 'commons-codec:commons-codec:1.13', commons_codec: 'commons-codec:commons-codec:1.13',
fastscroll: 'me.zhanghai.android.fastscroll:library:1.1.2', fastscroll: 'me.zhanghai.android.fastscroll:library:1.1.3',
jsch: 'com.jcraft:jsch:0.1.55', jsch: 'com.jcraft:jsch:0.1.55',
jgit: 'org.eclipse.jgit:org.eclipse.jgit:3.7.1.201504261725-r', jgit: 'org.eclipse.jgit:org.eclipse.jgit:3.7.1.201504261725-r',
leakcanary: 'com.squareup.leakcanary:leakcanary-android:2.2', leakcanary: 'com.squareup.leakcanary:leakcanary-android:2.2',