Trigger click and drag actions from item in RecyclerView - java

I have a vertically scrolling RecyclerView and I want to allow items on it to be clicked and reordered.
Each item in the RecyclerView has a "StatusIndicator" on the left hand side (inheriting from an AppCompatImageButton). I've added a GestureDetector to the StatusIndicator and this seems to work except that the vertical drag doesn't get sent to the StatusIndicator.
The behaviour I see is that if I attempt vertical scrolling on the StatusIndicator (intending to trigger a drag) neither the drag is triggered nor the RecyclerView is scrolled. If I continue the scrolling gesture but scroll horizontally suddenly I can drag the item.
I'm assuming the ACTION_DOWN event reaches the StatusIndicator's GestureDetector to trigger the drag but that the RecyclerView captures the following (predominantly vertical) ACTION_MOVE assuming it needs to handle them for it's vertical scroll. Once the gesture involves more horizontal than vertical movement RecyclerView::onInterceptTouchEvent no longer captures the event (confirmed in debug) and the ACTION_MOVEs are then sent to the StatusIndicator as intended.
What is the correct approach here?
The relevant bits of StatusIndicator.java:
class StatusIndicator extends AppCompatImageButton {
class GestureTouch extends GestureDetector.SimpleOnGestureListener {
#Override
public boolean onDown(MotionEvent event) {
return true;
}
#Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (mStatusIndicatorListener != null)
mStatusIndicatorListener.incrementStatus();
createDrawableState();
return true;
}
#Override
public boolean onScroll (MotionEvent e1,
MotionEvent e2,
float distanceX,
float distanceY) {
mStatusIndicatorListener.startDrag();
return true;
}
}
private StatusIndicatorListener mStatusIndicatorListener;
private GestureDetector mTouchDetector;
public StatusIndicator(Context context, AttributeSet attrs) {
super(context, attrs);
mTouchDetector = new GestureDetector(context,new GestureTouch());
}
#Override
public boolean onTouchEvent(MotionEvent event) {
mTouchDetector.onTouchEvent(event);
return true;
}

I believe what was happenning was that any ACTION_MOVE event was heading down the stack and being captured by the RecyclerView within its onInterceptTouchEvent method. Within this RecyclerView.LayoutManager.canScrollVertically() is used to determine that the RecyclerView should capture the event.
In the end I inherited from LinearLayoutManager and overrode canScrollVertically() so I could manually block it with a call to a new method. Then, within RecyclerView.onInterceptTouchEvent, I checked whether the incoming ACTION_DOWN event was over the StatusIndicator and, if so, forced RecyclerView.LayoutManater.canScrollVertically() to return false until the next ACTION_DOWN event.
This is a pretty hacky solution but appears robust. If anyone comes along with a "proper" solution I'd be keen to see it!
The new layout manager:
public class ListLayoutManager extends LinearLayoutManager {
boolean mScrollBlocked;
public ListLayoutManager(Context context) {
super(context);
mScrollBlocked = false;
}
#Override
public boolean canScrollVertically() {
return (!mScrollBlocked && super.canScrollVertically());
}
public void blockScrolling() {
mScrollBlocked = true;
}
public void unblockScrolling() {
mScrollBlocked = false;
}
}
The extended RecyclerView with overridden onInterceptTouchEvent:
ListLayoutManager mListLayoutManager;
public ListRecyclerView(Context context) {
super(context);
}
public ListRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ListRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// We override touch handling so the RecyclerView doesn't handle
// anything which occurs over the StatusIndicator.
#Override
public boolean onInterceptTouchEvent(MotionEvent e) {
if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
mListLayoutManager.unblockScrolling();
View child = findChildViewUnder(e.getX(), e.getY());
if (child != null) {
StatusIndicator statusInd = child.findViewById(R.id.rowStatus);
if ((statusInd != null) &&
(e.getX() <= statusInd.getWidth())) {
mListLayoutManager.blockScrolling();
}
}
}
return super.onInterceptTouchEvent(e);
}
public void setListLayoutManager(ListLayoutManager lm) {
mListLayoutManager = lm;
setLayoutManager(mListLayoutManager);
}
}

Related

What is the best way to automatically scroll to the top of a recycler view after hitting the bottom?

I'm creating an android app that displays a recyclerview of cards. I have implemented an auto-scroll feature as part of my linear layout manager that will smoothscroll to the last element in the recyclerview.
I would like the list to snap back to the top of the recyclerview once it has detected that the last element is visible.
Here is the code for my custom layout manager.
public class ScrollLayoutManager extends LinearLayoutManager {
private static final float MILLISECONDS_PER_INCH = 10000f; //default is 25f (bigger = slower)
public ScrollLayoutManager(Context context) {
super(context);
}
public ScrollLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public ScrollLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
final LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) {
#Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return super.computeScrollVectorForPosition(targetPosition);
}
#Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);
}
}
You might want attach a RecyclerView.OnScrollListener (docs) to your RecyclerView and override onScrolled() and use either findLastCompletelyVisibleItemPosition() (docs) or findLastVisibleItemPosition() (docs), depending on your needs, in combination with getItemCount() (docs) to check if you hit the last item and then scroll back to the top.

