4

I want to achieve Snapping effect in HorizontalScrollView i.e when the user scrolls horizontally the item which is most visible (item visible > 50%) comes to the center.

I tried to do this using:

hsv.getViewTreeObserver().addOnScrollChangedListener(
    new ViewTreeObserver.OnScrollChangedListener() {
        @Override
        public void onScrollChanged() {
            int scrollX = hsv.getScrollX(); // For HorizontalScrollView
            Log.e("scrollX",String.valueOf(scrollX));
            // DO SOMETHING WITH THE SCROLL COORDINATES
        }
    }
);

But the value is not constant even when we do not touch the screen.

Here is some part of logcat:

03-28 11:11:22.116 26639-26639/package_name E/scrollX: 0
03-28 11:11:22.133 26639-26639/package_name E/scrollX: 792
03-28 11:11:22.133 26639-26639/package_name E/scrollX: 0
03-28 11:11:22.151 26639-26639/package_name E/scrollX: 795
03-28 11:11:22.151 26639-26639/package_name E/scrollX: 0
03-28 11:11:22.166 26639-26639/package_name E/scrollX: 799
03-28 11:11:22.166 26639-26639/package_name E/scrollX: 0
03-28 11:11:22.183 26639-26639/package_name E/scrollX: 801
03-28 11:11:22.183 26639-26639/package_name E/scrollX: 0
03-28 11:11:22.199 26639-26639/package_name E/scrollX: 803
03-28 11:11:22.199 26639-26639/package_name E/scrollX: 0
03-28 11:11:22.216 26639-26639/package_name E/scrollX: 804
03-28 11:11:22.216 26639-26639/package_name E/scrollX: 0
03-28 11:11:22.233 26639-26639/package_name E/scrollX: 805
03-28 11:11:22.233 26639-26639/package_name E/scrollX: 0
03-28 11:11:22.249 26639-26639/package_name E/scrollX: 806
03-28 11:11:22.249 26639-26639/package_name E/scrollX: 0

I've already tried these solutions, either I am not getting the point or I don't know to do it:

  1. HorizontalScrollView within ScrollView Touch Handling
  2. HorizontalScrollView with snapping effect
  3. Creating Custom Horizontal Scroll View With Snap or paging
  4. Creating a “Snapping” Horizontal Scroll View

My Usecase: I have an HorizontalScrollView which is attached to the adapter of Recyclerview(Vertical) so snapHelper can be done in vertical but I don't know how to make it for horizontal.

ℛɑƒæĿᴿᴹᴿ
  • 4,983
  • 4
  • 38
  • 58
nimi0112
  • 2,065
  • 1
  • 18
  • 32

2 Answers2

4

Here is a complete code for a Custom Horizontal Scroll View that snaps the items.

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
import android.view.View;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;

import java.util.ArrayList;

public class HomeFeatureLayout extends HorizontalScrollView {
    private static final int SWIPE_MIN_DISTANCE = 5;
    private static final int SWIPE_THRESHOLD_VELOCITY = 300;

    private ArrayList mItems = null;
    private GestureDetector mGestureDetector;
    private int mActiveFeature = 0;

    public HomeFeatureLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public HomeFeatureLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public HomeFeatureLayout(Context context) {
        super(context);
    }

    public void setFeatureItems(ArrayList items){
        LinearLayout internalWrapper = new LinearLayout(getContext());
        internalWrapper.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
        internalWrapper.setOrientation(LinearLayout.HORIZONTAL);
        addView(internalWrapper);
        this.mItems = items;
        for(int i = 0; i< items.size();i++){
            LinearLayout featureLayout = (LinearLayout) View.inflate(this.getContext(),R.layout.homefeature,null);
            //...
          //Create the view for each screen in the scroll view
            //...
            internalWrapper.addView(featureLayout);
        }
        setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                //If the user swipes
                if (mGestureDetector.onTouchEvent(event)) {
                    return true;
                }
                else if(event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL ){
                    int scrollX = getScrollX();
                    int featureWidth = v.getMeasuredWidth();
                    mActiveFeature = ((scrollX + (featureWidth/2))/featureWidth);
                    int scrollTo = mActiveFeature*featureWidth;
                    smoothScrollTo(scrollTo, 0);
                    return true;
                }
                else{
                    return false;
                }
            }
        });
        mGestureDetector = new GestureDetector(new MyGestureDetector());
    }
        class MyGestureDetector extends SimpleOnGestureListener {
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            try {
                //right to left
                if(e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
                    int featureWidth = getMeasuredWidth();
                    mActiveFeature = (mActiveFeature < (mItems.size() - 1))? mActiveFeature + 1:mItems.size() -1;
                    smoothScrollTo(mActiveFeature*featureWidth, 0);
                    return true;
                }
                //left to right
                else if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
                    int featureWidth = getMeasuredWidth();
                    mActiveFeature = (mActiveFeature > 0)? mActiveFeature - 1:0;
                    smoothScrollTo(mActiveFeature*featureWidth, 0);
                    return true;
                }
            } catch (Exception e) {
                    Log.e("Fling", "There was an error processing the Fling event:" + e.getMessage());
            }
            return false;
        }
    }
}

