TouchSlidingScreen:
public class TouchSlidingScreen extends ViewGroup {
//平滑滚动中要用到Scroller
private Scroller scroller;
//最小滑动距离,超过了才认为开始滑动
private int touchSlop = 0;
//停止状态
private static final int TOUCH_STATE_STOP = 0;
//滑动状态
private static final int TOUCH_STATE_FLING = 1;
private int touchState = TOUCH_STATE_STOP;
///上次触摸屏的 x 位置
private float lastX = 0;
//当前位置
private int curScreen;
//VelocityTracker 主要用于跟踪触摸屏事件(flinging 事件和其他 gestures 手势事件)的速率
private VelocityTracker velocityTracker;
public static int SNAP_VELOCITY = 600; //最小的滑动速率
public TouchSlidingScreen(Context context) {
this(context, null);
}
public TouchSlidingScreen(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TouchSlidingScreen(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public void init(Context context) {
scroller = new Scroller(context);
//获取到当前手机上默认的最小滑动距离
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//测量子View
measureChildren(widthMeasureSpec, heightMeasureSpec);
int width = measureWidth(widthMeasureSpec);
int height = measureHeight(heightMeasureSpec);
//保存测量自己的宽高
setMeasuredDimension(width, height);
}
//子View宽必须是match_parent,容器总的宽是一屏*子View的个数
private int measureWidth(int widthMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int width = 0;
//ViewGroup不能是包裹
if (widthMode == MeasureSpec.AT_MOST) {
throw new IllegalStateException("the width must is match_parent!");
} else {
width = widthSize;
}
return width * getChildCount();
}
//容易的高必须是match_parent
private int measureHeight(int heightMeasureSpec) {
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int height = 0;
if (heightMode == MeasureSpec.AT_MOST) {
throw new IllegalStateException("the height must is match_parent!");
} else {
height = heightSize;
}
return height;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int childWidth = (r - l) / count;//一屏的宽度
int childHeight = b - t;//一屏的高度
for (int i = 0; i < count; i++) {
int left = i * childWidth;
int top = 0;
int right = (i + 1) * childWidth;
int bottom = childHeight;
View childView = getChildAt(i);
//定位子View
childView.layout(left, top, right, bottom);
}
}
//事件拦截
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
int x = (int) ev.getX();
int y = (int) ev.getY();
if (action == MotionEvent.ACTION_MOVE && touchState == TOUCH_STATE_STOP) {//滑动并且屏幕中的状态是停止时
return true;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
lastX = x;
touchState = scroller.isFinished() ? TOUCH_STATE_STOP : TOUCH_STATE_FLING;
break;
case MotionEvent.ACTION_HOVER_MOVE:
//滑动距离过小不算滑动
int dx = (int) Math.abs(x - lastX);
if (dx > touchSlop) {
touchState = TOUCH_STATE_FLING;
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
touchState = TOUCH_STATE_STOP;
break;
}
return touchState != TOUCH_STATE_STOP;//滑动时返回true拦截,不交给子View
}
/**
* @param event
* @return
*/
public boolean onTouchEvent(MotionEvent event) {
if (velocityTracker == null) {
//使用 obtain()方法得到这个类的实例
velocityTracker = VelocityTracker.obtain();
}
//addMovement(MotionEvent)函数将你接受到的 motion event 加入到 VelocityTracker 类实例中
velocityTracker.addMovement(event);
super.onTouchEvent(event);
int action = event.getAction();
final int x = (int) event.getX();
switch (action) {
case MotionEvent.ACTION_DOWN:
//手指按下时,如果正在滚动,则立刻停止
if (scroller != null && !scroller.isFinished()) {
scroller.abortAnimation();
}
lastX = x;
break;
case MotionEvent.ACTION_MOVE:
//随手指滚动
int dx = (int) (lastX - x);
scrollBy(dx, 0);
lastX = x;
break;
case MotionEvent.ACTION_UP:
final VelocityTracker velocityTracker = this.velocityTracker;
//使用 computeCurrentVelocity(int)初始化速率的单位
velocityTracker.computeCurrentVelocity(1000);
//获得x方向的速率
int velocityX = (int) velocityTracker.getXVelocity();
//通过 velocityX 的正负值可以判断滑动方向
if (velocityX > SNAP_VELOCITY && curScreen > 0) {
moveToPrevious();
} else if (velocityX < -SNAP_VELOCITY && curScreen < (getChildCount() - 1)) {
moveToNext();
} else {
moveToDestination();
}
if (velocityTracker != null) {//释放并回收资源
this.velocityTracker.clear();
this.velocityTracker.recycle();
this.velocityTracker = null;
}
touchState = TOUCH_STATE_STOP;
break;
case MotionEvent.ACTION_CANCEL:
touchState = TOUCH_STATE_STOP;
break;
}
return true;
}
public void moveToScreen(int whichScreen) {
curScreen = whichScreen;
int scrollX = getScrollX();
int splitWidth = getWidth() / getChildCount();
int dx = curScreen * splitWidth - scrollX;
scroller.startScroll(scrollX, 0, dx, 0, Math.abs(dx));
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
postInvalidate();
}
}
public void moveToDestination() {
//每一屏的宽度
int splitWidth = getWidth() / getChildCount();
//判断是回滚还是进入下一分屏
int toScreen = (getScrollX() + splitWidth / 2) / splitWidth;
//移动到目标分屏
moveToScreen(toScreen);
}
/**
* 滚动到下一屏
*/
public void moveToNext() {
if (curScreen >= getChildCount() - 1) {
Toast.makeText(getContext(), "当前已经是最后一页", Toast.LENGTH_SHORT).show();
return;
}
moveToScreen(curScreen + 1);
}
/**
* 滚动到上一屏
*/
public void moveToPrevious() {
if (curScreen <= 0) {
Toast.makeText(getContext(), "当前已经是第一页", Toast.LENGTH_SHORT).show();
return;
}
moveToScreen(curScreen - 1);
}
}
activity_main:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.example.user.myapplication4.TouchSlidingScreen
android:id="@+id/ml"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#FFFF00"
android:orientation="vertical"></LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#00FF00"
android:orientation="vertical"></LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#0000FF"
android:orientation="vertical"></LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#00FFFF"
android:orientation="vertical"></LinearLayout>
</com.example.user.myapplication4.TouchSlidingScreen>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/pre"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="上一屏" />
<Button
android:id="@+id/next"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@android:color/holo_green_light"
android:text="下一屏" />
</LinearLayout>
</LinearLayout>
MainActivity:
public class MainActivity extends Activity {
private TouchSlidingScreen ml;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ml = (TouchSlidingScreen) findViewById(R.id.ml);
Button pre = (Button) findViewById(R.id.pre);
Button next = (Button) findViewById(R.id.next);
pre.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ml.moveToPrevious();
}
});
next.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ml.moveToNext();
}
});
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
handler.sendEmptyMessage(0);
}
},2000,3000);
}
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what){
case 0:
ml.moveToNext();
break;
}
}
};
}
第一步:初始化。平滑滚动需要使用 Scroller 对象,另外还需要给定一个最小滑动距离,通过 ViewConfiguration.get(context).getScaledTouchSlop()可以获取到当前手机上默认的最小滑动距离。
第二步:测量容器宽度与高度。不允许使用 MeasureSpec.AT_MOST,每个子组件与容器相同,容器的 layout_width 值虽然为 MeasureSpec. EXACTLY,但容器大小 =父容器的宽度 * 子组件的个数,高度与父容器相同。
第三步:定位子组件。默认情况下,屏幕出现第一个子组件,子组件占满容器的可见区域,其他子组件以相同大小依次排列在后面。
第四步:判断滚动状态,状态为分两种:停止状态和滑动状态。容器根据状态决定是否截拦事件。
第五步:惯性滚屏:
手指滑动距离如果超过容器一半或者滑动速度足够快,则进入下一屏(或者上一屏)。如果没有超过一半或速度很慢则回滚到初始位置。定义 moveToDestination()方法如下,最关键的语句是 int toScreen= (getScrollX()+ splitWidth/ 2 ) / splitWidth,getScrollX()是容器滚动过的距离,splitWidth 是每一屏的宽度。比如每一屏的宽度为 10,当前屏为第 2 屏,容器已滚过 23,则 toScreen= (23 +10 / 2) / 10= (23 + 5) / 10 = 28/ 10 = 2.8 = 2,也就是说要回滚到第 2 屏;如果容器已滚动28,则 toScreen =(28 + 10 / 2)/ 10 = 32 /10 = 3.2 = 3,表示要滚动到第 3 屏。
第六步:响应用户手指的按下、移动和松开事件,这是整个滑动的关键,特别是松开后,要判断滚屏还是回滚。为了支持上一屏和下一屏,需要辨别手指滑动的方向,VelocityTracker 类可以获取 x 方向的速率,其正值代表向左滑动,负值代表向右滑动。如果 x 方向的速率在[-SNAP_VELOCITY,SNAP_VELOCITY]之间,则要根据用户滑动的距离(滑动距离是否超过一屏的1/2)决定是要继续滚屏还是回滚到初始状态。
VelocityTracker :
addMovement(MotionEvent)函数将你接受到的 motion event 加入到 VelocityTracker 类实例中。当我们需要使用到速率时,使用 computeCurrentVelocity(int)初始化速率的单位,并获得当前的事件的速率,然后使用 getXVelocity() 或 getXVelocity()获得横向和竖向的速率。另外,通过VelocityTracker 还可以知道手指的滑动方向。
VelocityTracker 的基本使用如下:
手指按下时(ACTION_DOWN),获取 VelocityTracker 对象
if(velocityTracker== null){
//创建 velocityTracker 对象
velocityTracker= VelocityTracker.obtain();
}
//关联事件对象
velocityTracker.addMovement(ev);
手指移动过程中(ACTION_MOVE),计算速率
velocityTracker.computeCurrentVelocity(1000);
获取 x、y 两个方向的速率:
int velocityX = velocityTracker.getXVelocity();
int velocityY =velocityTracker.getYVelocity();
手指松开后(ACTION_UP),释放并回收资源
//释放 VelocityTracker 资源
if(velocityTracker !=null){
velocityTracker.clear();
velocityTracker.recycle();
velocityTracker =null;
}