javaandroidperformancehorizontal-scrollinghorizontalscrollview

Snapping Effect in HorizontalScrollView


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.


Solution

  • 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.