Android Swipe Gesture that parent Activity can not intercept

I worked on android swipe gestures, i applied swipe gesture on Item inside recycler view and recycler view is inside fragment and then fragment is inside activity and activity has Scroll View, The problem was that parent activity was intercepting swipe gesture of item inside child recycler view, After so much hard i found solution and this code is working fine, Is there any other better solution to this problem? if yes then post it in answer otherwise vote my code so that i can use it in my app without any fear of failure. Here is my code
listingView.setOnTouchListener(new OnSwipeTouchListener(myContext)
{
public void onClick() {
ViewListing();
}
// #Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getActionMasked())
{
case MotionEvent.ACTION_DOWN:
x1 = event.getX();
break;
case MotionEvent.ACTION_UP:
LockableScrollView.setScrollingEnabled(true);
break;
case MotionEvent.ACTION_MOVE:
x2 = event.getX();
float deltaX = x2 - x1;
if (Math.abs(deltaX) > 20)
{
LockableScrollView.setScrollingEnabled(false);
// Left to Right swipe action
if (x2 > x1)
{
hideSidePanel();
LockableScrollView.setScrollingEnabled(true);
}
else
//MH: Right to Left Swipe
{
showSidePanel();
LockableScrollView.setScrollingEnabled(true);
}
}
break;
}
return super.onTouch(v, event);
}
});
Lockable Scroll View code
public class LockableScrollView extends ScrollView {
#Override
public void setClipChildren(boolean clipChildren) {
clipChildren=false;
super.setClipChildren(clipChildren);
}
#Override
public void setFocusable(int focusable) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
focusable=View.NOT_FOCUSABLE;
}
super.setFocusable(focusable);
}
// true if we can scroll (not locked)
// false if we cannot scroll (locked)
private static boolean mScrollable = true;
public LockableScrollView(Context context) {
super(context);
}
public LockableScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LockableScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public LockableScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public static void setScrollingEnabled(boolean enabled) {
mScrollable = enabled;
}
public static boolean isScrollable() {
return mScrollable;
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//MH: if we can scroll pass the event to the superclass
if (mScrollable) return super.onTouchEvent(ev);
//MH: only continue to handle the touch event if scrolling enabled
return mScrollable; // mScrollable is always false at this point
default:
return super.onTouchEvent(ev);
}
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//MH: Don't do anything with intercepted touch events if
//MH: we are not scrollable
if (!mScrollable) return false;
else return super.onInterceptTouchEvent(ev);
}
}

GestureDetector detect DoubleClick on GridView items

Note: This specific problem is solved, but there are serious follow-up problems. Have a look at GestureDetector - Detect double click in GridView item's although returning false in onTouchEvent()
I want to detect double clicks on distinct items in a GridView of images.
Therefore I assigned a separate OnTouchListener to each item-imageView in the getView() method of the adapter. The gestureDetector is a member variable of the adapter-class.
private GestureDetectorCompat gestureDetector;
public ImageGridViewAdapter(Context c, ArrayList<UriWrapper> startUpImages) {
mContext = c;
uriManager = new UriManager(startUpImages);
gestureDetector = new GestureDetectorCompat(mContext, new SingleTapConfirm());
}
public View getView(final int position, View recycled, ViewGroup parent) {
ViewHolder holder;
if (recycled == null) {
..... find items by id
} else{
holder = (ViewHolder) recycled.getTag();
}
// Set listener to item image
holder.image.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
// Always returns false, the gestureDetector does not detect anything
boolean ret = gestureDetector.onTouchEvent(event);
// At least the onTouch-callback gets called with the correct position
Log.e(TAG, "onTouch returned " + ret + " at position " + position);
return true;
}
});
// Use glide library to load images into the image views
Glide.with(mContext)....into(holder.image);
return recycled;
}
private class SingleTapConfirm extends GestureDetector.SimpleOnGestureListener {
#Override
public boolean onSingleTapConfirmed(MotionEvent event) {
Log.e(TAG, "onSingleTapConfirmed"); // never called..
return true;
}
#Override
public boolean onDoubleTap(MotionEvent e) {
Log.e(TAG, "onDoubleTap"); // never called..
return super.onDoubleTap(e);
}
}
The OnTouchListener's work and get called with the correct position.
However, no matter what I am doing, the methods of the GestureDetector are never called. What seems to be the issue with this code?
Update: The onTouch-callback needs to return true, now at least the GestureDetector works. However, returning true breaks the rest of the functionality, since I have a long-click-selection-mode and a global OnTouchListener for my GridView.
Second Update:
Merging the item-specific OnTouchListener and the global OnTouchListener did not work properly. (swipe gestures only recognized on certain items)
I hope that I can work around these two problems by creating a custom View extending ImageView and assigning the item-specific OnTouchListener there.
private GestureDetectorCompat gestureDetector;
// in your adapter constructor
gestureDetector = new GestureDetector(context, new SingleTapConfirm());
public View getView(final int position, View recycled, ViewGroup parent) {
ViewHolder holder;
if (recycled == null) {
.....find items by id
} else {
holder = (ViewHolder) recycled.getTag();
}
// Set listener to item image
holder.image.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
// Use lazy initialization for the gestureDetector
gestureDetector.onTouchEvent(event);
// At least the onTouch-callback gets called with the correct position
return true;
}
});
// Use glide library to load images into the image views
Glide.with(mContext)....into(holder.image);
return recycled;
}
private class SingleTapConfirm extends GestureDetector.SimpleOnGestureListener {
#Override
public boolean onSingleTapConfirmed(MotionEvent event) {
Log.e(TAG, "onSingleTapConfirmed"); // never called..
return true;
}
#Override
public boolean onDoubleTap(MotionEvent e) {
Log.e(TAG, "onDoubleTap"); // never called..
return super.onDoubleTap(e);
}
}
Update:
#Override
public boolean onTouch(View v, MotionEvent event) {anything
gestureDetector.onTouchEvent(event);
return true;
}

