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/