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
:
- HorizontalScrollView within ScrollView Touch Handling
- HorizontalScrollView with snapping effect
- Creating Custom Horizontal Scroll View With Snap or paging
- 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.
Advertisement
Answer
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.