NestedScrollView not fling with Recyclerview inside

I have a layout like that:
<NestedScrollView>
<RecyclerView> // vertical recycler view
<RecyclerView/> // horizontal recycler view
<RecyclerView/>
<RecyclerView/>
...
<RecyclerView>
</NestedScrollView>
The result looks like Google play store:
And I disabled NestedScrolling in horizontal Recycler view:
horizontalRecyclerView.setHasFixedSize(true);
horizontalRecyclerView.setNestedScrollingEnabled(false);
My problem:
The vertical recyclerview does not scroll fling, whenever ACTION_UP happen, the vertical recyclerview also stop scrolling.
How can I nest vertical recyclerview inside nestedscrollview, and horizontal recyclerview inside vertical recyclerview like Playstore and keep the scroll smooth.
Solved:
Using custom nested scroll view of #vrund purohit (code below), and disabled nestedscroll both vertical and horizontal recyclerview:
verticalRecyclerView.setNestedScrollingEnabled(false);
... add each horizontal recyclerviews:
horizontalRecyclerView.setNestedScrollingEnabled(false);
Use below code for smooth scroll:
ViewCompat.setNestedScrollingEnabled(recyclerView, false);
Add this in your RecyclerView xml:
android:nestedScrollingEnabled="false"
I had this same problem and I solved this issue by customizing NeatedScrollView.
Here is the class for that.
MyNestedScrollView
public class MyNestedScrollView extends NestedScrollView {
#SuppressWarnings("unused")
private int slop;
#SuppressWarnings("unused")
private float mInitialMotionX;
#SuppressWarnings("unused")
private float mInitialMotionY;
public MyNestedScrollView(Context context) {
super(context);
init(context);
}
private void init(Context context) {
ViewConfiguration config = ViewConfiguration.get(context);
slop = config.getScaledEdgeSlop();
}
public MyNestedScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public MyNestedScrollView(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private float xDistance, yDistance, lastX, lastY;
#SuppressWarnings("unused")
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final float x = ev.getX();
final float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
xDistance = yDistance = 0f;
lastX = ev.getX();
lastY = ev.getY();
// This is very important line that fixes
computeScroll();
break;
case MotionEvent.ACTION_MOVE:
final float curX = ev.getX();
final float curY = ev.getY();
xDistance += Math.abs(curX - lastX);
yDistance += Math.abs(curY - lastY);
lastX = curX;
lastY = curY;
if (xDistance > yDistance) {
return false;
}
}
return super.onInterceptTouchEvent(ev);
}
public interface OnScrollChangedListener {
void onScrollChanged(NestedScrollView who, int l, int t, int oldl,
int oldt);
}
private OnScrollChangedListener mOnScrollChangedListener;
public void setOnScrollChangedListener(OnScrollChangedListener listener) {
mOnScrollChangedListener = listener;
}
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (mOnScrollChangedListener != null) {
mOnScrollChangedListener.onScrollChanged(this, l, t, oldl, oldt);
}
}
}
Happy coding.
[RESOLVED] I have same issue with Horizontal recycleview. Change Gradle repo for recycleview
compile 'com.android.support:recyclerview-v7:23.2.1'
Write this: linearLayoutManager.setAutoMeasureEnabled(true);
Fixed bugs related to various measure-spec methods in update
Check http://developer.android.com/intl/es/tools/support-library/features.html#v7-recyclerview
I have found issue with 23.2.1 library: When item is match_parent recycle view fill full item to view, please always go with min height or "wrap_content".
Thanks
I've solved the issue by using below code:
myRecyclerView.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false){
#Override
public boolean canScrollHorizontally() {
return true;
}
#Override
public boolean canScrollVertically() {
return true;
}
});

