Vertical and horizontal RecyclerViews with PagerSnapHelper - android

I'm trying to figure out what's the best way to implement two lists, where the parent list can be swiped vertically and the children can be swiped horizontally.
Parent list is assumed to be an infinite list, while the children can have at most n pages.
RecyclerView appears to be the best tool for the job and given the addition of PagerSnapHelper, it's really easy to recreate a page swipe behavior (think ViewPager.)
The current problem I'm facing is that when the we horizontally swipe all the way to the end or the beginning, sometimes the vertical recyclerview (parent) takes over and changes page.
This can be recreated even with a single vertical recyclerview, as follows:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRecyclerView = (RecyclerView) findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
PagerSnapHelper snapHelper = new PagerSnapHelper();
SearchAdapter searchAdapter = new SearchAdapter();
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setAdapter(searchAdapter);
snapHelper.attachToRecyclerView(mRecyclerView);
}
A top to bottom or bottom to top swipe/fling will change pages as expected. However, if you do a horizontal motion (left to right or right to left) sometimes the vertical swipe kicks in.
It seems like PagerSnapHelper is very sensitive to fast movements. Is there any way to avoid this page changes when a swipe is initiated horizontally instead of vertically? This issue is more noticeable in my case since I have a horizontal pagersnaphelper as well.
Possible solutions:
Extend RecyclerView an control onTouchEvent and OnInterceptTouchEvent. Haven't figured out a way to use this.
Use GestureDetector
I'm happy to provide more context/code if needed.
Having two PagerSnapHelper may not be a current pattern in Android, but even if I take that part away, I wonder why PagerSnapHelper is so sensitive to some gestures.

I figured the issue when having to PagerSnapHelpers where the parent is vertical, was that the vertical parent was confusing some horizontal swipes as vertical. In order to avoid such confusion I intercept event that are detected as horizontal and disallow the parent to consume such events.
Here's a simplified version of what I did:
public class VerticalRecyclerView extends RecyclerView {
private GestureDetector mGestureDetector;
public VerticalRecyclerView(Context context) {
super(context);
init();
}
public VerticalRecyclerView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public VerticalRecyclerView(Context context, #Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
setLayoutManager(layoutManager);
mGestureDetector = new GestureDetector(getContext(), new HorizontalScrollDetector());
}
#Override
public boolean onInterceptTouchEvent(MotionEvent e) {
if (mGestureDetector.onTouchEvent(e)) {
return false;
}
return super.onInterceptTouchEvent(e);
}
public class HorizontalScrollDetector extends
GestureDetector.SimpleOnGestureListener {
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return Math.abs(distanceX) > Math.abs(distanceY);
}
}
}
They key is on how to detect if a scroll should be consider horizontal, as in return Math.abs(distanceX) > Math.abs(distanceY);

apSTRK's class in Kotlin:
class VerticalRecyclerView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
private var mGestureDetector = GestureDetector(context, object : SimpleOnGestureListener() {
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float)
= abs(distanceX) > abs(distanceY)
})
override fun onInterceptTouchEvent(e: MotionEvent?) = if (mGestureDetector.onTouchEvent(e)) false
else super.onInterceptTouchEvent(e)
}

Related

Android RecyclerView plus ViewPager

I have a ViewPager that utilizes a RecyclerView for each page and shares ViewItem rows across pages. Accordingly I share a single RecyclerViewPool between them. However, the ViewPager loads each RecyclerView whether or not it is the page on screen. Is there a way to indicate to the RecyclerView that all of its items are offscreen and force its views to be returned to the Recycler?
My sense is that subclassing LinearLayoutManager and overriding its onLayoutChildren method is the way to go, but I don't have much experience with LayoutManager and would like some guidance.
So here is a subclass of LinearLayoutManager that operates the way I described:
public class PageVisibleLinearLayoutManager extends LinearLayoutManager {
public PageVisibleLinearLayoutManager(Context context) {
super(context);
}
public PageVisibleLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public PageVisibleLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private boolean pageVisible = true;
void setPageVisible(boolean pageVisible) {
boolean change = (this.pageVisible != pageVisible);
this.pageVisible = pageVisible;
if(change) requestLayout();
}
#Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if(pageVisible) {
super.onLayoutChildren(recycler, state);
} else {
removeAndRecycleAllViews(recycler);
}
}
}
It works nicely and gives up its views if requested. As dsh mentioned, it is important to mark adjacent pages as being on screen (and I really don't know why setOffscreenPageLimit doesn't limit the number of pages loaded as expected). My previous solution was to use ViewStub and inflate a page only when it was on screen or adjacent. The layout manager method is slightly faster upon initial turning to an unloaded page, but ViewStub has the advantage of pages staying in memory once loaded (making subsequent scrolling more smooth), so I decided to stick with that.
Thank you all. Next question...

Using CustomBottomSheetBehavior in an Scrolling Activity causes an empty space behind tool-bar