This example does adding the views programatically and calls them Features. But you can simple change that behaviour and use getChildrenCount() instead of mItems.size() and so on.

The important part is the GestureDetector and TouchListener. In TouchListener, you can listen for ACTION_UP which is when the user's finger is removed (like after scroll) and you calculate which view is the active one based on the amount of scroll and their positions. You can also add a GestureDetector to catch the fling operations and do the same there.

Abandoned Cart
  • 4,512
  • 1
  • 34
  • 41
Adib Faramarzi
  • 3,798
  • 3
  • 29
  • 44
  • which layout should I attach to it ? – nimi0112 Mar 28 '18 at 06:14
  • 1
    You need to change that part as I said unless all your views inside the scroll view are the same like the example. In the example the `//Create the view for each screen in the scroll view` part is where you initialize each view for the scroll view but that might not be your case as you might have already have views inside your scroll view. add the `setOnTouchListener` in the constructors and use the suggestion I made above to make it work with your code. – Adib Faramarzi Mar 28 '18 at 06:16
  • Hey, I am still not able to figure out the part to attach it to my layout. CAn you help please? – nimi0112 Mar 28 '18 at 09:46
0

Building on the answer from @AdibFaramarzi, here is a version where you create the inner LinearLayout yourself and add / remove items dynamically:

import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;

import java.util.ArrayList;

public class SnapHorizontalScrollView extends HorizontalScrollView {
    private static final int SWIPE_MIN_DISTANCE = 5;
    private static final int SWIPE_THRESHOLD_VELOCITY = 300;

    private final ArrayList<LinearLayout> mItems = new ArrayList<>();
    private GestureDetector mGestureDetector;
    private int mActiveFeature = 0;

    public SnapHorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public SnapHorizontalScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SnapHorizontalScrollView(Context context) {
        super(context);
    }

    public void removeFeatureItem(LinearLayout featureLayout) {
        this.mItems.remove(featureLayout);
    }

    @SuppressLint("ClickableViewAccessibility")
    public void addFeatureItem(LinearLayout featureLayout) {
        this.mItems.add(featureLayout);
        setOnTouchListener((v, event) -> {
            //If the user swipes
            if (mGestureDetector.onTouchEvent(event)) {
                return true;
            }
            else if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL ){
                int scrollX = getScrollX();
                int featureWidth = v.getMeasuredWidth();
                mActiveFeature = ((scrollX + (featureWidth / 2)) / featureWidth);
                int scrollTo = mActiveFeature * featureWidth;
                smoothScrollTo(scrollTo, 0);
                return true;
            }
            else{
                return false;
            }
        });
        mGestureDetector = new GestureDetector(new MyGestureDetector());
    }
    class MyGestureDetector extends SimpleOnGestureListener {
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            try {
                // right to left
                if (e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
                    int featureWidth = getMeasuredWidth();
                    mActiveFeature = (mActiveFeature < (mItems.size() - 1))? mActiveFeature + 1:mItems.size() -1;
                    smoothScrollTo(mActiveFeature*featureWidth, 0);
                    return true;
                }
                // left to right
                else if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
                    int featureWidth = getMeasuredWidth();
                    mActiveFeature = (mActiveFeature > 0)? mActiveFeature - 1:0;
                    smoothScrollTo(mActiveFeature * featureWidth, 0);
                    return true;
                }
            } catch (Exception e) {
                Log.e("Fling", "There was an error processing the Fling event:" + e.getMessage());
            }
            return false;
        }
    }
}

In XML, this would be implemented in place of the HorizontalScrollView and then the usual LinearLayout would be placed inside it. For the following, we will assume that you set internalWrapper as the variable for the inner LinearLayout.

When adding an item to the LinearLayout, follow the internalWrapper.addView(layout) with snapScroller.addFeatureItem(layout) and similarly follow the internalWrapper.removeView(layout) with snapScroller.removeFeatureItem(layout)

Abandoned Cart
  • 4,512
  • 1
  • 34
  • 41