View Drag Helper helper

I am trying to get the ViewDragHelper to work with a vertical LinearLayout, - Container - which has
A ---- List View
B ---- horizontal linear layout
C ---- Support Map Fragment
as children views.
A has layout_height="0dp". Pulling down on B, should proportionally increase the height of A, there by revealing the contents on the ListView, thereby automatically re positioning B and resizing C in the process.
The following is the code for the Container LinearLayout.
public class MyLinearLayout extends LinearLayout {
private LinearLayout headerLayout;
private ListView filtersListView;
private ViewDragHelper viewDragHelper;
private int MARGIN;
public MyLinearLayout (Context context) {
super(context);
init();
}
public MyLinearLayout (Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MyLinearLayout (Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
viewDragHelper = ViewDragHelper.create(this, 1, new DragHelperCallback());
float GESTURE_THRESHOLD_DP = 10.0f;
float scale = getResources().getDisplayMetrics().density;
MARGIN = (int) (GESTURE_THRESHOLD_DP * scale + 0.5f);
}
#Override
protected void onFinishInflate() {
Log.i("Places", "onFinishInflate");
super.onFinishInflate();
headerLayout = (LinearLayout) findViewById(R.id.header);
filtersListView = (ListView) findViewById(R.id.filters_list);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("Places", "onInterceptTouchEvent");
return viewDragHelper.shouldInterceptTouchEvent(ev);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("Places", "onTouchEvent");
viewDragHelper.processTouchEvent(event);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, (int) event.getRawY());
layoutParams.setMargins(MARGIN, MARGIN, MARGIN, MARGIN);
filtersListView.setLayoutParams(layoutParams);
return true;
}
private class DragHelperCallback extends ViewDragHelper.Callback {
#Override
public boolean tryCaptureView(View child, int pointerId) {
Log.i("Places", "tryCaptureView " + (child == headerLayout));
return child == headerLayout;
}
#Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
Log.i("Places", "onViewPositionChanged " + changedView.getClass().getName());
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
#Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
Log.i("Places", "onViewReleased " + releasedChild.getClass().getName());
super.onViewReleased(releasedChild, xvel, yvel);
}
}
}
I have looked at the the following.
ViewDragHelper: how to use it?
Your ViewDragHelper is a bit off. Specifically you're missing the clampViewPositionVertical() override, which is required to enable dragging of the view at all. Right now, your ViewDragHelper is actually doing no work. You are probably getting some motion because you are manipulating the LayoutParams directly for every onTouchEvent(), which will also cause some problems.
You likely want your callback code to look more like this:
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return viewDragHelper.shouldInterceptTouchEvent(ev);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
viewDragHelper.processTouchEvent(event);
return true;
}
private class DragHelperCallback extends ViewDragHelper.Callback {
#Override
public int clampViewPositionVertical(View child, int top, int dy) {
//Never go below fully visible
final int bottomBound = getHeight() - child.getHeight();
//Never go above the top
final int topBound = 0;
return Math.max(Math.min(top, bottomBound), topBound);
}
#Override
public boolean tryCaptureView(View child, int pointerId) {
//Capture touches to the header view
return child == headerLayout;
}
#Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
if (dy == 0) return;
//React to the position change of the header by modifying layout
LinearLayout.LayoutParams layoutParams = (LayoutParams) filtersListView.getLayoutParams();
layoutParams.height += dy;
filtersListView.setLayoutParams(layoutParams);
}
}
This sets the drag bounds of your "header view" to move freely between the top of the container view and the bottom (clamping it so it doesn't go off-screen). By itself, this will only move the header view. Moving your LayoutParams code into onViewPositionChanged() lets your top/bottom views react to the exact drag distance.
Two Warnings:
Don't add margins to your LayoutParams. The way you are using them to constantly modify proportions will cause the dragging to jump. If you need to inset the ListView content, use padding or another nested container.
Changing the layout of an entire view continuously like this can be very expensive (layout passes are not cheap, and you trigger one every time you modify LayoutParams). This may work for a very simple layout, but if your view gets more complex you will likely see performance suffer on older hardware.

Categories

Resources