近期项目原因需要一个上下两屏滑动的效果。可以想象成viewpager左右滑动变成上下滑动。本来想用Srcollview实现,但是由于一些原因,比如按键冲突,和listview布局冲突等等,最后决定自己写一个自定义控件。
由于之前实现过SlidingMenu,所以就考虑参考那个模式,左右滑动变成上下滑动就可以。
其实就是两个大小一样的布局,一个显示在屏幕上,另一个隐藏在屏幕外,等到滑动的时候就显示出来。
我这边继承的是ViewGroup 。再重写onLayout方法,放置一个布局占满屏幕,另一个在屏幕外。
public class MySlidingMenu extends ViewGroup {
private static final String TAG = MySlidingMenu.class.getName();
private enum Scroll_State {
Scroll_to_Open, Scroll_to_Close;
}
private Scroll_State state;
private int mMostRecentY;
private int downY;
private boolean isOpen = false;
private View menu;
private View mainView;
private Scroller mScroller;
private OnSlidingMenuListener onSlidingMenuListener;
public MySlidingMenu(Context context, View main, View menu) {
super(context);
setMainView(main);
setMenu(menu);
init(context);
}
private void init(Context context) {
mScroller = new Scroller(context);
}
@Override
protected void onLayout(boolean arg0, int l, int t, int r, int b) {
mainView.layout(l,t,r,b);
//设置坐标再屏幕下方
menu.layout(l, menu.getMeasuredHeight(), r, menu.getMeasuredHeight()*2);
}
public void setMainView(View view) {
mainView = view;
addView(mainView);
}
public void setMenu(View view) {
menu = view;
addView(menu);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mainView.measure(widthMeasureSpec, heightMeasureSpec);
menu.measure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mMostRecentY = (int) event.getY();
downY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
int moveY = (int) event.getY();
int deltaY = mMostRecentY - moveY;
// 如果在菜单打开时向上滑动及菜单关闭时向下滑动不会触发Scroll事件
if ((!isOpen && (downY - moveY) > 0)
|| (isOpen && (downY - moveY) < 0)) {
scrollBy(0, deltaY);
}
mMostRecentY = moveY;
break;
case MotionEvent.ACTION_UP:
int upY = (int) event.getY();
int dy = upY - downY;
if (!isOpen) {
// 菜单关闭时向右滑动超过menu三分之一宽度才会打开菜单
if (dy < - menu.getMeasuredHeight() / 3) {
state = Scroll_State.Scroll_to_Open;
} else {
state = Scroll_State.Scroll_to_Close;
}
} else {
// 菜单打开时当按下时的触摸点在menu区域时,只有向左滑动超过menu的三分之一,才会关闭
if (dy > menu.getMeasuredHeight() / 3) {
state = Scroll_State.Scroll_to_Close;
} else {
state = Scroll_State.Scroll_to_Open;
}
}
smoothScrollto();
break;
default:
break;
}
return true;
}
private void smoothScrollto() {
int scrolly = getScrollY();
switch (state) {
case Scroll_to_Close:
mScroller.startScroll(0, scrolly, 0, - scrolly, 500);
isOpen = false;
break;
case Scroll_to_Open:
mScroller.startScroll(0, scrolly, 0, menu.getMeasuredHeight()- scrolly, 500);
isOpen = true;
break;
default:
break;
}
//需要主动出发一次invalidate,之后再移动的时候自动走computeScroll方法
invalidate();
}
@Override
public void computeScroll() {
//滑动时候computeScrollOffset会一直返回false,只有在startScroll结束时候才会为true
if (mScroller.computeScrollOffset()) {
scrollTo(0, mScroller.getCurrY());
postInvalidate();
}
super.computeScroll();
}
}
整段代码不算复杂,最主要的是坐标的计算。startScroll(int startX, int startY, int dx, int dy, int duration)这个方法对于坐标滑动比较难算。开始我一直在纠结dx,dy正的往什么方向,负的往什么方向。后来看了一下源码。
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY; //开始滑动坐标
mFinalX = startX + dx;
mFinalY = startY + dy; //滑动终点坐标
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
// This controls the viscous fluid effect (how much of it)
mViscousFluidScale = 8.0f;
// must be set to 1.0 (used in viscousFluid())
mViscousFluidNormalize = 1.0f;
mViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
}
所以如果从A(a1, a2)滑动到B(b1,b2), 只需要startScroll(a1,a2,b1-a1,b2-a2)就行。主要就求个差值。
private MySlidingMenu mSlidingMenu;
......
mSlidingMenu = new MySlidingMenu(this, LayoutInflater
.from(this).inflate(R.layout.fragment1, null), LayoutInflater
.from(this).inflate(R.layout.fragment2, null));
这个会导致listview 滑动冲突,如果列数固定可以一屏显示直接重写listviewe的onTouchEvent 返回false就行。如果比较多的话,那就通过在onTouchEvent () 中getParent().requestDisallowInterceptTouchEvent(bool)来设置哪个控件响应事件。
一般的思路是list滑动到头和尾的时候,才将相应方向的滑动事件传给父组件。