想实现类似网易新闻的右滑出menu的那种效果(youTube也有那种效果),网上找了种种方法和代码例子,发现都达不到想要的效果。发现他们大部分方法,不是重写HorizontalScrollView, 就是重写SlidingDrawer。
其实想像一下那个交互的直实情况,完全不用绕圏子,原理就可以是将上面的一个View拨开,将下面的View显示出来而以。 不如自己重写一个基本布局.
先看一下做出来的效果吧,有人说没图说真相, 这个自己多嵌套了一层。(每滑完一层,右边就变灰,表示不可操作了)
右滑---------->
再右滑---------->
最大的特点是如果你要优化自己的代码成这个效果,代码改动量非常小,只需要调整一下布局,改改View的位置就可以了。甚至代码可以不用修改。
下面看看实现过程
首先,两个View是可以重叠的。我们使用FrameLayout作为继承类。命名为ScrollDrawerView
这里要保证FrameLayout里有两个子View, 做很多事情先都要先检测一下。
/**
* 检查设置top, bottom
* @return
*/
private boolean checkTopBottomOk() {
if (mBottomView != null && mTopView != null) {
return mBottomView != mTopView;
}
int count = getChildCount();
if (mBottomView == null && mTopView == null) {
if (count != 2) {
return false;
} else {
mBottomView = getChildAt(0);
mTopView = getChildAt(1);
return mBottomView != mTopView;
}
} else {
if (count != 1) {
return false;
} else {
View v = getChildAt(0);
if (mBottomView != null) {
mTopView = v;
} else {
mBottomView = v;
}
return mBottomView != mTopView;
}
}
}
/**
* 设置(替换)抽屉的下层
* @param v
*/
public void setBottomView(View v) {
this.addView(v, 0);
mBottomView = v;
}
/**
* 设置(替换)抽屉的上层
* @param v
*/
public void setTopView(View v) {
this.addView(v, 1);
mTopView = v;
}
需要灵活一点的话,就是继可以在layout文件中去配置View的位置,也可以在代码中进行设置。见上面的代码。
2, 要写View的话,onInterceptTouchEvent和onTouchEvent是必须要处理的。因为之前对这些只是一知半解, 也懒得去细想。于是用一个偷懒的办法,从ViewPager那里“借”点东西喽。因为ViewPager跟我们的事件拦截非常相似。如果子View里面内容可以滑动,则不会触发父View的onTouchEvent事件。干脆将onInterceptTouchEvent抄过来,稍作修改,嘿嘿大功告成。注意canScroll方法是核心
private boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) v;
final int scrollX = v.getScrollX();
final int scrollY = v.getScrollY();
final int count = group.getChildCount();
// Count backwards - let topmost views consume scroll distance first.
for (int i = count - 1; i >= 0; i--) {
// TODO: Add versioned support here for transformed views.
// This will not work for transformed views in Honeycomb+
final View child = group.getChildAt(i);
if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
canScroll(child, true, dx, x + scrollX - child.getLeft(),
y + scrollY - child.getTop())) {
return true;
}
}
}
return checkV && v.canScrollHorizontally(-dx) && v.isEnabled();
}
另外拓展性想稍强一点, 可以自嵌套的话。也要重写canScrollHorizontally
@Override
public boolean canScrollHorizontally(int direction) {
if (checkTopBottomOk()) {
int postion = mTopView.getLeft() - direction;
return postion >= 0 && postion <= mBottomView.getWidth();
} else {
return false;
}
}
上面的意思就是mTopView的位置只能在0 到mBottomView.getWidth()的范围,不能越界。
看onTouchEvent的处理。ACTION_DOWN只是一个初始化的处理。而且很多情况不会运行到,原因是事件的起源是从onInterceptTouchEvent的ACTION_MOVE那里才决定到要去拦截处理这个事件。因此onTouchEvent的很大机率是从ACTION_MOVE开始接收的。
final int activePointerIndex = event.findPointerIndex(mActivePointerId);
final int x = (int) event.getX(activePointerIndex);
int deltaX = mLastMotionX - x;
if (!mIsBeingDragged) {
final int y = (int) event.getY(activePointerIndex);
int yDiff = Math.abs(y - mLastMotionY);
if (Math.abs(deltaX) > mTouchSlop && Math.abs(deltaX) > yDiff) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
mIsBeingDragged = true;
if (deltaX > 0) {
deltaX -= mTouchSlop;
} else {
deltaX += mTouchSlop;
}
}
}
if (mIsBeingDragged) {
mLastMotionX = x;
int bottomWidth = mBottomView.getWidth();
int left = mTopView.getLeft() - deltaX;
if (left < 0) {
left = 0;
} else if (left > bottomWidth) {
left = bottomWidth;
}
if (left == 0 || left == bottomWidth) {
mVelocityTracker.clear();
}
//位置随手指变动
mTopView.setLeft(left);
}
}
上面是ACTION_MOVE的处理,其分为两部分,if(!mIsBeingDragged){}里面是去决定是否真的去执行拖动事件。 if(mIsBeingDragged){} 里面是让mTopView的位置随手的移动而变动。
看下ACTION_UP的处理。这里决定了滑动过后的开关状态。
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(500, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
final int activePointerIndex = event.findPointerIndex(mActivePointerId);
final int x = (int) event.getX(activePointerIndex);
int xDiff = Math.abs(mInitialMotionX - x );
if ((Math.abs(initialVelocity) > mDirectVelocity)) {
//速度足够大,执行切换
opener(initialVelocity > 0);
} else if ((Math.abs(initialVelocity) > mMinimumVelocity) && (xDiff > mBottomView.getWidth() / 4)) {
//速度不太大,但移动距离够长,也执行切换
opener(initialVelocity > 0);
}else {
//按现在位置,不到一半就返回,到了一半就过去
opener(mTopView.getLeft() >= mBottomView.getWidth()/2);
}
根据速度,移动位置,最终位置三个条件来决定状态的开关, 这里很好理解了。
然后再用mScroller处理一下滑动的动画(关联函数opener, computeScroll)。
再处理一下滑开后使mTopView无效(关联函数setViewEnabled)。
再处理一下Layout后mTopView的复问问题(onLayout).再修理一下边边角角。
OK,搞定了,效果不错。还可以嵌套使用。可以延伸出很多有意思的效果来。
我比较懒很多东西也说不清楚。。上面只是一个大概思路。详细的看代码就OK了