Android: Creating a “Snapping” Horizontal Scroll View

Update: It appears as though the android.widget.Gallery layout can provide this same functionality. I’m not sure how it was missed when building this out, but if you’re interested in a custom solution, read on!

This type of view is fairly common in the mobile world, but doesn’t come out of the box with the Android SDK.  Think of the home screen(s) on an Android device.  You can slide between each home screen and when you lift your finger, it “snaps” to the most appropriate screen.  This is fairly simple to build yourself using the existing HorizontalScrollView (or regular ScrollView for vertical scrolling) and adding some code on top of it to handle snapping to each screen.  In this post, I’ll show you how I implemented this view for a rotating article feature.


Requirements

There are a few simple requirements that we had for creating this rotating feature view:

  • If the user slowly scrolls from one screen to another, the view should snap to the screen that is showing more than 50% when the user lifts their finger.
  • If the user quickly swipes in one direction, the view should scroll to the next (or previous) screen even if less than 50% of the next/previous screen is showing.  This allows users to quickly scroll through the features without requiring a long swipe each time.

How it’s Done

First, create your own view and extend HorizontalScrollView.  My sample below has a few constants for determining a “swipe”, as well as an array of Article items (one for each screen), a GestureDetector for detecting the swipe, and an integer value to keep track of the active feature.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class HomeFeatureLayout extends HorizontalScrollView {
     private static final int SWIPE_MIN_DISTANCE = 5 ;
     private static final int SWIPE_THRESHOLD_VELOCITY = 300 ;
 
     private ArrayList<ArticleListItem> 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);
     }
 
...
 
}

The next step is to create a method that adds each screen to your horizontal scroll view.  In my example, I first create a LinearLayout to hold my screens (1 for each article).  Then I iterate through each article, creating a view for each and adding it to my LinearLayout.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void setFeatureItems(ArrayList<ArticleListItem> 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++){
           
           
           
            internalWrapper.addView(myView);
        }
 
                ...
    }

Next, we’ll handle the “slow drag and release” requirement by setting the “OnTouchListener” for the view.  This listener will be triggered every time there is a touch event on our Scroll View, so we can easily trap out the relevant actions and handle them appropriately. We want to trigger our custom logic on the “ACTION_UP” and “ACTION_CANCEL” touch actions, so the snap happens when the user either lifts their finger, or the touch action is cancelled for any reason.  In the sample below I’m first checking for the fast swipe case (which will be described next) and then checking for the ACTION_UP or ACTION_CANCEL actions.  If we have one of these 2 actions, we know it’s time to snap. We use some math to figure out which view is showing more than 50% and smooth scroll to that view’s X position.  Finally, we return true to indicate that the touch event was handled by us.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
         setOnTouchListener( new View.OnTouchListener() {
             @Override
             public boolean onTouch(View v, MotionEvent event) {
                
                 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 ;
                 }
             }
         });

Finally, we’ll handle the quick swipe case.  For this case we’ll use a custom gesture listener.  I won’t get into the specifics of how these work, but here is a good tutorial on implementing the Swipe action in Android that I referenced when building this.  The idea is to determine when there is a left or a right swipe using the distance and velocity constants defined by us.  If a swipe has been captured, we activate the snapping logic to smooth scroll to the next screen (for right swipe) or previous screen (for left swipe). If there is no next or previous screen, we remain on the current screen.

1
2
mGestureDetector = new GestureDetector( new MyGestureDetector());

Create the gesture detector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyGestureDetector extends SimpleOnGestureListener {
         @Override
         public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
             try {
                
                 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 ;
                 }
                
                 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 ;
         }
     }

That’s all there is to it! Here is the code for the full custom scroll class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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 );
            
          
            
             internalWrapper.addView(featureLayout);
         }
         setOnTouchListener( new View.OnTouchListener() {
             @Override
             public boolean onTouch(View v, MotionEvent event) {
                
                 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 {
                
                 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 ;
                 }
                
                 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 ;
         }
     }
}


原文链接:http://blog.velir.com/index.php/2010/11/17/android-snapping-horizontal-scroll/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值