I am recently using CustomBottomSheetBehavior to make an googlemaps like bottom sheet behavior and it works great. I have only one problem.please look at this image
If I use it in a scrolling activity the content of tool-bar covers my list box. so I ahve to add margin-top to my list view. It works but when I draw bottomsheet up toolbar goes up and behind it, there is an empty space. This is because I have added some margin top to make my list's top visible. Is there any way to connect list's margin top to the amount of moving bottom-sheet and when It moves up decrease margin value to and when it moves down increase it?or is there any better way ?
It seems I have to develope my own TopMarginBehavior for this job but I have no idea how to do it.
thanks
Create your own class related to the behavior you want (MarginTopBehavior)
Extends it from CoordinatorLayout.Behavior
Now you have to focus on 2 methods: layoutDependsOn and onDependentViewChanged. With the first one you are selecting the view that your MarginTopBehavior is following, in this case is a NestedScrollView. With the second one you are reacting (the magic!) when the scroll get moved.
At this point you get this:
public class MarginTopBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
private FrameLayout.LayoutParams mLayoutParams;
public MarginTopBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof NestedScrollView;
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
}
}
The logic to be applied in onDependentViewChanged is just this:
* Define the cap (min/max margin value) and controls when the margin value has reached one of those cap.
* Update margin value while the values are between the caps. In this point you have to implement an algorithm about what you want (parallax, linear, etc). That is what I'm calling THE_MAGIC_ECC in the next code:
public class MarginTopBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
/**
* Params of the component you want to modify the margin
*/
private FrameLayout.LayoutParams mLayoutParams;
/**
* Used to access DIMENS in your project
*/
private Context mContext;
private int mMinYvalue;
private int mMaxYValue;
public MarginTopBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof NestedScrollView;
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
if (mLayoutParams == null) {
mLayoutParams = (FrameLayout.LayoutParams) child.getLayoutParams();
}
if (dependency.getY() <= mMinYvalue) {
mLayoutParams.setMargins(0, 0, 0, 0);
child.setLayoutParams(mLayoutParams);
return true;
}
else if (dependency.getY() > mMinYvalue && dependency.getY() <= mMaxYValue) {
int THE_MAGIC_ECC = 1 + 2 + 3;
mLayoutParams.setMargins(0, 0, 0, THE_MAGIC_ECC );
child.setLayoutParams(mLayoutParams);
return true;
}
else {
mLayoutParams.setMargins(0, 0, 0, 100);
child.setLayoutParams(mLayoutParams);
return true;
}
}
}

How can I fill RecyclerView with GridLayoutManager from right to left

I'm trying to fill some data into a RecyclerView with GridLayoutManager:
GridLayoutManager layoutManager = new GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false);
This will fill the data into the grid from left to right. The first item will be put into top-left place, etc.
But the app I'm developing is designed for an RTL language, so I need to make it fill from right to left.
I tried to change the last argument to true. But it just scrolled the RecyclerView all the way down.
Also setting layoutDirection attribute for RecyclerView doesn't work (without considering that my min API is 16 and this attribute is added for API17+):
<android.support.v7.widget.RecyclerView
android:id="#+id/grid"
android:layoutDirection="rtl"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
How can I create a right to left filled grid using RecyclerView?
Create a class that extends GridLayoutMAnager ,and override the isLayoutRTL() method like this:
public class RtlGridLayoutManager extends GridLayoutManager {
public RtlGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public RtlGridLayoutManager(Context context, int spanCount) {
super(context, spanCount);
}
public RtlGridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) {
super(context, spanCount, orientation, reverseLayout);
}
#Override
protected boolean isLayoutRTL(){
return true;
}
}
The answer by Mohammad is true but you do not need to create a new class. you can simply override isLayoutRTL method.
GridLayoutManager lm = new GridLayoutManager(this, 2) {
#Override
protected boolean isLayoutRTL() {
return true;
}
};
There is a better way to do it
You can programmatically change the layout direction of the RecycleView
workHourRecycler = view.findViewById(R.id.market_hours_recycler);
workHourRecycler.setLayoutManager(new GridLayoutManager(getContext(),4));
//Programtically set the direction
workHourRecycler.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
On the face of it, GridLayouManager should fill rows from right when getLayoutDirection() gives ViewCompat.LAYOUT_DIRECTION_RTL. Normally, this direction will be inherited from the RecyclerView's container.
But if this does not work as expected, or you need to push it down to API 16, or you some device with bad implementation of RTL support, you can simply pull the GridLayoutManager from github, and use a custom manager that you tune to your liking.
It may be enough to extend the stock GridLayoutManager and override layoutChunk().
Its very simple, just call method setReverseLayout as,
GridLayoutManager layoutManager = new GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false);
layoutManager.setReverseLayout(true);

Automatically disable HorizontalScrollView when content small enough

