兩個ScrollView在一起的故事
前言
很多時候,作為程序猿的我們都會接到產品的奇奇怪怪的需求,比如我們正要說的,要在同一個界面使用兩個ScrollView,這個兩個ScrollView不是并列的,而是在垂直方向的哦。這時你的心里肯定有千萬只草泥馬飛奔而過,這坑爹需求,怎么可能兩個ScrollView同時使用啊,一個界面里有兩個ScrollView不是滑動各種沖突了啊。別急,這篇文章就是為了幫你解決這樣的變態需求的,且聽我一一道來。
知識要點
首先,你必須掌握如何自定義View,然后還要熟悉事件的傳遞機制,再來就是怎么使用Scroller,最后還要知道VelocityTracker(計算手勢滑動的速度),掌握這些知識之后,看起這篇文章來將水到渠成了,如果還不是很了解或者熟悉的同學,就先自行Google或者度娘下了,如果有需要,我也會把相關的文章寫下來的。
先來看看效果圖:
實現
1、自定義一個Relativelayout。
public class LayoutContainer extends RelativeLayout {
public LayoutContainer(Context context) {
super(context);
}
public LayoutContainer(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LayoutContainer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
然后我們一步一步往里面添加代碼了。
2、將兩個ScrollView繪制進來。
我們把這兩個View分別定義為mTopView和mBottomView。
繪制進來我們使用了onMeasure和onLayout
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//isMeasured的存在是只onMeasure一次,不讓requestLayout調用的時候再次onMeasure
if (!isMeasured) {
isMeasured = true;
mViewHeight = getMeasuredHeight();
mViewWidth = getMeasuredWidth();
mTopView = getChildAt(0);
mBottomView = getChildAt(1);
mBottomView.setOnTouchListener(bottomViewTouchListener);
mTopView.setOnTouchListener(topViewTouchListener);
}
}
這里我們讓TopView和BottomView分別實現了OnTouchListener,目的就是為了判斷,如果當前界面在TopView時,是否可以繼續往下滑動,如果當前界面在BottomView的時候,是否可以繼續往上滑動。
private OnTouchListener topViewTouchListener = new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
ScrollView sv = (ScrollView) v;
//判斷是否在最底部
if (sv.getScrollY() == (sv.getChildAt(0).getMeasuredHeight() - sv.getMeasuredHeight()) && mCurrentViewIndex == 0) {
canPullUp = true;
} else {
canPullUp = false;
}
return mCanScroll;
}
};
private OnTouchListener bottomViewTouchListener = new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
ScrollView sv = (ScrollView) v;
//判斷是否在最頂部
if (sv.getScrollY() == 0 && mCurrentViewIndex == 1) {
canPullDown = true;
} else {
canPullDown = false;
}
return mCanScroll;
}
};
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//mMoveLen 手滑動距離,這個是控制布局的主要變量
mTopView.layout(0, (int) mMoveLen, mViewWidth, mTopView.getMeasuredHeight() + (int) mMoveLen);
mBottomView.layout(0, mTopView.getMeasuredHeight() + (int) mMoveLen,
mViewWidth, mTopView.getMeasuredHeight() + (int) mMoveLen + mBottomView.getMeasuredHeight());
}
一開始初始化的時候,就是把TopView和BottomView分別繪制進來,把BottomView繪制在TopView的下面。
3、添加事件分發,也就是dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
if (vt == null) {
vt = VelocityTracker.obtain();
} else {
vt.clear();
}
mLastY = ev.getY();
mLastX = ev.getX();
mTempLastY = ev.getY();
vt.addMovement(ev);
mEvents = 0;
break;
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_POINTER_UP:
mEvents = -1;
break;
case MotionEvent.ACTION_MOVE:
vt.addMovement(ev);
//mCurrentViewIndex:記錄當前展示的是哪個view,0是TopView,1是BottomView
if (canPullUp && mCurrentViewIndex == 0 && mEvents == 0) {
mMoveLen += (ev.getY() - mLastY);//不斷計算滑動的距離
if (mMoveLen > 0) {
mMoveLen = 0;
mCurrentViewIndex = 0;
} else if (mMoveLen < -mViewHeight) {
mMoveLen = -mViewHeight;
mCurrentViewIndex = 1;
}
if (mMoveLen < -5) {
// 防止事件沖突
mCanScroll = true;
} else {
mCanScroll = false;
}
} else if (canPullDown && mCurrentViewIndex == 1 && mEvents == 0) {
mMoveLen += (ev.getY() - mLastY);
if (mMoveLen < -mViewHeight) {
mMoveLen = -mViewHeight;
mCurrentViewIndex = 1;
} else if (mMoveLen > 0) {
mMoveLen = 0;
mCurrentViewIndex = 0;
}
if (mMoveLen > 5 - mViewHeight) {
// 防止事件沖突
mCanScroll = true;
} else {
mCanScroll = false;
}
} else {
mCanScroll = false;
mEvents++;
}
mLastY = ev.getY();
requestLayout();
break;
case MotionEvent.ACTION_UP:
double deltaX = Math.sqrt((ev.getX() - mLastX) * (ev.getX() - mLastX) + (ev.getY() - mTempLastY) * (ev.getY() - mTempLastY));
if (deltaX < 10) {
mIsClick = true;
}
if (mIsClick) {
mIsClick = false;
break;
}
mLastY = ev.getY();
vt.addMovement(ev);
//你想要指定的得到的速度單位,如果值為1,代表1毫秒運動了多少像素。如果值為500,代表 0.5秒內運動了多少像素
vt.computeCurrentVelocity(500);
// 獲取Y方向的速度 可以通過getXVelocity()和getYVelocity()獲得橫向和豎向的速率
int initialVelocity = (int) vt.getYVelocity();
if (mMoveLen != 0 && mMoveLen != mTempMoveLen) {
fling(-initialVelocity);
}
mTempMoveLen = mMoveLen;
try {
vt.recycle();
vt = null;
} catch (Exception e) {
e.printStackTrace();
}
break;
}
super.dispatchTouchEvent(ev);
return super.dispatchTouchEvent(ev);
}
這里可以看到最后是return super.dispatchTouchEvent(ev),這是為了讓子View也可以接收到事件。
這里的VelocityTracker,就是用來計算手滑動的速度,這個要用來當TopView要過渡到BottomView的時候,模擬ScrollView的滾動慣性的。
模擬ScrollView滾動慣性關鍵就在于Scroller,我們可以看到MotionEvent.ACTION_UP的時候,有這樣的方法:fling(-initialVelocity);
public void fling(int velocityY) {
if (getChildCount() > 0) {
mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, -700, 700);
awakenScrollBars(mScroller.getDuration());
invalidate();
}
}
這里的 -700和700分別是往上滾動和往下滾動的最大值。
然后重寫computeScroll方法
/**
* 當scrollY > 0時,是往下滑動,當scrollY < 0時,是往上滑動
*/
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset() && mMoveLen != (-mScreenH + getStatusBarHeight(getContext()))) {
int scrollX = mScroller.getCurrX();
int scrollY = mScroller.getCurrY();
//往上滑動
if (scrollY > 0 && oldY < 0) {
oldY = 0;
}
if (scrollY > 0 && scrollY < oldY) {
oldY = 0;
}
//往下滑動
if (scrollY < 0 && scrollY > oldY) {
oldY = 0;
}
if (scrollY < 0 && oldY > 0) {
oldY = 0;
}
if (mMoveLen != 0 || mMoveLen != (-mScreenH + getStatusBarHeight(getContext()))) {
scrollTo(scrollX, scrollY);
}
if (scrollY > 0) {
mMoveLen = mMoveLen - scrollY + oldY;
}
if (scrollY < 0) {
mMoveLen = mMoveLen - scrollY + oldY;
}
if (mMoveLen > 0) {
mMoveLen = 0;
}
if (mMoveLen <= (-mScreenH + getStatusBarHeight(getContext()))) {
mMoveLen = -mScreenH + getStatusBarHeight(getContext());
}
oldY = scrollY;
if (mMoveLen != 0 || mMoveLen != (-mScreenH + getStatusBarHeight(getContext()))) {
requestLayout();
}
}
}
這里進行了一系列判斷,當時也是累啊,首先是要判斷是往上滑動還是往下滑動,再來就是判斷最大的滑動距離,然后再進行重寫scrollTo方法。
@Override
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
invalidate();
}
}
}
這里使用到的是onScrollChanged方法進行View的滾動,然后對View進行invalidate,這樣就可以達到滾動的慣性了。
整個代碼的流程就是這樣的了,實現了兩個ScrollView同時在一個界面內,而且可以做到平滑的滑動,具有ScrollView本身的滑動慣性,而且不局限于ScrollView,這要是有滾動條的View都可以使用。
總結
整個代碼看下來是不是有點凌亂,我也覺得有點亂,不過只要有耐心點就可以很快理解了。
首先,一個自定義RelativeLayout,然后把兩個包含ScrollView的子View同時繪制在這個自定義的RelativeLayout內,并且是上下排列,然后重寫dispatchTouchEvent方法,對事件進行處理,主要是在TopView和BottomView過渡的時候進行處理,要判斷是TopView往BottomView過渡還是BottomView往TopView過渡,這個是關鍵點,最后就使用到Scroller進行滾動慣性的模擬,需要重寫View的computeScroll和scrollTo兩個方法,然后在滾動的時候需要判斷是否超出了滾動的距離,還有滾動的方向。
來自:http://www.jianshu.com/p/0fa34bbd0b65