I use a HorizontalScrollView to contain a bunch of dynamic TextView elements. They are dropped into a LinearLayout container that is the only child of the scroll view:
<HorizontalScrollView android:id="#+id/outline_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none"
android:requiresFadingEdge="horizontal"
android:fadingEdgeLength="16dp">
<LinearLayout android:id="#+id/outline"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</HorizontalScrollView>
This is to ensure that if (and only if) there's more text than the available width can show, the user can scroll horizontally through the texts.
BUT: in many, many cases, the texts are short enough to be shown on screen. The LinearLayout container with id outline thus fits completely within the HorizontalScrollView.
Problem is: horizontal swipe gestures are still caught but should not be, because the whole thing is within a ViewPager which itself would like to handle the horizontal swipes!
I am looking for a solution that enables this HorizontalScrollView's scrolling only if the room for the contents is too limited.
In order to prevent the HorizontalScrollView from scroll, you have to override the onTouchEvent method to return false. That led me to create my own HSV like so:
public class MyHorizontalScrollView extends HorizontalScrollView{
boolean tooSmall = true;
public MyHorizontalScrollView(Context context) {
super(context);
}
public MyHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyHorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setTooSmall(boolean tooSmall){
this.tooSmall = tooSmall;
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
if(tooSmall)
return false;
else
return super.onTouchEvent(ev);
}
}
Then, after you replace your HSV with this custom view, you need monitor the size of your LinearLayout(R.id.outline) to see if it is smaller or larger than your HSV. Adding this snippet helped me achieve that goal.
ll = (LinearLayout) view.findViewById(R.id.outline);
hsv = (MyHorizontalScrollView) view.findViewById(R.id.outline_container);
ll.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
Log.d("widths", ll.getWidth() + " : " + hsv.getWidth());
hsv.setTooSmall(ll.getWidth() < hsv.getWidth());
}
});

Analyse and handle scroll gestures in Imageview (with canvas) without a ScrollView

I've tried many things to handle my problem, but it didn't really work and google also couldn't help me.
I've an ImageView which contains a canvas. The canvas draws a part of a graph. So when the user scrolls (horizontally) the canvas should draw another part of the graph. For example:
The canvas width is 200px
The canvas draws the graph from x=0 to x=200
The user scrolls (horizontally), he moves his finger over 100px
The canvas should now draw the graph from x=100 to x=300
The canvas should redraw by every pixel the user scrolls (for a smooth scrolling)
This solution did not work:
Draw the whole graph and put it in a Scrollview. This delivers a OutOfMemoryError, because the canvas/bitmap is too big.
Also I need to do some other things, so I need to do this like I described above.
Here is the Code:
XML:
<de.touristenfahrerforum.Marcel.Fragments.SpeedGraph
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="#+id/graph"
android:background="#android:color/black"
android:name="de.touristenfahrerforum.MarcelMoiser.Fragments.SpeedGraph">
</de.touristenfahrerforum.Marcel.Fragments.SpeedGraph>
Java Class:
public class SpeedGraph extends ImageView
{
...
public SpeedGraph(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public SpeedGraph(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SpeedGraph(Context context) {
super(context);
}
public void setup( ArrayList<Location> locations, ScrollViewListener scrollViewListener )
{
...
Bitmap bitmap = Bitmap.createBitmap(speedCanvasWidth,speedCanvasHeight,Bitmap.Config.ARGB_8888);
canvas = new Canvas(bitmap);
this.setImageBitmap(bitmap);
...}
...
private void drawGraph(int start, int end, int color)
{
graphPaint.setColor(color);
long startTime = locations.get(start).getTime();
Path path = new Path();
path.moveTo((locations.get(start).getTime()-startTime)/10*V.LOGICAL_DENSITY, speedCanvasHeight-(int)(locations.get(start).getSpeed()*3.6*SIZE_FACTOR));
for( int it = start; it < end; it++ )
{
Location location = locations.get(it);
path.lineTo((location.getTime()-startTime)/10*V.LOGICAL_DENSITY, speedCanvasHeight-(int)(location.getSpeed()*3.6*SIZE_FACTOR));
}
canvas.drawPath(path,graphPaint);
}
...
So I would like to implement something that recognizes horizontal scroll gestures and gives me a number of pixels that shows me the pixels the user has scrolled.
Thank you guys in advance
It's the first time that nobody answered, but i figured out how to solve my problem.
The best way is to implement GestureDetector.OnGestureListener and overwrite all it's methods. Also you have to create a GestureDetector Object. The method OnScroll(...) is where to look for the pixels the user has scrolled.
Here is some code:
public class SpeedGraph extends ImageView implements GestureDetector.OnGestureListener
{
private Context context;
...
public void setup( ... )
{
this.gestureDetector = new GestureDetector(context, this);
gestureDetector.setIsLongpressEnabled(false);
...}
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
{
scrolled+=distanceX;
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
drawGrid();
redrawGraphsInCanvas();
this.invalidate();
return true;
}
#Override
public boolean dispatchTouchEvent(MotionEvent ev){
gestureDetector.onTouchEvent(ev);
return true;
}
...

Categories

Resources